feat(app-iced): 页面切换按导航深度定向 + 模态缩放淡入(M256 MOTION1)

- transition.rs:唯一一张动效表。同级换页走水平(±12 / 旧页 ∓8)、进深层走纵向
  (新页 +16 上浮 / 旧页 -8),进腿 medium2/medium1(300/250),退腿 short4→medium1(更短),
  进用 Standard、退用 StandardAccelerate。幅度全部取自 Space 令牌。
- pump.rs:Slot 三互斥槽 → Kind(Boot/Peer{forward}/Push/Pop)+ 两腿轨道;新增模态状态机
  (Opening/Open/Closing,展开后不占帧,退场走完交回一次 take_modal_closed)。
- components/motion/zoom.rs:绕中心缩放的自绘控件(renderer.with_transformation,
  tiny-skia 与 wgpu 都实现了,文字也真的跟着缩放);scale==1.0 时不套变换 ⇒ 出图基线不变。
- pages/mod.rs:page_with_settings_transition(ui, current, previous, …) 叠
  stack![opaque(旧页@退场位移), 新页@进场位移];旧函数只剩一句委托(blast radius 不变)。
- App.previous_view:换页时用 std::mem::replace **移动**旧模型(冻结快照),过渡结束当拍
  置 None(不随换页次数增长)。
- 模态:逻辑状态当拍关(state.dialogs 立刻空),画面多留 150ms;退场期间弹窗消息被闸门吞掉。
- platform/motion.rs:系统「减少动效」三态探针(Windows=SystemParametersInfoW,
  Linux/macOS=子进程+300ms 超时),OnceLock 缓存;Unknown **不降级**(探针失败不静默关动效)。
- MotionScale::HALF 语义核清并写进 design 文档:是**时长减半**(更快),不是半速。
This commit is contained in:
AskaEth committed 2026-11-12 10:50:02 +08:00
1 parent 5a182e80de
commit b1c09c4c91
21 files changed
+2168 -658

No files matched your search

+29 -2
View File
@@ -9,6 +9,7 @@ use unlucky_presenter::dto::{AppCommand, ModsCommand, SavesCommand};
use super::{mods_shot, saves_shot};
use unlucky_presenter::toast::ToastKind;
use crate::app::components::motion;
use crate::app::view;
use crate::pages;
@@ -81,7 +82,7 @@ impl App {
// 右栏占掉的宽度先扣掉:面板展开时页面按更窄的宽度排版(「收缩而不是溢出」,M104)
let width = self.agent.content_width(self.width);
// M106:语言形参跟着 `LangState` 走(`draft` 那一位是 M105 的安装草稿)。
self.view = super::super::view::model(
let next = super::super::view::model(
&self.state,
width,
self.height,
@@ -91,6 +92,15 @@ impl App {
self.lang(),
&self.content,
);
// M256:换页 / 换层(屏 或 详情 id 变了)时才把**上一帧的模型**移进 `previous_view`。
// 用 `replace`(移动)而不是克隆:旧页要的是**转换开始那一刻的快照**,之后 presenter 再怎么
// 刷新都不该动它(否则会看到"两个页面同时在动内容")。
// 同一屏的普通刷新(下载进度 / 主题 / 语言)不换——否则每来一条事件就多留一份模型。
if (self.view.screen, detail_key(&self.view)) != (next.screen, detail_key(&next)) {
self.previous_view = Some(std::mem::replace(&mut self.view, next));
} else {
self.view = next;
}
// M157:启动参数表单的重播种(换实例必重播种、同实例的脏草稿不覆盖)。
self.sync_launch_form();
// 账户页(M114):与 `view` 同一次门控重建(实现见 `view/account_page.rs`,
@@ -98,7 +108,15 @@ impl App {
self.rebuild_account_page(width);
// M187:弹窗的视图模型也在这一处重建——`view()` 只借 `&self`,放不进局部变量
// (Element 借字符串,局部一死树就悬空)。全仓「快照 → 视图模型」只在这里发生。
self.dialog_model = self.dialog_modal();
//
// M256:**退场期间要留住旧框**。逻辑状态(`state.dialogs`)在用户点下去的那一拍就关掉了
// (所以 `dialog_modal()` 变 `None`),但画面还要缩着退出去 150ms——那一份模型必须留到
// 退场跑完(`App::sync_motion` 收到 `take_modal_closed()` 时置 `None`)。
// 判据用 `motion::modal_closing()` 而不是"有没有动效":关着动效时它恒为假 ⇒ 当场清。
let next_modal = self.dialog_modal();
if next_modal.is_some() || !motion::modal_closing() {
self.dialog_model = next_modal;
}
self.view_dirty = false;
self.refreshes += 1;
}
@@ -232,3 +250,12 @@ impl App {
self.mods_failed_seen = Some(key);
}
}
/// 详情的身份键(`None` = 不在详情页):与 `app/update/motion_sync.rs` 的 `detail` 同一判据。
fn detail_key(model: &pages::Model) -> Option<&str> {
model
.instances
.detail
.as_ref()
.map(|detail| detail.id.as_str())
}