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:
1 parent
5a182e80de
commit
b1c09c4c91
21 files changed
+2168
-658
No files matched your search
@@ -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())
|
||||
}
|
||||
Reference in new issue
Block a user