feat(app-iced): M101 r2 设置页状态漏斗 + pages/mod.rs 拆分 + r1 复审订正
r1 复审的 P1:消息漏斗只解决了「加一支消息」,一组要画真值仍要改 `settings/mod.rs` (`Group.body` 与 `view` 只认 `appearance::State`)。本 commit 补齐与消息漏斗同构的状态漏斗: - 新增 `pages/settings/state.rs`:`Groups`(一个组一个字段,字段类型在组自己文件里)+ `States`(按值传给渲染器的只读视图:外观状态 + 组状态)+ `demo_groups()`(harness 的 `'static` 默认状态)。 - `Group.body` 签名改成 `for<'a> fn(&'a Ui, States<'a>) -> Option<Element>`:每组自己从 `States` 取自己要的数据,「没有可画的状态」返回 `None` 由 `form` 退回演示行。 `appearance` 组照旧接 `pages/appearance/section.rs`(M93 路径行为不变)。 - `App.settings: pages::settings::Groups`(`app/state.rs` 字段列表末尾)+ `app/view.rs` 构造 `States` 走新入口 `pages::page_with_settings`。加/改某一组的字段只改那一组的文件。 - 四个组各自的 `State` 落在各自文件;`java::Message` 留一支只有测试构造的 `Ping`,配 `app/update/settings/tests.rs` 的 `a_group_message_reaches_its_handler_through_the_funnel` ——消息漏斗第一次有**运行时**证据。 r1 其余条目: - P2-1 删掉 `only_the_appearance_group_carries_a_real_control_body`(被四条 per-group 断言蕴含), 并在同处写明删掉之后「唯一性」没人守;三份 section 文档从「改 3 个文件」订正为 2 个。 - P2-2 `pub mod settings` → `pub(crate) mod settings`(四个消息口模块 `pub(crate)`), 并订正两处理由(lint 按类型自身可见性判,不是可达性)。 - P2-3 `pages/mod.rs` 397 行 → 拆出 `pages/message.rs` / `pages/screen.rs`(纯搬移,`pub use` 保证 `pages::Message` / `Action` / `Screen` 路径不变),并删掉 M101 自己那 18 行重复文档。 - P3-1 订正共享点清单(`Key` 由宏从域文件累积生成,加 key 不改 `catalog.rs`)。 - P3-2 守门改成「除已知臂外任何含 `=>` 的行都算兜底」(`other =>` 也拦),并加「恰好四臂」断言。 - P3-3 `ai.rs` → `ai_about.rs` 单独一条 R100 改名提交,`git log --follow` 不断链。 - P3-4 `message.rs` 的「加一支消息改 1 个文件」订正为 2 个。
This commit is contained in:
1 parent
423ea77758
commit
029ece3f17
25 files changed
+1004
-240
No files matched your search
@@ -43,6 +43,7 @@
|
||||
//! | [`model`] | **视图模型**:presenter 快照 → 页面要画的东西(页面层不认识 presenter 类型) |
|
||||
//! | [`frame`] | 页头 / 内容列 / 两列骨架 / 空态 / 卡片 / 分隔线 |
|
||||
//! | [`nav`] | 导航轨的 `NAV` 表 + 部件版 `rail()`(harness / 单测;产品走 `shell/nav.rs`) |
|
||||
//! | [`message`] / [`screen`] | `pages::Message` / `Action` / `Screen`(M101 r2 从本文件原样搬出) |
|
||||
//! | `instances` | 实例页(list-detail)——**M90 唯一打通到真组件 + 真数据真命令的页** |
|
||||
//! | `settings` | 设置页(supporting-pane,占位视觉):分组注册表 + 每个分组一个文件 |
|
||||
//! | `download` / `logs` | 下载 / 日志页(单列,占位视觉) |
|
||||
@@ -55,6 +56,14 @@
|
||||
//! 于是「页面里点一行」= 「压一层栈」,语义与 presenter 的 `Router` 天然一致,
|
||||
//! 中间不再有一层「页面自定义的选中项」需要同步。
|
||||
//!
|
||||
//! ### 设置页只有一个投递口(M101 `SETTINGSMSG`)
|
||||
//!
|
||||
//! 设置页的四条填页线各自在自己的文件里定义消息与状态,本文件的 [`Message`] 只留一个
|
||||
//! [`Message::Settings`](详见 `pages/settings/mod.rs` 的「消息 / 状态」两节);`Message` 与
|
||||
//! `Screen` 本身也在 M101 r2 搬到 [`message`] / [`screen`](`pages/mod.rs` 合并后 397 行,只剩
|
||||
//! 3 行余量),这里的 `pub use` 保证了 `pages::Message` / `pages::Action` / `pages::Screen`
|
||||
//! 三条路径一字未变。
|
||||
//!
|
||||
//! ## 偏离登记(D30 要求逐条写清;M84 六条 + M90 三条)
|
||||
//!
|
||||
//! | # | 偏离 | 为什么 | 什么时候收口 |
|
||||
@@ -103,179 +112,50 @@ pub mod model;
|
||||
mod download;
|
||||
mod instances;
|
||||
mod logs;
|
||||
mod message;
|
||||
mod nav;
|
||||
mod settings;
|
||||
mod screen;
|
||||
// M101:设置页的两条漏斗(消息 / 状态)都从这个模块树里出——app 层要构造
|
||||
// `settings::GroupMessage`、借用 `settings::Groups`,所以最小暴露面是 `pub(crate)`。
|
||||
// **不是** `pub`:`private_interfaces` 按**类型自身的声明可见性**判(四个 `Message` 枚举与
|
||||
// `GroupMessage` 声明成 `pub` 就够),把整个设置页模块树暴露成 crate 公开 API 没有回报。
|
||||
pub(crate) mod settings;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub use download::{Category, Download};
|
||||
pub use layout::{FrameMetrics, PageLayout, PaneKit, WidthClass};
|
||||
pub use logs::{Confidence, Finding, Level, Line, Logs, Outcome};
|
||||
pub use message::{Action, Message};
|
||||
pub use model::{Detail, Instances, Meta, Model, Row};
|
||||
pub use screen::Screen;
|
||||
|
||||
use iced::widget::{column, container, row, space};
|
||||
use iced::{Element, Length};
|
||||
|
||||
use unlucky_design::elevation::MaterialLayer;
|
||||
use unlucky_presenter::router::{NavSection, Route};
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use frame::filled;
|
||||
|
||||
/// 页面消息。
|
||||
///
|
||||
/// **故意不用 `crate::app::Message`**(组件库契约 §2 的同一条理由):页面不绑 app 的状态层,
|
||||
/// 接线时由 app 层把这里的三类意图翻成 presenter 的 `AppCommand` / `Route`。
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
/// 骨架期动作(三页的占位按钮 + 实例页上还没接后端的两个页头动作)。
|
||||
Action(Action),
|
||||
/// 路由跳转:导航轨、列表行、窄窗「返回」都走这一条(栈语义归 `presenter::router`)。
|
||||
Navigate(Route),
|
||||
/// 启动某实例(app 层翻成 `AppCommand::LaunchInstance`)。
|
||||
Launch(String),
|
||||
/// 停止某实例(app 层翻成 `AppCommand::StopInstance`)。
|
||||
Stop(String),
|
||||
/// 外观项改动(设置页「外观」分组 → `App` 的状态与落盘)。
|
||||
///
|
||||
/// 载荷是**用户请求值**(未钳位):钳位与 CPU 档降级在 `appearance::alpha` 里做,
|
||||
/// 页面只表达意图(与组件库「不持有业务状态」同一条纪律)。
|
||||
Appearance(appearance::Change),
|
||||
/// 选中下载页的一个版本(M94):**页面内选中**,app 层只改视图模型,不 dispatch。
|
||||
SelectVersion(String),
|
||||
/// 安装指定版本(M94;app 层翻成 `AppCommand::InstallVersion`)。
|
||||
InstallVersion(String),
|
||||
/// 取消正在跑的安装(M94;`AppCommand::CancelInstall`)。
|
||||
CancelInstall,
|
||||
/// 切换下载页分类(M94;app 层翻成 `AppCommand::ListVersions`,`All` 只画现有快照)。
|
||||
SetCategory(Category),
|
||||
/// 把现成文本塞进剪贴板(**M95**:诊断卡片的一行摘要,文本由页面造,app 只搬)。
|
||||
CopySummary(String),
|
||||
}
|
||||
|
||||
/// 页面动作清单(骨架期够用就行;接线时按业务语义重命名/扩充)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Action {
|
||||
/// 新建实例(**还没有后端**,见模块文档偏离 #8)。
|
||||
NewInstance,
|
||||
/// 导入(同上)。
|
||||
Import,
|
||||
/// 返回实例列表(窄窗整页切换)。**已由 `Message::Navigate(Route::Instances)` 取代**,
|
||||
/// 这里保留枚举项是为了别把「返回」这件意图从动作表里抹掉(外壳回退路径仍可能用到)。
|
||||
Back,
|
||||
/// 启动(占位动作表里的「启动」;真启动走 `Message::Launch`)。
|
||||
Play,
|
||||
/// 更多。
|
||||
More,
|
||||
/// 复制实例。
|
||||
Duplicate,
|
||||
/// 打开实例目录。
|
||||
OpenFolder,
|
||||
/// 删除实例。
|
||||
Delete,
|
||||
/// 重新探测下载源。
|
||||
RefreshSources,
|
||||
/// 浏览版本列表。
|
||||
BrowseVersions,
|
||||
/// 导入整合包。
|
||||
ImportModpack,
|
||||
/// 检查下载源。
|
||||
CheckSources,
|
||||
/// 打开日志目录。
|
||||
OpenLogs,
|
||||
/// 复制诊断信息。
|
||||
CopyDiagnostics,
|
||||
/// 清空日志。
|
||||
ClearLogs,
|
||||
/// 重置当前设置分组。
|
||||
ResetSection,
|
||||
/// 保存设置。
|
||||
SaveSettings,
|
||||
/// 新增逐实例覆盖。
|
||||
AddOverride,
|
||||
}
|
||||
|
||||
/// 四个页面。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Screen {
|
||||
/// 实例(list-detail)。
|
||||
Instances,
|
||||
/// 下载(单列)。
|
||||
Download,
|
||||
/// 设置(supporting-pane)。
|
||||
Settings,
|
||||
/// 日志(单列,两行页头)。
|
||||
Logs,
|
||||
}
|
||||
|
||||
impl Screen {
|
||||
/// 全部页面(截图矩阵按这个顺序铺)。
|
||||
pub const ALL: [Screen; 4] = [
|
||||
Screen::Instances,
|
||||
Screen::Download,
|
||||
Screen::Settings,
|
||||
Screen::Logs,
|
||||
];
|
||||
|
||||
/// 稳定 slug(命令行参数与文件名用)。
|
||||
pub const fn slug(self) -> &'static str {
|
||||
match self {
|
||||
Screen::Instances => "instances",
|
||||
Screen::Download => "download",
|
||||
Screen::Settings => "settings",
|
||||
Screen::Logs => "logs",
|
||||
}
|
||||
}
|
||||
|
||||
/// 反向解析(`--screen=instances`)。
|
||||
pub fn from_slug(slug: &str) -> Option<Self> {
|
||||
Self::ALL.into_iter().find(|screen| screen.slug() == slug)
|
||||
}
|
||||
|
||||
/// 页面声明的**最宽**版面;实际用哪个由 [`layout::resolve`] 按窗口宽度决定。
|
||||
pub const fn kit(self) -> PaneKit {
|
||||
match self {
|
||||
Screen::Instances => PaneKit::ListDetail,
|
||||
Screen::Settings => PaneKit::Supporting,
|
||||
Screen::Download | Screen::Logs => PaneKit::Single,
|
||||
}
|
||||
}
|
||||
|
||||
/// 路由落在哪一页上(`None` = 这一页还没有实现,由 app 层决定回退)。
|
||||
///
|
||||
/// `Route::Detail(_)` 仍属实例页(窄窗时是「整页切换」的那一屏)。
|
||||
pub fn of(route: &Route) -> Option<Self> {
|
||||
Some(match route {
|
||||
Route::Instances | Route::Detail(_) => Screen::Instances,
|
||||
Route::Download => Screen::Download,
|
||||
Route::Logs => Screen::Logs,
|
||||
Route::Settings => Screen::Settings,
|
||||
Route::Agent => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// 该页高亮的导航项(与 `Route::nav` 同源,但这里是「按页问」的口径)。
|
||||
pub const fn nav(self) -> NavSection {
|
||||
match self {
|
||||
Screen::Instances => NavSection::Instances,
|
||||
Screen::Download => NavSection::Download,
|
||||
Screen::Logs => NavSection::Logs,
|
||||
Screen::Settings => NavSection::Settings,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// **单页内容**(不含外壳):交给 `shell::view_with_content` 塞进真标题栏与导航轨之间。
|
||||
///
|
||||
/// 产品路径(`app/view.rs`)走 [`page_with_appearance`]:外壳由 `shell` 画(拖动/缩放/三个
|
||||
/// 产品路径(`app/view.rs`)走 [`page_with_settings`]:外壳由 `shell` 画(拖动/缩放/三个
|
||||
/// caption 键、rail 88),这里只画「页头 + 内容列」。列宽仍按**整个窗口宽**算
|
||||
/// (`layout::frame_metrics` 内部会扣掉 rail 88 与页边距),所以外壳换了不影响版面判据。
|
||||
///
|
||||
/// 视图模型由**调用方持有**(`App` 在状态变化时重建一次 `Model`),这里只借用:
|
||||
/// `view(&self)` 不能返回一棵借用局部变量的树。
|
||||
pub fn page<'a>(ui: &'a Ui, model: &'a Model) -> Element<'a, Message> {
|
||||
render(ui, model, None)
|
||||
render(
|
||||
ui,
|
||||
model,
|
||||
settings::States {
|
||||
appearance: None,
|
||||
groups: settings::demo_groups(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// 同 [`page`],但带上**外观状态**(设置页的「外观」分组要读当前值与诊断)。
|
||||
@@ -283,25 +163,44 @@ pub fn page<'a>(ui: &'a Ui, model: &'a Model) -> Element<'a, Message> {
|
||||
/// 为什么另开一个入口而不是给 `Model` 加字段:`pages/model.rs` 是页面与 presenter 之间的展示
|
||||
/// 契约(`Model` 只装「要画什么」),外观是**运行时状态**(滑块请求值、已解码的背景图、诊断),
|
||||
/// 塞进去会让演示数据(`pages::demo`)也得伪造一份外观状态。
|
||||
///
|
||||
/// 这条入口拿的是**演示组状态**([`settings::demo_groups`]):页面层没有真组状态可用,
|
||||
/// 产品路径走 [`page_with_settings`]。
|
||||
pub fn page_with_appearance<'a>(
|
||||
ui: &'a Ui,
|
||||
model: &'a Model,
|
||||
appearance: &'a appearance::State,
|
||||
) -> Element<'a, Message> {
|
||||
render(ui, model, Some(appearance))
|
||||
render(
|
||||
ui,
|
||||
model,
|
||||
settings::States {
|
||||
appearance: Some(appearance),
|
||||
groups: settings::demo_groups(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render<'a>(
|
||||
/// 产品路径:带上**设置页那份只读状态**(外观运行期状态 + 各组自己的状态)。
|
||||
///
|
||||
/// 为什么要单独一个入口:组状态的所有者是 `App`(`app/state.rs` 的 `settings` 字段),页面层
|
||||
/// 只借用一帧。加/改某一组的字段不改变这条签名(`States` 里只有 `&Groups`),所以四条填页线
|
||||
/// 都不必来这里动一行。
|
||||
pub fn page_with_settings<'a>(
|
||||
ui: &'a Ui,
|
||||
model: &'a Model,
|
||||
appearance: Option<&'a appearance::State>,
|
||||
states: settings::States<'a>,
|
||||
) -> Element<'a, Message> {
|
||||
render(ui, model, states)
|
||||
}
|
||||
|
||||
fn render<'a>(ui: &'a Ui, model: &'a Model, states: settings::States<'a>) -> Element<'a, Message> {
|
||||
let metrics = layout::frame_metrics(model.screen.kit(), model.window_width);
|
||||
|
||||
match model.screen {
|
||||
Screen::Instances => instances::view(ui, metrics, model),
|
||||
Screen::Download => download::view(ui, metrics, &model.download, unlucky_i18n::Lang::En),
|
||||
Screen::Settings => settings::view(ui, metrics, appearance),
|
||||
Screen::Settings => settings::view(ui, metrics, states),
|
||||
Screen::Logs => logs::view(ui, metrics, &model.logs, model.lang),
|
||||
}
|
||||
}
|
||||
@@ -326,7 +225,15 @@ pub fn app_with_appearance<'a>(
|
||||
model: &'a Model,
|
||||
appearance: Option<&'a appearance::State>,
|
||||
) -> Element<'a, Message> {
|
||||
let body = render(ui, model, appearance);
|
||||
// harness 路径没有真组状态:用演示状态([`settings::demo_groups`]),外观状态照传。
|
||||
let body = render(
|
||||
ui,
|
||||
model,
|
||||
settings::States {
|
||||
appearance,
|
||||
groups: settings::demo_groups(),
|
||||
},
|
||||
);
|
||||
|
||||
let shell = row![nav::rail(ui, model.active), body]
|
||||
.width(Length::Fill)
|
||||
|
||||
Reference in new issue
Block a user