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
@@ -119,6 +119,12 @@ pub struct App {
|
||||
pub(super) appearance_prefs: unlucky_presenter::prefs::AppearancePrefs,
|
||||
/// 窗口句柄(`Message::WindowHandle` 之后才有;非 Windows 平台恒 `None`)。
|
||||
pub(super) hwnd: Option<isize>,
|
||||
/// 设置页各组自己的运行期状态(M101 r2 的**状态漏斗**)。
|
||||
///
|
||||
/// 所有者在 `pages/settings/state.rs` 的 `Groups`:加/改某一组的字段只改那一组自己的文件,
|
||||
/// 本字段与 `app/view.rs` 都不用动(`settings::States` 只看 `&Groups`,不看任何一组的字段)。
|
||||
/// 放在字段列表**末尾**:后续 mission 若也要往这里加字段,冲突面最小。
|
||||
pub(super) settings: pages::settings::Groups,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -202,6 +208,7 @@ impl App {
|
||||
appearance,
|
||||
appearance_prefs,
|
||||
hwnd: None,
|
||||
settings: pages::settings::Groups::default(),
|
||||
};
|
||||
// 配色来源档可能已经指向一张背景图:那时 `Ui` 必须由解析出的方案构造,而不是 baseline。
|
||||
app.rebuild_ui();
|
||||
|
||||
@@ -44,6 +44,7 @@ use super::CAPTURE_AT_FRAME;
|
||||
|
||||
mod download;
|
||||
mod page_messages;
|
||||
mod settings;
|
||||
|
||||
impl App {
|
||||
/// 一条消息进来,改状态并返回后续任务。
|
||||
|
||||
@@ -38,6 +38,9 @@ impl App {
|
||||
pages::Message::Launch(id) => self.dispatch(AppCommand::LaunchInstance(id)),
|
||||
pages::Message::Stop(id) => self.dispatch(AppCommand::StopInstance(id)),
|
||||
pages::Message::Appearance(change) => self.on_appearance(change),
|
||||
// M101:设置页的四条填页线共用一个投递口;组内变体在 `update/settings.rs` 里穷尽分发
|
||||
// (那一层没有 `_ =>`,见本文件末尾与 `update/settings/tests.rs` 的两条守门)
|
||||
pages::Message::Settings(group) => return self.on_settings(group),
|
||||
// M95:摘要落剪贴板(文本由页面造,这里只搬)
|
||||
pages::Message::CopySummary(text) => return iced::clipboard::write(text),
|
||||
// 下面四支已由 `on_download` 提前消化;显式列出而不是 `_ => {}`,见模块文档
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
//! 设置页分组消息的**二级分发**(M101 `SETTINGSMSG`):
|
||||
//! `pages::Message::Settings(GroupMessage)` → 四条填页线各自的 handler。
|
||||
//!
|
||||
//! ## 为什么分两级
|
||||
//!
|
||||
//! `app/update/page_messages.rs` 对 [`pages::Message`] 做穷尽匹配(没有 `_ =>`,有
|
||||
//! `the_page_match_has_no_wildcard_fallback` 守门)。设置页的四条填页线若各自往 `pages::Message`
|
||||
//! 里加变体,就都得改那个文件——四路并行必然冲突。M101 把它们收进一个投递口
|
||||
//! `pages::Message::Settings(GroupMessage)`,本文件再把 `GroupMessage` 分给四条线:
|
||||
//!
|
||||
//! ```text
|
||||
//! GroupMessage::Java(m) → settings/java.rs
|
||||
//! GroupMessage::Download(m) → settings/download.rs
|
||||
//! GroupMessage::LanguageAppearance(m) → settings/language_appearance.rs
|
||||
//! GroupMessage::AiAbout(m) → settings/ai_about.rs
|
||||
//! ```
|
||||
//!
|
||||
//! ## 穷尽性:抓得住什么 / 抓不住什么
|
||||
//!
|
||||
//! - **抓得住**:某条线给 `GroupMessage` 加了载荷变体(= 在自己文件里加了一支消息)却忘了在本
|
||||
//! match 里接线 → **E0004**;四个 handler 里对空枚举的 `match message {}` 一旦有人写成
|
||||
//! `_ => {}`,`the_group_dispatch_has_no_wildcard_fallback` 会红(兜底会把「漏接线」从编译
|
||||
//! 错误降级成「点了没反应」,`page_messages.rs` 的同类守卫是这么写的,这里照抄)。
|
||||
//! - **抓不住**:handler 忽略自己的消息(`let _ = message; Task::none()`)、两个 handler 身体写反
|
||||
//! 或写成一样——类型对得上就编得过,只有人读能看出来。也因此「接错了组」的一半是编译期兜住的
|
||||
//! (载荷类型不同 ⇒ 编不过),另一半(接了个空壳)不是。
|
||||
//!
|
||||
//! ## 加一支设置页消息要改哪
|
||||
//!
|
||||
//! 自己那条线的两个文件:消息口(`pages/settings/<线>.rs`)+ 落点(本目录 `<线>.rs`)。
|
||||
//! 本文件的 match 只在 `GroupMessage` **加变体**时才动,而那意味着新增一条线,不是加一支消息。
|
||||
|
||||
use super::*;
|
||||
|
||||
impl App {
|
||||
/// 设置页的分组消息:分给四条线各自的文件,本文件不认识任何一支具体消息。
|
||||
pub(super) fn on_settings(&mut self, message: pages::settings::GroupMessage) -> Task<Message> {
|
||||
match message {
|
||||
pages::settings::GroupMessage::Java(message) => self.on_settings_java(message),
|
||||
pages::settings::GroupMessage::Download(message) => self.on_settings_download(message),
|
||||
pages::settings::GroupMessage::LanguageAppearance(message) => {
|
||||
self.on_settings_language_appearance(message)
|
||||
}
|
||||
pages::settings::GroupMessage::AiAbout(message) => self.on_settings_ai_about(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod ai_about;
|
||||
mod download;
|
||||
mod java;
|
||||
mod language_appearance;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,17 @@
|
||||
//! AI / About 分组消息的落点(M101 `SETTINGSMSG`):**只归 ai+about 线**。
|
||||
//!
|
||||
//! 消息口在 `pages/settings/ai_about.rs`(`pages::settings::ai_about::Message`)。
|
||||
//!
|
||||
//! 首版是空枚举上的 `match message {}`:这是**穷尽**的写法(不是兜底),加了变体不接线就报 E0004。
|
||||
|
||||
use super::super::*;
|
||||
|
||||
impl App {
|
||||
/// AI / About 分组的一条消息。首版没有可处理的东西(空枚举)。
|
||||
pub(super) fn on_settings_ai_about(
|
||||
&mut self,
|
||||
message: pages::settings::ai_about::Message,
|
||||
) -> Task<Message> {
|
||||
match message {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//! Downloads 分组消息的落点(M101 `SETTINGSMSG`):**只归 download 线**。
|
||||
//!
|
||||
//! 消息口在 `pages/settings/download.rs`(`pages::settings::download::Message`)。与下载页
|
||||
//! (`pages/download*` 与 `app/update/download.rs`)无关:那是「装哪个版本」,这一组是「怎么下」。
|
||||
//!
|
||||
//! 首版是空枚举上的 `match message {}`:这是**穷尽**的写法(不是兜底),加了变体不接线就报 E0004。
|
||||
|
||||
use super::super::*;
|
||||
|
||||
impl App {
|
||||
/// Downloads 分组的一条消息。首版没有可处理的东西(空枚举)。
|
||||
pub(super) fn on_settings_download(
|
||||
&mut self,
|
||||
message: pages::settings::download::Message,
|
||||
) -> Task<Message> {
|
||||
match message {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//! Java 分组消息的落点(M101 `SETTINGSMSG`):**只归 java 线**。
|
||||
//!
|
||||
//! 消息口在 `pages/settings/java.rs`(`pages::settings::java::Message`)。加一支消息 = 改那两个
|
||||
//! 文件,`pages/mod.rs` / `page_messages.rs` / `app/update/settings.rs` 都不用动。
|
||||
//!
|
||||
//! 消息进来的通道有两条,都不经过共享文件:
|
||||
//! 1. **页面点击**:`pages::Message::Settings(GroupMessage::Java(..))` → `on_page` → `on_settings`;
|
||||
//! 2. **异步结果**:本文件自己构造 `Message::Page(pages::Message::Settings(GroupMessage::Java(..)))`
|
||||
//! (`app::Message::Page` 是既有通道,不用加新变体)。
|
||||
//!
|
||||
//! 穷尽性:这里对 `java::Message` 做**穷尽**匹配(没有 `_ =>`、没有 `other =>`)。加了变体不接线
|
||||
//! 立刻 E0004;写成兜底会把编译错误降级成「点了没反应」,`app/update/settings/tests.rs` 的
|
||||
//! `the_group_dispatch_has_no_catch_all_arm` 会红。
|
||||
|
||||
use super::super::*;
|
||||
|
||||
impl App {
|
||||
/// Java 分组的一条消息。
|
||||
pub(super) fn on_settings_java(
|
||||
&mut self,
|
||||
message: pages::settings::java::Message,
|
||||
) -> Task<Message> {
|
||||
match message {
|
||||
// M101 r2 的**活的端到端证据**:只有测试构造这一支(java 线接第一个真控件时换成真变体)。
|
||||
// 它真的改一个 app 状态,所以「消息从外壳走到 handler」这件事有运行时证据,不只是编译期。
|
||||
#[cfg(test)]
|
||||
pages::settings::java::Message::Ping => {
|
||||
self.view_dirty = true;
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//! 语言 + 外观两组消息的落点(M101 `SETTINGSMSG`):**只归 language+appearance 线**。
|
||||
//!
|
||||
//! 消息口在 `pages/settings/language_appearance.rs`(`pages::settings::language_appearance::Message`)。
|
||||
//! 为什么两组共用一个消息口:它俩本就是同一条线的两个分组(General 的语言 / 主题 / 启动页,
|
||||
//! Appearance 的密度 / 圆角 / 动效 / 文字缩放),`GroupMessage` 只给这条线留一个变体。
|
||||
//!
|
||||
//! 首版是空枚举上的 `match message {}`:这是**穷尽**的写法(不是兜底),加了变体不接线就报 E0004。
|
||||
|
||||
use super::super::*;
|
||||
|
||||
impl App {
|
||||
/// 语言 / 外观线的一条消息。首版没有可处理的东西(空枚举)。
|
||||
pub(super) fn on_settings_language_appearance(
|
||||
&mut self,
|
||||
message: pages::settings::language_appearance::Message,
|
||||
) -> Task<Message> {
|
||||
match message {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
//! 设置页分组消息分发的**守门测试**(M101 `SETTINGSMSG`)。
|
||||
//!
|
||||
//! 判据与 `app/update/page_messages.rs` 里那条 `the_page_match_has_no_wildcard_fallback` 同源:
|
||||
//! 兜底会把「加了变体忘了接线」从**编译错误**降级成「点了没反应」。两级 dispatch 各守一条:
|
||||
//! 那一层守 `pages::Message`,本文件守 `GroupMessage` 与四个 handler。
|
||||
//!
|
||||
//! r1 复审的 P3-2 指出上一版有洞:只拦 `_ =>` 的话,`other => { … }` 这种 **binding 兜底**
|
||||
//! 照样取消穷尽性、照样静默。本版判据反过来写——**除已知臂外,任何含 `=>` 的行都算兜底**:
|
||||
//! `settings.rs` 里每条臂都必须以 `pages::settings::GroupMessage::` 开头且恰好 4 条;handler 里
|
||||
//! 每条臂都必须以该组自己的变体路径开头;三个还是空枚举的 handler 一行 `=>` 都不许有。
|
||||
//!
|
||||
//! 站位:本文件是 `app::update::settings::tests`,`include_str!` 的路径相对本文件所在目录。
|
||||
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_presenter::mock::MockBackend;
|
||||
use unlucky_presenter::settings_bridge::SettingsBridge;
|
||||
|
||||
use crate::app::message::Message;
|
||||
use crate::app::state::App;
|
||||
use crate::cli::Options;
|
||||
use crate::pages;
|
||||
use crate::theme::Families;
|
||||
|
||||
/// 不碰磁盘的 App(mock 后端 + 内存设置桥),与 `app/tests.rs` 的 `headless` 同一副骨架。
|
||||
fn headless() -> App {
|
||||
const FAMILIES: Families = Families {
|
||||
text: "Segoe UI",
|
||||
display: "Segoe UI",
|
||||
mono: "Consolas",
|
||||
};
|
||||
|
||||
App::with_presenter(
|
||||
Options::default(),
|
||||
FAMILIES,
|
||||
SchemeFlavor::Light,
|
||||
Box::new(MockBackend::new()),
|
||||
SettingsBridge::memory_only(),
|
||||
)
|
||||
}
|
||||
|
||||
/// 收集「不该存在的臂」:非注释行里出现 `=>`,但不以该文件允许的前缀开头。
|
||||
fn offending_arms(path: &str, source: &str, allowed: Option<&str>) -> Vec<String> {
|
||||
source
|
||||
.lines()
|
||||
.enumerate()
|
||||
.filter_map(|(index, line)| {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("//") || !trimmed.contains("=>") {
|
||||
return None;
|
||||
}
|
||||
match allowed {
|
||||
Some(prefix) if trimmed.starts_with(prefix) => None,
|
||||
_ => Some(format!("{path}:{}: {trimmed}", index + 1)),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `on_settings` 与四个 handler 里都**不许**出现任何形式的兜底臂(`_ =>` 或 `other =>`)。
|
||||
///
|
||||
/// 抓得住:① 给 `on_settings` 加 `_ =>` 或 `other =>`(`GroupMessage` 不再穷尽);
|
||||
/// ② 四个 handler 把 `match message {}` 改成 `_ => Task::none()` / `other => { … }`(同样取消穷尽性);
|
||||
/// ③ 给 settings.rs 多接一条不认识的臂(前缀不对)。
|
||||
/// 抓不住:handler 忽略消息(`let _ = message; Task::none()`)、两个 handler 身体写反或写成一样
|
||||
/// —— 类型对得上就编得过;也抓不住「新变体接了个空壳」。那两条只有人读能看得出来。
|
||||
#[test]
|
||||
fn the_group_dispatch_has_no_catch_all_arm() {
|
||||
let sources: [(&str, &str, Option<&str>); 5] = [
|
||||
(
|
||||
"app/update/settings.rs",
|
||||
include_str!("../settings.rs"),
|
||||
Some("pages::settings::GroupMessage::"),
|
||||
),
|
||||
(
|
||||
"app/update/settings/java.rs",
|
||||
include_str!("java.rs"),
|
||||
Some("pages::settings::java::Message::"),
|
||||
),
|
||||
(
|
||||
"app/update/settings/download.rs",
|
||||
include_str!("download.rs"),
|
||||
None,
|
||||
),
|
||||
(
|
||||
"app/update/settings/language_appearance.rs",
|
||||
include_str!("language_appearance.rs"),
|
||||
None,
|
||||
),
|
||||
(
|
||||
"app/update/settings/ai_about.rs",
|
||||
include_str!("ai_about.rs"),
|
||||
None,
|
||||
),
|
||||
];
|
||||
|
||||
let mut offending = Vec::new();
|
||||
for (path, source, allowed) in sources {
|
||||
offending.extend(offending_arms(path, source, allowed));
|
||||
}
|
||||
|
||||
assert!(
|
||||
offending.is_empty(),
|
||||
"设置页分组消息的 dispatch 里出现了兜底臂({offending:?}):\
|
||||
任何 catch-all / binding 臂都会取消 `GroupMessage` 的穷尽性,\
|
||||
新变体漏接线时编译器不再报 E0004"
|
||||
);
|
||||
}
|
||||
|
||||
/// `on_settings` 必须**恰好四条**臂,一条一条显式写全(不是「至少」)。
|
||||
///
|
||||
/// 抓得住:有人删掉一条臂、或者把两条合到一起(`A(..) | B(..) =>` 这种也过不了上面那条前缀判据,
|
||||
/// 这里是第二条独立判据)。
|
||||
/// 抓不住:臂的顺序(顺序不影响语义)。
|
||||
#[test]
|
||||
fn the_group_dispatch_has_exactly_four_arms() {
|
||||
let source = include_str!("../settings.rs");
|
||||
let arms = source
|
||||
.lines()
|
||||
.filter(|line| line.trim().starts_with("pages::settings::GroupMessage::"))
|
||||
.count();
|
||||
|
||||
assert_eq!(
|
||||
arms, 4,
|
||||
"`on_settings` 应该恰好四条臂(java / download / language_appearance / ai_about),实际 {arms} 条"
|
||||
);
|
||||
}
|
||||
|
||||
/// **活的端到端证据**:一条组消息真的从外壳那条既有通道走到该组的 handler(r1 复审要求)。
|
||||
///
|
||||
/// 路径:`app::Message::Page` → `on_page` → `pages::Message::Settings` → `on_settings` →
|
||||
/// `on_settings_java`,最后改到 `App` 的状态。r1 之前这条链只有编译期证据(`GroupMessage` 是
|
||||
/// uninhabited,运行时是死代码);现在至少有一支真的跑得起来。
|
||||
///
|
||||
/// 抓得住:漏斗某一环接断了(`on_page` 没转交、`on_settings` 转错组、handler 忽略消息)——那时
|
||||
/// `view_dirty` 不会翻。抓不住:真实用户点击(本条没有 UI 控件,构造点只有测试)。
|
||||
#[test]
|
||||
fn a_group_message_reaches_its_handler_through_the_funnel() {
|
||||
let mut app = headless();
|
||||
app.view_dirty = false;
|
||||
|
||||
let _ = app.update(Message::Page(pages::Message::Settings(
|
||||
pages::settings::GroupMessage::Java(pages::settings::java::Message::Ping),
|
||||
)));
|
||||
|
||||
assert!(
|
||||
app.view_dirty,
|
||||
"java 线的消息没有走到 `on_settings_java`:消息漏斗在某一环断了"
|
||||
);
|
||||
}
|
||||
@@ -49,7 +49,8 @@ impl App {
|
||||
// `Close` 五个消息只在外壳里构造 ⇒ 产品页「拖不动、缩不了、点不到关闭」。
|
||||
//
|
||||
// M93:三样东西从外观状态进来——①背景层(垫在最底下)②面板 α(外壳层的底色)③设置页要读
|
||||
// 的那份状态本身(`page_with_appearance`)。
|
||||
// 的那份状态本身。M101 r2 起 ③ 走 `page_with_settings`:外观状态 + 各组自己的状态
|
||||
// (`App.settings`)一起递进去,四条填页线加字段不用动这一行。
|
||||
match pages::Screen::of(self.state.route()) {
|
||||
Some(_) => {
|
||||
let background =
|
||||
@@ -61,8 +62,17 @@ impl App {
|
||||
nav_item_of(self.state.route()),
|
||||
APP_TITLE,
|
||||
background,
|
||||
pages::page_with_appearance(&self.ui, &self.view, &self.appearance)
|
||||
.map(Message::Page),
|
||||
// M101 r2:设置页的只读状态(外观运行期状态 + 各组自己的状态)一次递进去。
|
||||
// `settings` 的所有者是 `App`;这里只借一帧,加/改某一组的字段不用动这行。
|
||||
pages::page_with_settings(
|
||||
&self.ui,
|
||||
&self.view,
|
||||
pages::settings::States {
|
||||
appearance: Some(&self.appearance),
|
||||
groups: &self.settings,
|
||||
},
|
||||
)
|
||||
.map(Message::Page),
|
||||
self.options.force_state,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user