diff --git a/crates/app-iced/src/app/update/motion_sync.rs b/crates/app-iced/src/app/update/motion_sync.rs index 37da015..c9c0faa 100644 --- a/crates/app-iced/src/app/update/motion_sync.rs +++ b/crates/app-iced/src/app/update/motion_sync.rs @@ -320,8 +320,9 @@ mod tests { /// **退场窗口内那个框已经关掉、且不可再交互**(tower 的硬要求)。四件事:①逻辑状态当拍就空; /// ②画面还在退场(`dialog_model` 仍 `Some`);③150ms 内再点一次**什么都不发生**(`refreshes` - /// 不涨 = `on_dialog` 的闸门);④**Esc 那条路径**也挡住(`escape` 的闸门)。**抓不住**:几何级 - /// 命中(单测没有 iced 布局树,`zoom.rs` 也不改命中矩形)——那一层由闸门保证。 + /// 不涨 = `on_dialog` 的闸门);④**Esc 那条路径**也挡住(`escape` 的闸门)。 + /// **抓得住**:删掉任一道闸门(`on_dialog` / `escape`)或把逻辑关闭改成延后,①②③④ 各自红。 + /// **抓不住**:几何级命中(单测没有 iced 布局树,`zoom.rs` 也不改命中矩形)——那一层由闸门保证。 #[test] fn the_exiting_dialog_is_already_closed_and_swallows_clicks() { use crate::app::dialog::Message as DialogMessage; diff --git a/crates/app-iced/src/components/motion/tests/mod.rs b/crates/app-iced/src/components/motion/tests/mod.rs index 2566131..77a960f 100644 --- a/crates/app-iced/src/components/motion/tests/mod.rs +++ b/crates/app-iced/src/components/motion/tests/mod.rs @@ -310,6 +310,10 @@ fn none_scale_is_strictly_zero_not_merely_small() { // ------------------------------------------------------------------ 触发判据 /// 重复绘制**永远不会**重新起跑(否则每帧都重启动画 = 永远停不下来)。 +/// +/// 抓得住:把「标识没变 ⇒ 不起跑」的判断写错(比如每个 view 都 `play_page`)——30 次重复绘制后 +/// `is_animating()` 仍为真、帧泵永远停不下来,本条红。 +/// 抓不住:真实屏幕上"重画是否真的只是重画"(那要看 iced 的订阅表,归 `app::subscription` 的用例)。 #[test] fn repeated_paints_never_restart_the_animation() { let mut pump = standard(); @@ -324,6 +328,9 @@ fn repeated_paints_never_restart_the_animation() { } /// 不归 pages 画的屏(账户 / AI 独立页)**不起跑**;回到某一屏照样跑。 +/// +/// 抓得住:把 `screen == None` 也当成一次换页起跑(会白跑几百毫秒的帧却没人采样)。 +/// 抓不住:账户页 / AI 页自己的渲染(它们不走 `pages::render`,不归本 pump 管)。 #[test] fn screens_rendered_elsewhere_never_start_a_track() { let mut pump = standard(); diff --git a/crates/app-iced/src/components/motion/zoom.rs b/crates/app-iced/src/components/motion/zoom.rs index c7c712f..80c94f8 100644 --- a/crates/app-iced/src/components/motion/zoom.rs +++ b/crates/app-iced/src/components/motion/zoom.rs @@ -42,16 +42,22 @@ where }) } -/// 绕**中心**缩放的变换:`T(-c) · S · T(c)`(`Mul` 的乘法序 = 先右后左,见 iced_core 的 -/// `impl Mul for Transformation`)。 +/// 绕**中心**缩放的变换:`T(c) · S · T(-c)`。 +/// +/// `Mul` 的乘法序是**先右后左**(`impl Mul for Transformation` 用 `self.0 * rhs.0`,点按 +/// `p' = M·p` 变换)⇒ 这个式子算出来是 `p → s(p − c) + c = s·p + (1−s)c`,**不动点正是 `c`**。 +/// +/// ⚠️ 上一次写反了:`T(-c) · S · T(c)` 会得到 `s·p − (1−s)c`,不动点在 `(−cx, −cy)` ——窗口外的 +/// 镜像点,于是"缩放"变成了"从左上斜滑进来"(复审 r3 用真 iced_core 实跑抓到:card 中心 +/// (700,450) 漂到 (644,414))。`tests::about_center_keeps_the_center_fixed` 把这条钉住。 fn about_center(bounds: Rectangle, scale: f32) -> Transformation { let (cx, cy) = ( bounds.x + bounds.width / 2.0, bounds.y + bounds.height / 2.0, ); - Transformation::translate(-cx, -cy) + Transformation::translate(cx, cy) * Transformation::scale(scale) - * Transformation::translate(cx, cy) + * Transformation::translate(-cx, -cy) } struct Scaled<'a, Message, Theme, Renderer> { @@ -184,3 +190,65 @@ where .overlay(tree, layout, renderer, viewport, translation) } } + +#[cfg(test)] +mod tests { + use super::*; + use iced::{Point, Size}; + + /// **绕中心缩放:中心不动、尺寸按倍率**(复审 r3 的 P1 就是这条从未被断言过)。 + /// + /// 抓得住:①把两个 `translate` 乘反(上一版的 bug:不动点落在 `(−cx, −cy)`,窗口外的镜像点 + /// ⇒ 卡片从左上斜滑进来)——中心断言立刻以几十像素的偏差红;②倍率写错 / 漏乘(尺寸断言红); + /// ③把 `about_center` 改成纯缩放(不含平移)——同样红。 + /// 抓不住:**像素**。这里断言的是几何矩阵;两个渲染后端都按同一个 `Transformation` 画 + /// (`filled`/`text` 的 `bounds * transformation`),所以"几何等价 ⇒ 像素等价"成立的前提是 + /// **后端真的把矩阵乘上了**——那一条由真机截图看,不由本用例保证。 + #[test] + fn about_center_keeps_the_center_fixed() { + // 默认窗口 1180×760 里居中的卡片(DialogGeometry::max_width = 560) + for (rect, scale) in [ + ( + Rectangle::new(Point::new(310.0, 220.0), Size::new(560.0, 320.0)), + 0.96_f32, + ), + ( + Rectangle::new(Point::new(310.0, 220.0), Size::new(560.0, 320.0)), + 0.5, + ), + ( + Rectangle::new(Point::new(0.0, 0.0), Size::new(384.0, 288.0)), + 1.04, + ), + ] { + let matrix = about_center(rect, scale); + let center = rect.center(); + let moved = center * matrix; + assert!( + (moved.x - center.x).abs() < 1.0e-3 && (moved.y - center.y).abs() < 1.0e-3, + "scale={scale}: 中心必须不动,实测 ({}, {}) 应为 ({}, {})", + moved.x, + moved.y, + center.x, + center.y + ); + + let size = rect.size() * matrix; + assert!( + (size.width - rect.width * scale).abs() < 1.0e-3 + && (size.height - rect.height * scale).abs() < 1.0e-3, + "scale={scale}: 尺寸必须是 {scale} 倍,实测 {size:?}" + ); + + // 缩小时左上角必须往中心方向收(不是往外飞) + if scale < 1.0 { + let corner = rect.position() * matrix; + assert!( + corner.x > rect.x && corner.x < center.x, + "scale={scale}: 左上角 x 应收向中心,实测 {}", + corner.x + ); + } + } + } +} diff --git a/crates/app-iced/src/platform/motion.rs b/crates/app-iced/src/platform/motion.rs index 236f2da..b5ea1d1 100644 --- a/crates/app-iced/src/platform/motion.rs +++ b/crates/app-iced/src/platform/motion.rs @@ -208,6 +208,10 @@ mod tests { use super::*; /// 三态必须**三**个值可区分:`Unknown` 不能被当成 `Reduced`(M256 的硬约束 C)。 + /// + /// 抓得住:把 `is_reduced()` 写成「不是 AnimationsOn 就算 Reduced」(= 探针失败就静默关动效, + /// 于是某些机器上动效神秘消失)——第一条断言立刻红。 + /// 抓不住:真机上这三个值读得对不对(那要改系统设置,见报告的未验清单)。 #[test] fn unknown_is_not_reduced() { assert!(!ReduceMotion::Unknown.is_reduced());