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
@@ -18,16 +18,17 @@
|
||||
//!
|
||||
//! ## 文件地图(M99 `SETTINGSFRAME`:**纯搬移**,行为与视觉零变化)
|
||||
//!
|
||||
//! 本文件只做两件事:**分组注册表**([`GROUPS`])+ **内容分发**([`form`] 按分组挑渲染器)。
|
||||
//! 一组的正文、文案、演示行都在它自己的文件里,于是设置页接下来的四条 section 线各占一个文件:
|
||||
//! 本文件只做三件事:**分组注册表**([`GROUPS`])、**内容分发**([`form`] 按分组挑渲染器)、
|
||||
//! 以及 M101 加的两条漏斗的**转发**(消息 → [`message`],状态 → [`state`])。
|
||||
//! 一组的正文、文案、演示行、状态、消息都在它自己的文件里:
|
||||
//!
|
||||
//! | 分组 | 文件 | 谁会动它 |
|
||||
//! | 分组 | 文件(正文 / 状态 / 消息) | 谁会动它 |
|
||||
//! |---|---|---|
|
||||
//! | General | [`general`] | language+appearance 线(语言下拉 + 主题 / 启动页) |
|
||||
//! | Appearance | [`appearance`] | language+appearance 线(真控件已在 `pages/appearance/section.rs`) |
|
||||
//! | Java | [`java`] | java 线 |
|
||||
//! | Downloads | [`download`] | download 线 |
|
||||
//! | (页面上还没有这个分组) | [`ai`] | ai+about 线:先建分组再接线 |
|
||||
//! | (页面上还没有这个分组) | [`ai_about`] | ai+about 线:先建分组再接线 |
|
||||
//! | 页头 / 侧列 / 滚动区 / 演示卡片 / 空态位 | [`frame`] | 谁都不该改(改了就是改版面) |
|
||||
//!
|
||||
//! ## 测试也按 section 分(M100)
|
||||
@@ -40,24 +41,75 @@
|
||||
//! | `tests/appearance_section.rs` | Appearance 自己的行 / 图标 / body 现状 | language+appearance 线 |
|
||||
//! | `tests/java_section.rs` | Java 自己的行 / 图标 / body 现状 | java 线 |
|
||||
//! | `tests/download_section.rs` | Downloads 自己的行 / 图标 / body 现状 | download 线 |
|
||||
//! | `tests/mod.rs` | **跨 section**:版面契约、行宽上限、窄窗退化、注册表顺序 / 分组下标 / 「只有一组有真控件」 | 谁都别随手改(每条都写了为什么不能下放) |
|
||||
//! | `tests/mod.rs` | **跨 section**:版面契约、行宽上限、窄窗退化、注册表顺序 / 分组下标 / 每条线的漏斗安装情况 | 谁都别随手改(每条都写了为什么不能下放) |
|
||||
//!
|
||||
//! 判据:**给某一组加一条新断言 = 只改它自己那个测试文件**。
|
||||
//!
|
||||
//! **加一个分组要动哪里**:① 在它自己的文件里写一个 `pub(super) const GROUP`(名字 / 图标 / 演示行 /
|
||||
//! 可选的真控件渲染器 `body`);② 本文件的 [`GROUPS`] 加一行、数组长度 +1。**不需要**动 `frame.rs`、
|
||||
//! 可选的真控件渲染器 `body`);② 本文件的 [`GROUPS`] 加一行、数组长度 +1;③ [`state::Groups`] 加一个
|
||||
//! 字段、[`message::GroupMessage`] 加一个变体、`app/update/settings.rs` 加一条臂。**不需要**动 `frame.rs`、
|
||||
//! 别的分组文件或 `pages/mod.rs`。
|
||||
//!
|
||||
//! ## 消息:一个投递口,四条线各写各的(M101 `SETTINGSMSG`)
|
||||
//!
|
||||
//! 四条填页线都要往 `pages::Message` 里加变体,而那是**一个**枚举加上
|
||||
//! `app/update/page_messages.rs` 里一条不许 `_ =>` 的 match —— 四路并行必然四路冲突。
|
||||
//! M101 把这件事收成一个投递口:
|
||||
//!
|
||||
//! ```text
|
||||
//! pages::Message::Settings(GroupMessage) ← 全 crate 只有这一个设置页变体
|
||||
//! ├─ Java(java::Message) → settings/java.rs
|
||||
//! ├─ Download(download::Message) → settings/download.rs
|
||||
//! ├─ LanguageAppearance(language_appearance::Message) → settings/language_appearance.rs
|
||||
//! └─ AiAbout(ai_about::Message) → settings/ai_about.rs
|
||||
//! ```
|
||||
//!
|
||||
//! 加一支消息 = 改自己那个消息口文件 + 自己那条落点(`app/update/settings/<线>.rs`),2 个文件、
|
||||
//! 0 个共享文件。四个消息口枚举声明成 `pub` 是为了过 `private_interfaces`:那条 lint 按**类型自身
|
||||
//! 的声明可见性**判,所以模块用最小的 `pub(crate)`(`pages/mod.rs` 的 `pub(crate) mod settings;`)
|
||||
//! 就够,不必把整个设置页模块树暴露成 crate 公开 API。
|
||||
//!
|
||||
//! ## 状态:与消息同构的状态漏斗(M101 r2,r1 复审 P1)
|
||||
//!
|
||||
//! ```text
|
||||
//! App.settings: state::Groups ← app/state.rs 一行(M101 加一次,四条线都不碰)
|
||||
//! ├─ java / download / language_appearance / ai_about:各自的 State 在各自文件里
|
||||
//! States { appearance, groups } ← 只读视图,按值传给 view;每组的 GROUP.body 自己取自己要的那份
|
||||
//! ```
|
||||
//!
|
||||
//! **加/改某一组的字段**只改 `pages/settings/<组>.rs`;**异步结果**在自己那条落点里构造
|
||||
//! `Message::Page(pages::Message::Settings(GroupMessage::<线>(…)))` 回来——`Page` 这条既有通道就是
|
||||
//! 异步结果的入口,`app::Message` / `app/update.rs` 都不用动。
|
||||
//!
|
||||
//! ### 仍然共享、本 mission 无法消除的点(四条线都可能碰到,只能靠 tower 串行合并)
|
||||
//!
|
||||
//! 1. `crates/i18n/src/catalog/golden.rs` —— 每加一条文案 key 都要按声明顺序往金表里插一行
|
||||
//! (`split_keeps_every_translation` 逐条比 key + 英/中译文)。**注意**:`catalog.rs` 的 `Key`
|
||||
//! 枚举由 `catalog!` 宏从各域文件累积生成,加 key **不用**改 `catalog.rs`,也不用改
|
||||
//! `catalog/settings/mod.rs`(五个域已全部接进装配链)。
|
||||
//! 2. 本文件的 [`GROUPS`] —— 只有 ai+about 线**上第 5 组**时加一行(页面上多一组本来就是整页的变化)。
|
||||
//! 3. `tests/mod.rs` —— ai+about 线新建 `tests/ai_section.rs` 时要加一行 `mod ai_section;`
|
||||
//! (测试文件不挂 `mod` 就不编译),以及「左列顺序」那条字面量断言会连带红,需要人确认。
|
||||
|
||||
mod ai;
|
||||
// 四条填页线的文件要能被**各自那条落点**(`app/update/settings/<线>.rs`)与状态架子
|
||||
// (`state::Groups`)命名,所以是 `pub(crate)`:最小面,不是 crate 公开 API。
|
||||
// 分组正文文件 `appearance` / `frame` / `general` 只在设置页内部用,保持私有。
|
||||
pub(crate) mod ai_about;
|
||||
mod appearance;
|
||||
mod download;
|
||||
pub(crate) mod download;
|
||||
mod frame;
|
||||
mod general;
|
||||
mod java;
|
||||
pub(crate) mod java;
|
||||
pub(crate) mod language_appearance;
|
||||
mod message;
|
||||
mod state;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use message::GroupMessage;
|
||||
pub use state::{demo_groups, Groups, States};
|
||||
|
||||
use iced::widget::column;
|
||||
use iced::{Element, Length};
|
||||
|
||||
@@ -66,18 +118,23 @@ use unlucky_design::icons::IconKind;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
|
||||
use crate::pages::appearance::State;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::layout::{FrameMetrics, PageLayout};
|
||||
use super::Message;
|
||||
|
||||
/// 一个分组正文的**真控件渲染器**:需要外观的运行期状态(现在只有「外观」分组用得上)。
|
||||
type Body = for<'a> fn(&'a Ui, &'a State) -> Element<'a, Message>;
|
||||
/// 一个分组正文的**真控件渲染器**:入参是「画一帧时那份只读状态」(组状态 + 外观状态),
|
||||
/// 返回 `None` = 这一组现在没东西可画(例如外观分组在 harness 路径下没有外观状态),
|
||||
/// 由 [`form`] 退回演示行。
|
||||
///
|
||||
/// 各组自己的渲染器**定义在自己文件里**,签名是这一条:于是「这一组要读哪些状态」由它自己决定,
|
||||
/// 本文件不需要认识任何一组的字段(r1 复审 P1 要的正是这一点)。
|
||||
type Body = for<'a> fn(&'a Ui, States<'a>) -> Option<Element<'a, Message>>;
|
||||
|
||||
/// 一个设置分组:名字、图标、演示行、(可选)把这一组正文画成真控件的渲染器。
|
||||
///
|
||||
/// 除 `name`/`icon`/`rows` 外没有别的字段:**分组自己的状态归分组自己**,这里只装「怎么画」。
|
||||
/// 除 `name`/`icon`/`rows`/`body` 外没有别的字段:**分组自己的状态归分组自己**(见 [`state`]),
|
||||
/// 这里只装「怎么画」。
|
||||
struct Group {
|
||||
/// 左列显示名(也是表单列的分组标题)。
|
||||
name: &'static str,
|
||||
@@ -104,22 +161,21 @@ const SELECTED: usize = 0;
|
||||
|
||||
/// 整页。
|
||||
///
|
||||
/// `appearance` 是 M93 接进来的外观运行期状态(`None` = harness / 演示路径,设置页仍画占位行)。
|
||||
pub fn view<'a>(
|
||||
ui: &'a Ui,
|
||||
metrics: FrameMetrics,
|
||||
appearance: Option<&'a State>,
|
||||
) -> Element<'a, Message> {
|
||||
/// `states` 是「画一帧时那份只读状态」:[`States::appearance`] 是 M93 接进来的外观运行期状态
|
||||
/// (`None` = harness / 演示路径,设置页仍画占位行),[`States::groups`] 是各组自己的状态。
|
||||
pub fn view<'a>(ui: &'a Ui, metrics: FrameMetrics, states: States<'a>) -> Element<'a, Message> {
|
||||
// 选中分组:有真实外观状态时看状态(点左列可切),否则退回演示值 0。
|
||||
let selected = appearance.map_or(SELECTED, |state| state.selected_group);
|
||||
let selected = states
|
||||
.appearance
|
||||
.map_or(SELECTED, |state| state.selected_group);
|
||||
|
||||
let body = match metrics.layout {
|
||||
PageLayout::SupportingPane => frame::panes(
|
||||
metrics,
|
||||
frame::group_nav(ui, selected),
|
||||
frame::left_aligned(metrics.column, form(ui, Some(selected), appearance)),
|
||||
frame::left_aligned(metrics.column, form(ui, Some(selected), states)),
|
||||
),
|
||||
_ => frame::single_column(metrics, form(ui, None, appearance)),
|
||||
_ => frame::single_column(metrics, form(ui, None, states)),
|
||||
};
|
||||
|
||||
frame::page(ui, metrics, frame::header(ui), body)
|
||||
@@ -130,13 +186,10 @@ pub fn view<'a>(
|
||||
/// 表单列(正文列宽 ≤424)。`only = Some(i)` 只渲染一组(supporting-pane),
|
||||
/// `None` 渲染全部(窄窗:分组导航退化成按组堆叠)。
|
||||
///
|
||||
/// **分发点**:一组的正文由它自己文件里的 `GROUP.body` 决定——`Some` 画真控件(现在只有「外观」),
|
||||
/// `None` 画演示行([`frame::demo_card`])。这里不出现任何一个分组名,也不比较下标。
|
||||
fn form<'a>(
|
||||
ui: &'a Ui,
|
||||
only: Option<usize>,
|
||||
appearance: Option<&'a State>,
|
||||
) -> Element<'a, Message> {
|
||||
/// **分发点**:一组的正文由它自己文件里的 `GROUP.body` 决定——`Some(f)` 让 `f` 自己去
|
||||
/// [`States`] 里取它要的数据(`None` 返回值 ⇒ 退回演示行),`None` 直接画演示行
|
||||
/// ([`frame::demo_card`])。这里不出现任何一个分组名,也不比较下标,更不认识任何一组的字段。
|
||||
fn form<'a>(ui: &'a Ui, only: Option<usize>, states: States<'a>) -> Element<'a, Message> {
|
||||
let mut stack = column![].spacing(ui.space(Space::X6)).width(Length::Fill);
|
||||
|
||||
for (index, group) in GROUPS.iter().enumerate() {
|
||||
@@ -145,11 +198,11 @@ fn form<'a>(
|
||||
}
|
||||
|
||||
// 「外观」分组:有真实外观状态时画真控件(M93),否则退回演示行(harness)。
|
||||
// 这两条的差别现在由 `GROUP.body` 表达,而不是「下标 == APPEARANCE_GROUP」的硬比较。
|
||||
let card = match (group.body, appearance) {
|
||||
(Some(body), Some(state)) => body(ui, state),
|
||||
_ => frame::demo_card(ui, group.rows),
|
||||
};
|
||||
// 这两条的差别由 `GROUP.body` 的返回值表达,而不是「下标 == APPEARANCE_GROUP」的硬比较。
|
||||
let card = group
|
||||
.body
|
||||
.and_then(|body| body(ui, states))
|
||||
.unwrap_or_else(|| frame::demo_card(ui, group.rows));
|
||||
|
||||
stack = stack.push(
|
||||
column![
|
||||
|
||||
Reference in new issue
Block a user