diff --git a/crates/app-iced/src/components/motion/mod.rs b/crates/app-iced/src/components/motion/mod.rs index 27c540c..9eb182d 100644 --- a/crates/app-iced/src/components/motion/mod.rs +++ b/crates/app-iced/src/components/motion/mod.rs @@ -84,21 +84,22 @@ thread_local! { /// 上次收到帧的时刻(`advance` 用它把「帧到达」换算成流逝毫秒)。 #[allow(clippy::missing_const_for_thread_local)] static LAST_FRAME: Cell> = const { Cell::new(None) }; - /// **测试专用**:置真后 [`configure`] 不再读系统偏好(见 [`configure_for_test`])。 + /// **测试专用**:`Some(档)` = 钉住这个档,[`configure`] 既不再读系统偏好、也不吃传入值。 #[allow(clippy::missing_const_for_thread_local)] - static BYPASS_PROBE: Cell = const { Cell::new(false) }; + static FORCED_SCALE: Cell> = const { Cell::new(None) }; } /// 定档:`on_boot` 调一次(截图档给 `NONE`,CPU 档给 `HALF`,见 `app/update/boot_env.rs`)。 /// /// **系统「减少动效」在这里合成**(见模块文档的表):`Reduced` ⇒ `NONE`;`Unknown` ⇒ 不动。 pub fn configure(scale: MotionScale, pin: Option) { - // 用例走 `configure_for_test` 置位旁路:**产品启动路径**(`on_boot` → 这里)也会重新定档, - // 只挡调用点是不够的。 - let scale = if BYPASS_PROBE.with(Cell::get) { - scale - } else { - effective_scale(scale, ReduceMotion::probe(), pin.is_some()) + // 三条定档路径**都汇到这里**:①测试/`on_boot` 直接调;②`App::on_boot` → `configure_motion` + // → 这里(`app/update.rs:276`);③`boot_env::motion_scale(..)` 算出的 env / 后端 tier 也是经② + // 喂进来的。所以「钉住某个档」必须在这个函数里生效——堵调用点或堵 `boot_env` 都会被后一次 + // `on_boot` 覆盖(r2 复审实测:`idle_app()` 先钉 STANDARD,Boot 之后变 HALF/NONE)。 + let scale = match FORCED_SCALE.with(Cell::get) { + Some(forced) => forced, + None => effective_scale(scale, ReduceMotion::probe(), pin.is_some()), }; PUMP.with(|pump| pump.borrow_mut().configure(scale, pin)); } @@ -111,7 +112,7 @@ pub fn configure(scale: MotionScale, pin: Option) { /// 这条判据悄悄删掉。 #[cfg(test)] pub fn configure_for_test(scale: MotionScale, pin: Option) { - BYPASS_PROBE.with(|flag| flag.set(true)); + FORCED_SCALE.with(|forced| forced.set(Some(scale))); configure(scale, pin); } @@ -131,18 +132,6 @@ pub fn scale() -> MotionScale { PUMP.with(|pump| pump.borrow().scale()) } -/// **测试专用**:动效全局态的串行门(复审 P2-5 的第二半)。 -/// -/// `Pump` 与「旁路探针」都是 thread-local,正常并行用例本就互不干扰;但用例会读**产品启动路径** -/// 写下的档位,而 libtest 允许 `--test-threads=1`(届时所有用例同线程)。凡是会改写动效全局态的 -/// 用例都先取这把锁,把时序固定下来——**跨模块共用的只有这一把**(`subscription.rs` 与 -/// `motion_sync.rs` 都从这里取)。 -#[cfg(test)] -pub fn serialize() -> std::sync::MutexGuard<'static, ()> { - static GATE: std::sync::Mutex<()> = std::sync::Mutex::new(()); - GATE.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) -} - /// 已推进的帧数。 pub fn samples() -> u64 { PUMP.with(|pump| pump.borrow().samples()) @@ -258,8 +247,8 @@ pub fn shifted<'a, M: 'a>( pub fn reset() { PUMP.with(|pump| *pump.borrow_mut() = pump::Pump::new()); LAST_FRAME.with(|last| last.set(None)); - // 旁路也要清:「故意走真探针」的那条用例靠 `reset()` 回到未旁路状态。 - BYPASS_PROBE.with(|flag| flag.set(false)); + // 钉住也要清:「故意走真探针」的那条用例靠 `reset()` 回到未钉住状态。 + FORCED_SCALE.with(|forced| forced.set(None)); } /// 测试专用:按给定毫秒推进(`advance` 读真实时钟,单元测试不能等它)。 diff --git a/crates/app-iced/src/components/motion/pump/modal.rs b/crates/app-iced/src/components/motion/pump/modal.rs index 758e696..69bcd39 100644 --- a/crates/app-iced/src/components/motion/pump/modal.rs +++ b/crates/app-iced/src/components/motion/pump/modal.rs @@ -24,8 +24,9 @@ pub(crate) enum Modal { Opening { start_ms: f64 }, /// 完全展开(稳态,不占帧)。 Open, - /// 退场中。 - Closing { start_ms: f64 }, + /// 退场中。`from` = 起手那一刻的进度:进场途中被关掉时它是**当前进度**,不是 1.0 + /// (否则会先跳到完全展开再往回缩 = 反向硬跳)。 + Closing { start_ms: f64, from: f32 }, } impl Pump { @@ -72,8 +73,16 @@ impl Pump { self.close_pending = false; return true; } + // 已经在退场 ⇒ 保持原样(重复的关闭请求不该重算起点)。 + if matches!(self.modal, Modal::Closing { .. }) { + return false; + } + // **起点是当前进度**:进场走到 20% 就关 ⇒ 从 0.20 往回缩;已展开 ⇒ 从 1.0 缩。 + // 不这么做就是「先跳到 1.0 再退」的反向硬跳(复审 r2 的 P2-2)。 + let from = self.modal_frame().progress; self.modal = Modal::Closing { start_ms: self.start_now(), + from, }; false } @@ -107,7 +116,7 @@ impl Pump { progress: spec.enter.easing.sample(t), } } - Modal::Closing { start_ms } => { + Modal::Closing { start_ms, from } => { let spec = modal_spec(); let t = leg_fraction( self.elapsed_ms() - start_ms, @@ -115,8 +124,9 @@ impl Pump { ); ModalFrame { visible: true, - // 退场是**反向**走同一条线:1 → 0(与进场共用 `modal_scale` 的映射)。 - progress: 1.0 - spec.exit.easing.sample(t), + // 从起手进度**连续**缩到 0:`from * (1 - eased)`。`from = 1.0` 时与改造前 + // 逐帧等价;进场途中关则是"从半开接着缩"。 + progress: from * (1.0 - spec.exit.easing.sample(t)), } } }