feat(app-iced): M90 —— app.rs 拆成 src/app/ + presenter 真接线 + 实例页端到端

拆分(行为不变,截图四件套逐字节相同):
- 400 行的 app.rs 拆成 src/app/{mod,state,message,update,subscription,view,boot,tests}.rs,
  每个文件 ≤ 300 行;components 的 #[path] 跟着搬到 src/app/mod.rs(逻辑路径仍是 crate::app::components),
  可见性放到 pub(crate) 让页面层能用真组件。
- 拆分前后各出 shell/gallery 浅深四张图,cmp 全部 IDENTICAL。

presenter 接线(crate 文档 §4):
- App 持有 Box<dyn Backend> + SettingsBridge + AppState;启动 build_backend + SettingsBridge::open + AppState::new。
- 截图通道强制 mock(D30 可复现);交互模式走真 host,--data-dir 从 M90 起真的被消费。
- Boot 抽干首屏快照(第 3 帧落盘,等节拍会把空页面拍进图);节拍 = 帧事件 + 33ms 时间闸门
  (iced::time::every 要 tokio/smol,见 finding),tick → drain_events → AppState::apply。
- 用户动作不乐观回显:Launch/Stop 走 Backend::dispatch,状态等事件回来才变。
- 路由:pages::Message::Navigate(Route) 直接进 AppState::navigate,选中项 = 路由栈顶(不再有本地副本);
  首屏自动选中第一行(list-detail 桌面惯例),只做一次。

实例页打通(其余三页保留占位视觉 + Message/update + 路由):
- 列表行换成 components::navigation::ListItem(真五态/选中),页头与详情动作换成 components::buttons
  (Play 是唯一的实心 accent),分隔线换成 components::divider。
- 数据全部来自 presenter 快照:VERSION/LOADER/ISOLATION/VERSION ID/FILES/LAST PLAYED + 运行状态行。
- 导航轨换成真 NavigationRail(88),5 项各自压/替换路由。

版面约束未破(实测):列表列 336、正文列 424(元信息卡 206+12+206,右缘 896)、rail 88(药丸 16..71)、
断点 600/840 走 frame_metrics;新增 pages/tests.rs 的 9 条数值断言钉住它们。
This commit is contained in:
AskaEth committed 2026-11-02 15:56:57 +08:00
1 parent 270026338f
commit b484998fc9
17 files changed
+1990 -676

No files matched your search

@@ -0,0 +1,51 @@
//! 实例页的**演示数据 → 视图模型**转换(M84 的出图基线靠这份数据)。
//!
//! 只被 [`crate::pages::demo_model`] 用:产品路径的转换在 `app/view.rs`(presenter 快照 → 模型)。
//! 两份数据源喂同一个渲染函数,这是 [`crate::pages::model`] 存在的理由。
use super::super::model::{Detail, Instances, Meta, Row};
use super::data::{DEMO, NOTE, SELECTED};
/// 把 M84 的演示条目 [`DEMO`] 转成视图模型(真页面走 `app/view.rs` 的 `AppState` 转换)。
pub(crate) fn demo() -> Instances {
let rows = DEMO
.iter()
.map(|entry| Row {
id: entry.name.to_string(),
name: entry.name.to_string(),
supporting: format!("{} · {}", entry.version, entry.loader),
status: None,
})
.collect();
let entry = &DEMO[SELECTED];
let detail = Detail {
id: entry.name.to_string(),
name: entry.name.to_string(),
subtitle: format!("{} · {}", entry.version, entry.loader),
meta: vec![
meta("VERSION", entry.version),
meta("LOADER", entry.loader),
meta("MEMORY", entry.memory),
meta("JAVA", entry.java),
meta("MODS", entry.mods),
meta("LAST PLAYED", entry.last_played),
],
note: Some(NOTE.to_string()),
status: None,
can_launch: true,
can_stop: false,
};
Instances {
rows,
detail: Some(detail),
}
}
fn meta(label: &str, value: &str) -> Meta {
Meta {
label: label.to_string(),
value: value.to_string(),
}
}