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
@@ -115,7 +115,7 @@ mod tests {
|
||||
//! 时间轴用 `motion::advance_by` 摆(不等真实时钟),每个用例在自己的线程上跑,互不干扰。
|
||||
|
||||
use super::*;
|
||||
use crate::app::components::motion::{Slot, FRAME_MS};
|
||||
use crate::app::components::motion::FRAME_MS;
|
||||
use crate::cli::Options;
|
||||
use crate::pages;
|
||||
use crate::theme::Families;
|
||||
@@ -341,10 +341,58 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// 槽枚举的可见性检查:`Slot` 只经帧泵导出(页面拿不到「给我 250ms」这种口子)。
|
||||
/// 「**页面拿不到时长**」的源码守卫(M256 取代 M192 的 `Slot` 三条断言)。
|
||||
///
|
||||
/// 口径:`pages/**` 的产品代码里不许出现时长的任何入口(`DurationToken` / `page_spec` /
|
||||
/// `modal_spec` / `FRAME_MS` / `advance(`):页面只能说「我要换页了」,用哪一档、多久由
|
||||
/// `components/motion/transition.rs` 那张表决定(D31)。
|
||||
///
|
||||
/// **抓得住**:页面里写 `DurationToken::Medium2` / `motion::FRAME_MS` / 自己推时间轴
|
||||
/// (变异:在 `pages/download.rs` 顶上加 `use unlucky_design::motion::DurationToken;` ⇒ 红)。
|
||||
/// **抓不住**:①表里某一档的数字写错(那是 `components/motion/tests` 的登记用例);②绕过
|
||||
/// 符号名的路径(转一手、通配导入)——只按符号名扫,不做类型分析。
|
||||
#[test]
|
||||
fn slots_are_the_three_documented_ones() {
|
||||
assert_eq!(Slot::ALL.len(), 3);
|
||||
assert_eq!(Slot::ALL.map(Slot::name), ["boot", "screen", "detail"]);
|
||||
fn pages_cannot_reach_for_a_duration() {
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("src")
|
||||
.join("pages");
|
||||
let mut files = Vec::new();
|
||||
collect_rust_files(&dir, &mut files);
|
||||
assert!(
|
||||
files.len() >= 20,
|
||||
"只扫到 {} 个页面文件:扫描逻辑坏了",
|
||||
files.len()
|
||||
);
|
||||
|
||||
const FORBIDDEN: [&str; 5] = [
|
||||
"DurationToken",
|
||||
"page_spec",
|
||||
"modal_spec",
|
||||
"FRAME_MS",
|
||||
"advance(",
|
||||
];
|
||||
let mut hits = Vec::new();
|
||||
for path in files {
|
||||
let text = std::fs::read_to_string(&path).expect("读源文件");
|
||||
// 只看产品代码(测试与文档注释不算:`#[cfg(test)]` 之后整段跳过)。
|
||||
let product = text.split("#[cfg(test)]").next().unwrap_or(&text);
|
||||
let code = product
|
||||
.lines()
|
||||
.filter(|line| !line.trim_start().starts_with("//"));
|
||||
for symbol in FORBIDDEN {
|
||||
if code.clone().any(|line| line.contains(symbol)) {
|
||||
let name = path
|
||||
.strip_prefix(&dir)
|
||||
.unwrap_or(&path)
|
||||
.to_string_lossy()
|
||||
.replace(std::path::MAIN_SEPARATOR, "/");
|
||||
hits.push((name, symbol));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
hits.is_empty(),
|
||||
"页面层不许直接碰时长/时间轴(用哪一档是 `transition.rs` 的表决定的):{hits:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user