fix(app-iced): M90 r3 —— 空闲 1Hz 心跳恢复配置热切 + 导航轨口径订正 + 文档收口

复审 P2-2(空闲态掐时钟 ⇒ config_poll_due 再也不跑):
- 空闲态改用 1 Hz 自续心跳(Task::perform(sleep(1s)) → Message::Heartbeat),忙碌态仍用
  window::frames() + 33ms 闸门;两条时钟不并行(心跳在 needs_frames() 为真时只续排不推进)。
- ticks 改成「33 ms 一拍」记账(1 s 心跳记 30 拍),config 轮询判据从「整除」改成「跨越」——
  实测整除版在 ticks=137 之后每拍 +30 永远不命中,配置轮询静默死掉。
- 顺手修一个真 bug:Message::SystemTheme 原来无条件 set_flavor,会把配置文件里的显式主题盖掉
  (优先级现在是 --theme > 配置文件 > 系统),否则「改 launcher.json 主题 1 秒内生效」验收根本看不到。
- 实测:14 s 交互跑,空闲段 [tick] clock=idle-1hz 每秒一行、frames 冻结在 2947、refreshes 冻结在 6;
  t≈7s / t≈11s 两次改 launcher.json,各自 ≤1 s 内打出 [settings] 配置文件变了 + [theme] 翻转。

复审 P2-1(导航轨口径):
- pages/mod.rs 交付表改成事实(产品路径的 rail 由 shell/nav.rs 画,组件版只给 harness/单测,
  像素等价是复审判的),补偏离表第 10 条(两套 rail 各测各的,漂移没人拦);
- pages/tests.rs 的 the_real_rail_agrees_with_the_layout_system 注释同步订正。

P3 文档五条:shell 的「8px 拖动热区」改成事实(拖动面=整条 48px 标题栏到第一个 caption;
缩放热区 4px)、state/update 的「每拍重建视图模型」改成 view_dirty 门控、message 的 Frame 注释
改成「只在忙碌时订阅」、pages/model 的 From<&AppState> 改成自由函数 model(..)、pages/tests 的守门
目录补 src/app。

r3 浅/深产品图与 r2 逐字节相同(视觉零变化)。
This commit is contained in:
AskaEth committed 2026-11-02 15:56:59 +08:00
1 parent 022778a5a7
commit 6b66adff44
9 files changed
+254 -42

No files matched your search

