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:
AskaEth committed 2026-11-03 20:57:08 +08:00
1 parent 423ea77758
commit 029ece3f17
25 files changed
+1004 -240

No files matched your search

@@ -0,0 +1,79 @@
//! 设置页的**状态漏斗**(M101 r2):与 [`super::message`] 的消息漏斗同构——一个组的状态归它自己的
//! 文件,[`Groups`] 只是一个按组排开的架子。
//!
//! ## 为什么要这一层(r1 复审的 P1)
//!
//! 消息漏斗解决了「加一支消息要改哪些文件」,但一组要画**真值**就得有数据进渲染器:r1 之前
//! `settings::view` 只收 `Option<&appearance::State>`,于是 java / download / language+appearance
//! 三条线要显示「当前运行时 / 当前并发数 / 当前语言」时,**都得改 `settings/mod.rs`**;带运行期状态
//! 的还要改 `pages/mod.rs` / `app/view.rs` / `app/state.rs`。四条线一铺开就是第二处四路撞车。
//!
//! 现在的形状:
//!
//! ```text
//! App.settings: pages::settings::Groups ← 唯一的所有者(app/state.rs 一行)
//! ├─ java: java::State → pages/settings/java.rs
//! ├─ download: download::State → pages/settings/download.rs
//! ├─ language_appearance: language_appearance::State → pages/settings/language_appearance.rs
//! └─ ai_about: ai_about::State → pages/settings/ai_about.rs
//!
//! settings::view(ui, metrics, States { appearance, groups }) ← 只读视图,按值传
//! └─ 每组的 GROUP.body(ui, states) 自己从 states 里取自己要的那份
//! ```
//!
//! **加/改某一组的字段** = 只改 `pages/settings/<组>.rs`(状态类型在它自己文件里)+ 用它自己的
//! handler 更新(`app/update/settings/<线>.rs`)。本文件、`pages/mod.rs`、`app/state.rs`、
//! `app/view.rs` **都不用动**——本 mission 的这半边判据就是这么验的(见报告「变异证据」)。
//!
//! **加一整条新线**(多一个组)才需要:本文件加一个字段、`message.rs` 加一个变体、
//! `app/update/settings.rs` 加一条臂。那是「加一条线」,不是「加一个字段」。
//!
//! ## 外观那条真控件路径(M93)没有被挤掉
//!
//! `appearance::State` 仍是 `App` 的字段(不复用、不搬走),[`States::appearance`] 原样借用它;
//! 「没有外观状态(harness)⇒ 画演示行」这条判据现在由 `appearance` 组自己的 `GROUP.body`
//! 表达(返回 `None` ⇒ [`super::frame::demo_card`]),与 r1 之前逐像素等价。
use std::sync::LazyLock;
use crate::pages::appearance;
use super::{ai_about, download, java, language_appearance};
/// 各组自己拥有的运行期状态:**一个组一个字段**,字段类型定义在那条线自己的文件里。
///
/// 字段的**内部**结构归各组(加字段只改它自己的文件);架子(有哪几个组)归这里。
#[derive(Debug, Default)]
pub struct Groups {
/// Java 分组的状态(`pages/settings/java.rs`)。
pub java: java::State,
/// Downloads 分组的状态(`pages/settings/download.rs`)。
pub download: download::State,
/// 语言 + 外观两组的状态(`pages/settings/language_appearance.rs`)。
pub language_appearance: language_appearance::State,
/// AI / About 分组的状态(`pages/settings/ai_about.rs`)。
pub ai_about: ai_about::State,
}
/// 画一帧时**按值**递给各组渲染器的只读视图:组状态 + 外观运行期状态(两个引用,`Copy`)。
///
/// 为什么按值而不是按引用:`Settings::view` 返回的 `Element<'a>` 会借走它,按引用传会逼着
/// harness 那条路径([`super::demo_groups`] 撑着的 `page()` / `page_with_appearance()`)为一份
/// **临时构造**的 `States` 造 `'static`,或者把默认状态塞进 `Model`(两份真相)。按值传没有
/// 这层借用纠缠,调用方写一行就够。
#[derive(Debug, Clone, Copy)]
pub struct States<'a> {
/// 外观运行期状态(M93):`None` = harness / 演示路径(设置页画演示行)。
pub appearance: Option<&'a appearance::State>,
/// 各组自己的状态(见 [`Groups`])。
pub groups: &'a Groups,
}
/// 演示 / harness 路径的默认组状态(`pages::page` 与 `pages::page_with_appearance` 用它)。
///
/// `'static` 是有意的:那两条入口不持有任何组状态,而返回的 `Element<'a>` 会借走这份 `Groups`;
/// 让它活得比调用方长,借用就不会把返回值钉在函数栈上。
pub fn demo_groups() -> &'static Groups {
static DEMO: LazyLock<Groups> = LazyLock::new(Groups::default);
&DEMO
}