diff --git a/crates/app-iced/src/app/update/settings/tests.rs b/crates/app-iced/src/app/update/settings/tests.rs index e457a52..150f2ba 100644 --- a/crates/app-iced/src/app/update/settings/tests.rs +++ b/crates/app-iced/src/app/update/settings/tests.rs @@ -30,8 +30,10 @@ //! 抓不住(老实列出来): //! 1. **嵌套 match 里的兜底**——深度 ≥2 的臂不看。这是刻意的:嵌套 match 的穷尽性由它自己的类型 //! 保证,跟 `GroupMessage` 漏接线无关;代价是没人守「嵌套 match 自己写了个 `_ =>`」; -//! 2. **跨行 pattern**(除 `|` 续行)——把 struct 字段拆成多行、`=>` 落在第 3 行那种写法会被 -//! 当成「前缀不对」而误判兜底。当前代码风格不出现;真出现时放宽规则,别把守门删掉; +//! 2. **跨行 pattern 分两种,别当一种**:以 `|` 开头的 or-pattern 续行**放行**(不误红); +//! 但**花括号形状**的多行 pattern(例如 `Message::SetRuntime {` 折成多行、`=>` 落在后面) +//! 会被**静默跳过**——那一行不带 `=>`,于是既不算臂也不算违规(臂数 0、零违规),是**漏检** +//! 而不是误红。两种都是洞,当前代码风格都不出现;真要用这种写法时把规则补上,别把守门删掉; //! 3. **字符串/字符字面量里的 `{` `}`** 会扰乱深度计数(本文件与四个 handler 都没有这种写法); //! 4. handler 忽略消息(`let _ = message; Task::none()`)、两个 handler 身体写反或写成一样 //! ——类型对得上就编得过,只有人读能看出来; diff --git a/crates/app-iced/src/pages/settings/message.rs b/crates/app-iced/src/pages/settings/message.rs index 1e9e557..be4fcdd 100644 --- a/crates/app-iced/src/pages/settings/message.rs +++ b/crates/app-iced/src/pages/settings/message.rs @@ -42,7 +42,8 @@ //! ## 测试 //! //! 本文件的 [`tests::each_group_owns_its_own_message_type`];dispatch 侧的 -//! `the_group_dispatch_has_no_wildcard_fallback` 在 `app/update/settings.rs`。 +//! `the_group_dispatch_has_no_catch_all_arm` 在 `app/update/settings/tests.rs`(`app/update/settings.rs` +//! 只是 `#[cfg(test)] mod tests;` 的声明点,别去那里找)。 use super::{ai_about, download, java, language_appearance}; diff --git a/crates/app-iced/src/pages/settings/mod.rs b/crates/app-iced/src/pages/settings/mod.rs index 6fc2d7b..ce36e2d 100644 --- a/crates/app-iced/src/pages/settings/mod.rs +++ b/crates/app-iced/src/pages/settings/mod.rs @@ -90,27 +90,35 @@ //! 2. 本文件的 [`GROUPS`] —— 只有 ai+about 线**上第 5 组**时加一行(页面上多一组本来就是整页的变化)。 //! 3. `tests/mod.rs` —— ai+about 线新建 `tests/ai_section.rs` 时要加一行 `mod ai_section;` //! (测试文件不挂 `mod` 就不编译),以及「左列顺序」那条字面量断言会连带红,需要人确认。 -//! 4. **读页外真值**(r2 复审 P1 补的,上一版漏了)—— [`States`] 只有 `appearance` + `groups` 两格, -//! 组正文读不到 `pages::Model`,也读不到 presenter 的设置快照。最典型的是 language+appearance 线: -//! 「当前语言」的唯一来源是 `Model.lang`(`pages/model.rs`),General 分组要显示真值就得让值进 -//! `States`,那要改 `settings/state.rs` + `pages/mod.rs`(`render` 现在只拿 `model` 算 metrics, -//! **2 个共享文件**);或者反过来在 `app/state.rs` 用真值初始化 `Groups.language_appearance` -//! (**1 个共享文件**)。建议形状:**一次性**给 `States` 加一格 `model: &'a Model`(并让 -//! `render` 把 `model` 传下去),此后四条线都不必再碰这两个文件——这是「谁先撞谁付一次钱」的 -//! 那类代价,不是永久性的四路撞车。 +//! 4. **读页外真值**(r2 复审 P1 补的)—— [`States`] 只有 `appearance` + `groups` 两格,组正文读不到 +//! `pages::Model`(「当前语言」的值在 `Model.lang`,`pages/model.rs`),也读不到 presenter 的设置 +//! 快照。两条路的**真实成本**(`States` 没有 `Default`,加字段必然 E0063,已实测): +//! - 把 `model` **塞进 [`States`]** ⇒ struct literal 有 **4 处**(`pages/mod.rs:154/177/232` 与 +//! `app/view.rs:70`),四处都要补字段 ⇒ **3 个共享文件**:`settings/state.rs`、`pages/mod.rs`、 +//! **`app/view.rs`**(最后这个正是 M104 的落点,本 mission 反复设锚点要避开的就是它); +//! - 给 `settings::view` **加一个 `model: &Model` 形参**(`pages/mod.rs` 的 `render` 本来就拿着 +//! `model`,直接往下传)⇒ **2 个共享文件**(`settings/mod.rs` + `pages/mod.rs`), +//! `app/view.rs` 不用动 —— **推荐这个形状**。 +//! +//! 顺带订正 r3 之前那句「在 `app/state.rs` 用真值初始化 `Groups.language_appearance`(1 个文件)」: +//! 今天 `App` / `app/state.rs` 里**没有**任何语言真值源(`presenter::prefs::AppearancePrefs` 无 +//! language 字段;唯一的值是 `app/view.rs:128` 里硬编码的 `Lang::En`),要先有真值源才谈得上 +//! 初始化位置。两种形状都是「谁先撞谁付一次钱」的一次性代价,不是永久性四路撞车。 //! 5. **首帧取数 / 异步发起**(同上,r2 复审 P1 补)—— [`Body`] 是 -//! `fn(&Ui, States) -> Option`,**返回不了 `Task`**,而 `view` 是纯函数:一组想在自己 -//! 的首帧就发起探测(java 的「探测本机 JVM」、download 的「测源可用性」)有两条路: -//! ①在 `app/update.rs` 的 `Message::Boot` 分支里补一次首帧派发(**1 个共享文件**); -//! ②把 `Body` 的返回类型改成 `Option<(Element, Task)>` 或 `Option` 旁路 -//! (改本文件 + `form`,**1 个共享文件**,一次性)。不改的代价是:首屏显示演示值,等用户点一次 -//! 才变真值——四条线都能接受,就不必动。 +//! `fn(&Ui, States) -> Option`,返回不了 `Task`;而且 `settings::view` → `pages::render` +//! → `App::view` **一路都返回 `Element`**(iced 0.14 的 `view` 也只返回 `Element`,`view(&self)` +//! 又改不了状态),**view 期间产出的 `Task` 没有出口**。所以只有一条路:在 `app/update.rs` 的 +//! `Message::Boot` 分支补一次首帧派发(**1 个共享文件**——`on_boot` 本来就在派 +//! `AppCommand::RefreshAll`)。想让「组自己」发起首帧探测,得先在 app 侧另造一条旁路 +//! (`settings/mod.rs` + `pages/mod.rs` + `app/view.rs` 三处都要动),**不建议**。不改的代价: +//! 首屏显示演示值、等用户点一次才变真值——四条线都能接受就不必动。 //! 6. **隐性代价:`Groups` 必须是 `Sync + Default`**(r2 复审 P3 指出)—— //! [`demo_groups`] 用 `static LazyLock` 撑着 harness 路径,所以某个组的 `State` 里一旦 //! 放了 `Rc` 这类非 `Sync` 字段,编译会在**本模块**(`settings/state.rs`)红。选择 `LazyLock` //! 是为了让 `page()` / `page_with_appearance()` 的返回 `Element<'a>` 借得到一个 `'static` 的默认 //! 状态;如果哪天必须放非 `Sync` 状态,改法是把默认状态改成「每个调用点自己造一个 `Groups`」 -//! (harness 侧改两处,同样是**一次性**代价)。 +//! ——`pages/mod.rs` 的**三个**调用点都要改(`page` / `page_with_appearance` / +//! `app_with_appearance`,行 156 / 179 / 234),同样是**一次性**代价。 // 四条填页线的文件要能被**各自那条落点**(`app/update/settings/<线>.rs`)与状态架子 // (`state::Groups`)命名,所以是 `pub(crate)`:最小面,不是 crate 公开 API。 diff --git a/crates/app-iced/src/pages/settings/state.rs b/crates/app-iced/src/pages/settings/state.rs index 0e87ad6..44113a8 100644 --- a/crates/app-iced/src/pages/settings/state.rs +++ b/crates/app-iced/src/pages/settings/state.rs @@ -76,8 +76,9 @@ pub struct States<'a> { /// /// **隐性代价(r2 复审 P3)**:`static` 要求 `Groups: Sync + Default`,于是某个组的 `State` 里一旦 /// 放了 `Rc` 这类非 `Sync` 字段,编译会红在**本文件**(共享)而不是那一组自己的文件。真需要 -/// 非 `Sync` 状态时的改法:去掉这个 `static`,改成每个 harness 调用点自己造一份 `Groups` -/// (`pages/mod.rs` 的两个入口各一处,一次性代价),见 [`super`] 文档「仍然共享」第 6 条。 +/// 非 `Sync` 状态时的改法:去掉这个 `static`,改成每个调用点自己造一份 `Groups`——`pages/mod.rs` 的 +/// **三个**调用点都要改(`page` / `page_with_appearance` / `app_with_appearance`,行 156 / 179 / 234), +/// 一次性代价,见 [`super`] 文档「仍然共享」第 6 条。 pub fn demo_groups() -> &'static Groups { static DEMO: LazyLock = LazyLock::new(Groups::default); &DEMO diff --git a/crates/app-iced/src/pages/settings/tests/appearance_section.rs b/crates/app-iced/src/pages/settings/tests/appearance_section.rs index f0aeaf5..c32efa6 100644 --- a/crates/app-iced/src/pages/settings/tests/appearance_section.rs +++ b/crates/app-iced/src/pages/settings/tests/appearance_section.rs @@ -3,7 +3,7 @@ //! //! **归 language+appearance 线**:这是设置页唯一接了真控件的一组,真控件实现全在 //! `pages/appearance/section.rs`;本文件只守「注册表这一侧的接线」。 -//! 本组已经把 `body` 接上了;`tests/mod.rs` 那条「只有一组有真控件」的**聚合绞线已在 M101 r2 删除** +//! 本组已经把 `body` 接上了;`tests/mod.rs` 那条「只有一组有真控件」的**聚合绊线已在 M101 r2 删除** //! (被四条 per-group 断言完全蕴含——代价是「唯一性」从此没人守,那段说明写在 `tests/mod.rs` 同处)。 use super::super::appearance; @@ -46,7 +46,7 @@ fn the_appearance_group_keeps_its_icon() { fn the_appearance_group_keeps_a_real_control_body() { assert!( appearance::GROUP.body.is_some(), - "外观分组必须是挂了真控件的那一组(`settings/appearance.rs` 的 `body: Some(appearance::body)`;\ - 具体渲染器由那一行源码 + 截图担保,函数指针相等性断言不了)" + "外观分组必须是挂了真控件的那一组(`settings/appearance.rs` 里是 `body: Some(body)`,\ + `body` 是本文件那个本地 fn;具体渲染器由那一行源码 + 截图担保,函数指针相等性断言不了)" ); }