+9 -1
View File
@@ -5,7 +5,14 @@
//! | 阶段 | 交付 | 判据 |
//! |---|---|---|
//! | M84 | **布局系统**(档位/版面/列宽)+ 四页占位视觉 | 下面的版面阈值表 |
//! | **M90(本文件当前状态)** | ①视图模型 [`model`] ②导航轨换成真组件并接上 `presenter::router::Route` ③**实例页整页接真组件 + 真数据** | 「接真组件后版面约束一条没破」(见 `pages/tests.rs` 的数值断言) |
//! | **M90(本文件当前状态)** | ①视图模型 [`model`] ②导航项接上 `presenter::router::Route` ③**实例页整页接真组件 + 真数据** | 「接真组件后版面约束一条没破」(见 `pages/tests.rs` 的数值断言) |
//!
//! **导航轨归属(M90 r3 订正,复审 P2-1)**:产品路径(`app/view.rs`)走
//! `shell::view_with_content`,导航轨由 **`shell/nav.rs` 的手搓实现**画——它与 M78 组件几何同值
//! (宽 88、药丸 56×32),复审逐像素量过「r1 产品图(真组件 rail)vs r2 产品图(shell rail)
//! 在 y≥48 区域差异为 0」;组件版的 [`rail`] 现在只服务 `pages::app`(`pages_demo` 出口图、
//! harness)与单测。为什么不是「把 rail 挪进内容槽位」:那需要把 `shell` 的消息类型泛型化/映射,
//! 成本远大于收益(见偏离表 #10)。
//!
//! 实例页之外的三页**仍是 M84 的占位视觉**(下载/日志/设置),但它们已经:
//! 走同一个 `Message` → `update` → `Route` 通道,导航轨点得动、路由栈按 `presenter::router` 的语义走。
@@ -58,6 +65,7 @@
//! | 6 | 实例页「未选中详情」空态**没有出图** | 演示数据固定选中第 1 条;空态代码路径在(`detail_empty`) | D30 状态矩阵补齐时补一张,不必重跑全套 |
//! | 7 | ~~标题栏仍是 48px 留白带~~ → **已于 M90 r2 修**:产品页现在走 `shell::view_with_content`,标题栏 / 拖动热区 / 两条缩放热区 / 三个 caption 键全在 | r1 的实质问题是**交互面缺失**(无边框窗口 + 产品页没有任何窗口装饰 ⇒ 拖不动、缩不了、点不到关闭),不是「少画了一条栏」 | ✅ 已收口(见下) |
//! | 8 | 实例页的「New instance / Import」仍是**没有后端的按钮**(M90) | presenter 没有「打开新建向导」的指令(`wizard` 只有状态机,`AppCommand` 里无对应项),已报 finding | 向导 mission 补 `AppCommand::OpenWizard` 之后 |
//! | 10 | **两套 rail 各测各的**(M90 r3):产品走 `shell/nav.rs`,组件版只给 harness/单测 | r2 把产品页接进真外壳后,rail 自然落到 `shell/nav.rs`(像素等价,见上)。但两边的守门是分开的:`shell/nav.rs` 自带 indicator 56×32 的单测,`pages/tests.rs` 守的是组件 rail 的 88 ⇒ **将来哪边先漂没人拦**。统一要泛型化 shell 的消息类型,成本大于收益 | 谁做「rail 单一实现」的 mission(已在报告登记);在那之前任何改 rail 几何的改动都要两处一起改并各自跑守门 |
//! | 9 | 实例页的正文段落只在**演示数据**里出现(M90) | 真快照里没有「正文描述」这种字段;M84 那段 361 字符的 `NOTE` 是给行宽测量当样本的,不能凭空造到真页面里 | i18n / 实例详情 mission 补真实描述字段 |
//!
//! ### 偏离 #7 的收口记录(M90 r2 / P1-1)
+1 -1
View File
@@ -6,7 +6,7 @@
//!
//! | 数据源 | 谁转 | 用途 |
//! |---|---|---|
//! | presenter 的 `AppState` 快照 | `app/view.rs`(`From<&AppState>`) | 产品运行时的真页面 |
//! | presenter 的 `AppState` 快照 | `app/view.rs` 的自由函数 `model(&AppState, width, forced)` | 产品运行时的真页面 |
//! | `pages/instances/data.rs` 的演示条目 | [`crate::pages::demo`] | M84 骨架图的可复现基线、`pages_demo` example |
//!
//! 如果页面直接读 `InstanceSummary`,演示数据就得先伪造成 `InstanceSummary`(含 core 语义字段),
+9 -4
View File
@@ -84,7 +84,8 @@ fn the_width_comparison_really_changes_the_layout() {
///
/// **抓得住**:`frame_metrics` 的 `column`(页面唯一的列宽来源)不会越界;
/// **抓不住**:页面自己绕过 metrics 写死宽度(那条由 `tests/no_hardcoded_tokens.rs` 的
/// 「`.width(` 后面不许跟数字字面量」守门——注意它扫的是 `src/shell` + `src/pages`)。
/// 「`.width(` 后面不许跟数字字面量」守门——它扫的是 `src/shell` + `src/pages` + `src/app`
/// (M90 r2/P3-3 把 `app` 加进去了)。
#[test]
fn every_page_any_width_stays_under_the_measure_cap() {
let cap = measure_cap_px();
@@ -119,10 +120,14 @@ fn the_three_frozen_numbers_are_unchanged() {
assert_eq!(WidthClass::of(840.0), WidthClass::Expanded);
}
/// 导航轨用的**真组件**(M78 的 `NavigationRail`)声明的宽度必须与布局系统同值。
/// **组件版**导航轨(M78 的 `NavigationRail`)声明的宽度必须与布局系统同值。
///
/// **抓得住**:`NavPaneGeometry::rail().width` == `layout::rail_width()` == 88(组件与布局系统没分叉);
/// **抓不住**:容器圆角/药丸摆放的像素级正确性(组件自己的 probe 管)。
/// 注意这不是产品路径的实现(M90 r3 订正):产品页的 rail 由 `shell/nav.rs` 画,组件版只服务
/// `pages::app`(harness 出口图)与单测。这条守的是「组件与布局系统没分叉」,**不是**「产品 rail
/// 没问题」——两套实现的漂移风险见 `pages/mod.rs` 偏离表 #10。
///
/// **抓得住**:`NavPaneGeometry::rail().width` == `layout::rail_width()` == 88;
/// **抓不住**:容器圆角/药丸摆放的像素级正确性(组件自己的 probe 管),也抓不住 `shell/nav.rs`。
#[test]
fn the_real_rail_agrees_with_the_layout_system() {
let ui = ui();