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
@@ -7,10 +7,33 @@
|
||||
use iced::Task;
|
||||
use unlucky_presenter::toast::ToastKind;
|
||||
|
||||
use crate::app::components::motion;
|
||||
use crate::app::dialog::{export, Message as DialogMessage};
|
||||
use crate::app::state::App;
|
||||
|
||||
/// 关掉当前这个框(**唯一出口**,M256)。
|
||||
///
|
||||
/// 逻辑状态是**当场**关的(`close_dialog()` 立刻清 `state.dialogs`)——延迟关闭会让队列里那个
|
||||
/// 框在退场期间仍然"活着"(`current()` 还是它),所有按"有没有框"判断的代码都会看到一个已经
|
||||
/// 点过「确定」的框。所以退场只发生在**画面上**:`refresh_view` 在 `motion::modal_closing()`
|
||||
/// 期间保留旧框的视图模型,`App::sync_motion` 在退场走完那一拍把它置 `None`。
|
||||
///
|
||||
/// 关着动效(`MotionScale::NONE` / 截图通道)时 `begin_modal_close()` 返回 `true`,
|
||||
/// `modal_closing()` 恒假 ⇒ 当场清,退回改造前的行为。
|
||||
fn close(app: &mut App) {
|
||||
let _ = motion::begin_modal_close();
|
||||
app.close_dialog();
|
||||
// 立即重画:空闲态只有 1 Hz 心跳,只置 `view_dirty` 会让弹窗再留最多 1 秒
|
||||
// (r1 复审 P3-1)。退场的第一帧必须在这一拍画出来。
|
||||
app.refresh_view();
|
||||
}
|
||||
|
||||
pub(super) fn on_dialog(app: &mut App, message: DialogMessage) -> Task<crate::app::Message> {
|
||||
// M256:退场动画期间**吞掉**弹窗消息。框正在缩小,这时再点一次「删除世界」不该再生效
|
||||
// (主按钮的 `take_confirm` 已经在上一次点击里核销过目标,但用户看到的仍是"框还在")。
|
||||
if motion::modal_closing() {
|
||||
return Task::none();
|
||||
}
|
||||
match message {
|
||||
DialogMessage::Primary => {
|
||||
// M247:删除存档的二次确认——主按钮就是**真删**(这是本页唯一不可逆的动作)。
|
||||
@@ -20,7 +43,10 @@ pub(super) fn on_dialog(app: &mut App, message: DialogMessage) -> Task<crate::ap
|
||||
// 崩溃卡片的「导出诊断包」也拿不走它——否则就是一次静默删世界。
|
||||
let current = app.state.dialogs.current().map(|request| request.id);
|
||||
if let Some(dir) = current.and_then(|id| app.state.saves.take_confirm(id)) {
|
||||
app.close_dialog();
|
||||
// M256:走统一出口(逻辑当拍关,画面多留 150ms 退场)。
|
||||
// 目标已经在上一行核销 ⇒ 退场期间再点一次主按钮不会删第二个世界
|
||||
// (那一拍的消息还会被 `modal_closing` 那道闸门吞掉)。
|
||||
close(app);
|
||||
if let Some(instance) = app.selected_instance_id() {
|
||||
app.state
|
||||
.saves
|
||||
@@ -31,12 +57,11 @@ pub(super) fn on_dialog(app: &mut App, message: DialogMessage) -> Task<crate::ap
|
||||
unlucky_presenter::dto::SavesCommand::DeleteWorld { id: instance, dir },
|
||||
)));
|
||||
}
|
||||
app.refresh_view();
|
||||
return Task::none();
|
||||
}
|
||||
// 崩溃卡片的主按钮是「导出诊断包」;普通确认框的主按钮就是「关掉」。
|
||||
let Some(card) = app.dialog_card.clone() else {
|
||||
app.close_dialog();
|
||||
close(app);
|
||||
return Task::none();
|
||||
};
|
||||
let done = export::export(&card.report, app.lang());
|
||||
@@ -64,10 +89,9 @@ pub(super) fn on_dialog(app: &mut App, message: DialogMessage) -> Task<crate::ap
|
||||
if let Some(current) = app.state.dialogs.current().map(|request| request.id) {
|
||||
app.state.saves.close_confirm(current);
|
||||
}
|
||||
app.close_dialog();
|
||||
// 立即重画:空闲态只有 1 Hz 心跳,只置 `view_dirty` 会让弹窗再留最多 1 秒
|
||||
// (r1 复审 P3-1)。
|
||||
app.refresh_view();
|
||||
// M256:统一出口(有动效时先退场)。「立即重画」这条要求由 `close` 里的
|
||||
// `refresh_view` 满足:退场的第一帧必须在这一拍画出来。
|
||||
close(app);
|
||||
Task::none()
|
||||
}
|
||||
DialogMessage::Toggle => {
|
||||
@@ -129,8 +153,8 @@ pub(super) fn escape(app: &mut App) -> bool {
|
||||
// 现在还有 id 绑定兜一道(`take_confirm(current)`),这里是纵深防御的第二层。
|
||||
// 复审 r2 加固:清的时候**只清这个框自己的目标**(`close_confirm`),不碰别人的。
|
||||
app.state.saves.close_confirm(current);
|
||||
app.close_dialog();
|
||||
app.refresh_view();
|
||||
// M256:统一出口(Esc 也走退场动画,而不是硬跳)。
|
||||
close(app);
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user