feat(nav): M300 导航轨折叠按钮 + 折叠宽度令牌 + 宽度过渡与落盘
用户原话「默认左侧固定,允许折叠」:M297 盘查确认 iced 从来没接过折叠按钮, presenter 的 toggle_nav/nav_collapsed 是 Slint 遗留、唯一调用方在已退役的 crates/app。 ## 令牌(先定数值再动手) - nav_collapsed_width = 56 + 2 × Space::X1 = **64**:胶囊(M283 定稿 56×32,本轮不动) 两侧各留 4px 才不被裁切;旧值 48 的死因写进注释(48 − 2×4 = 40 < 56)。 - 断言:折叠宽 ≥ 胶囊宽 + 2×X1、≥ touch_target 48、在 4px 栅格上。 ## 折叠按钮与折叠态 - 按钮放**轨底、头像位之上**:轨顶一个像素都不动(M283/M299 的展开态对照图仍可比), 轨底本来就是「固定贴底」的槽,两态下位置不变。几何与导航项同一套(56×32 药丸 + 52 命中高)。 - 折叠态:隐藏文字标签与账户名,每项/头像/按钮都补 M3 气泡 tooltip(折叠态不许是一列无名符号); 纵节奏不变(项高仍 52),所以切换时图标不上下跳。五项图标两两不同由用例钉住。 ## 动效(复用底盘、零新常数) - 帧泵加**第二条并列宽度轨**(nav ⇄ rail),时长/曲线复用同一张 rail_spec(); 借用右栗那条会让左轨折叠改掉右栗的目标(暗耦合),tower 已否掉那个方案。 - 起跑在 update 侧;启动按落盘档对表(幂等,仅在没有轨道在跑时);折叠**不置 view_dirty** —— 宽度在 view 里逐帧采样,帧由轨道自己续,跑完 needs_frames() 回 false。 ## 落盘(任务要求「重启读回仍折叠」) - core schema 登记 ui.nav.collapsed(纯增量 bool,不碰 schemaVersion);presenter 桥加 load/persist 一对(走唯一写入口 write_with,现读→改→落盘);启动 construct 读回。 - 截图通道不读用户配置(D30),折叠态出图走 ULL_NAV_COLLAPSED=1。 ## 判据 - app-iced: 1053 passed(新增 3 条接线用例 + 7 条 shell/几何/树结构用例 + 6 条帧泵用例); presenter: 408 passed(新增落盘往返 1 条);fmt / clippy --workspace --all-targets 均 0。 - 变异实验:删 observe_nav ⇒ 宽度停在 88 红;删启动对表 ⇒ 首次展开硬跳红。 - 像素(DevDocs/artifacts/m300/):折叠态内容左缘 112→88(24px 真回收)、药丸两态都 56 宽 且折叠档右缘 59 < 64(不裁切)、图标行是展开态子集(纵节奏不变)。 ## 已知偏离(已登记,tower 批了紧随 mission) crates/app-iced/src/pages/layout.rs 的 rail_width() 仍是 const fn(恒 88),所以折叠态内容块 宽度不变、整块左移 24px ⇒ 右侧多一条 24px 空白带(像素图里保留、未裁)。
This commit is contained in:
1 parent
1ce494631c
commit
fa53d72f73
26 files changed
+1640
-320
No files matched your search
@@ -0,0 +1,177 @@
|
||||
//! 左轨(导航轨)宽度轨道(M300 `NAVCOLLAPSE`)的判据:与右栗**并列**、同一张表、端点精确。
|
||||
//!
|
||||
//! 判据口径同 M257(`14-motion-and-animation.md` §7-7):**不靠像素比对**。像素只能证明「某一张图
|
||||
//! 里宽度是多少」,证明不了「中间有十几个相位」与「结束时不多要一帧」。
|
||||
//!
|
||||
//! 全部用**本地** `Pump` 连拍 `advance(FRAME_MS)`(T37 的「从驱动源连拍」):这里不经过
|
||||
//! `App`、不等真实时钟,于是「状态已切换但没有事件」那段完全是自然覆盖的——两次 `advance`
|
||||
//! 之间没有任何别的调用,时间轴照样往前走。
|
||||
|
||||
use super::{settle, standard};
|
||||
use crate::app::components::motion::*;
|
||||
use unlucky_design::motion::MotionScale;
|
||||
|
||||
/// 一路采样左轨进度(含终点),用完自己 settle。
|
||||
fn nav_samples(pump: &mut Pump) -> Vec<f32> {
|
||||
let mut out = vec![pump.nav_progress()];
|
||||
let mut guard = 0;
|
||||
while pump.is_animating() {
|
||||
pump.advance(FRAME_MS);
|
||||
out.push(pump.nav_progress());
|
||||
guard += 1;
|
||||
assert!(guard < 200, "左轨轨道没在跑完,疑似终止条件失效");
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// **展开:帧数落在 12~20、端点精确、中间严格单调**(`medium1` 250ms / 16ms)。
|
||||
///
|
||||
/// 抓得住:①时长被改成 600ms(帧数窜到 38)或 0(硬跳);②`expire` 漏了新轨道(`while` 跑满
|
||||
/// 200 帧的兜底断言红);③插值写成阶跃(单调性断言的 `>` 会变成 `==`)。
|
||||
/// 抓不住:观感是否像 M3E——那要出图(`DevDocs/artifacts/m300/`)。
|
||||
#[test]
|
||||
fn expanding_the_nav_rail_runs_the_expected_frames_and_lands_on_one() {
|
||||
let mut pump = standard();
|
||||
pump.observe_nav(false);
|
||||
settle(&mut pump);
|
||||
assert_eq!(pump.nav_progress(), 0.0, "前提:先落到折叠档");
|
||||
|
||||
assert!(pump.observe_nav(true), "目标变了必须起跑");
|
||||
let values = nav_samples(&mut pump);
|
||||
let frames = values.len() - 1;
|
||||
assert!(
|
||||
(12..=20).contains(&frames),
|
||||
"250ms / 16ms 应落在 12~20 帧,实测 {frames} 帧"
|
||||
);
|
||||
assert_eq!(values.last().copied(), Some(1.0), "结束必须**严格**是 1.0");
|
||||
for pair in values.windows(2) {
|
||||
assert!(pair[1] > pair[0], "展开必须严格单调:{:?}", pair);
|
||||
}
|
||||
assert!(!pump.is_animating(), "跑完必须清空轨道(否则空闲烧帧)");
|
||||
assert_eq!(pump.nav_progress(), 1.0, "没有轨道时是终值");
|
||||
}
|
||||
|
||||
/// **折叠:反向同一条判据**(`short4` 200ms ⇒ 10~20 帧)。
|
||||
#[test]
|
||||
fn collapsing_the_nav_rail_is_monotone_downwards_and_stops_at_zero() {
|
||||
let mut pump = standard();
|
||||
assert!(pump.observe_nav(false), "出厂展开 ⇒ 折叠必须起跑");
|
||||
let values = nav_samples(&mut pump);
|
||||
let frames = values.len() - 1;
|
||||
assert!(
|
||||
(10..=20).contains(&frames),
|
||||
"200ms / 16ms 应落在 10~20 帧,实测 {frames} 帧"
|
||||
);
|
||||
assert_eq!(values.last().copied(), Some(0.0), "结束必须严格是 0.0");
|
||||
for pair in values.windows(2) {
|
||||
assert!(pair[1] < pair[0], "折叠必须严格单调下降");
|
||||
}
|
||||
assert!(!pump.is_animating());
|
||||
}
|
||||
|
||||
/// **`MotionScale::NONE`:严格等于终值,且一帧都不要。**
|
||||
///
|
||||
/// 抓得住:把 `rail_track` 的 `is_off()` 分支删掉(插一条 0 时长轨道 ⇒ 进度仍是终值,但
|
||||
/// `is_animating()` 会有一拍为真 ⇒ 「一帧都不要」红)。
|
||||
/// 抓不住:中间帧该不该存在(NONE 下不许有,这是定义)。
|
||||
#[test]
|
||||
fn motion_scale_none_puts_the_nav_rail_strictly_at_the_end_value() {
|
||||
let mut pump = Pump::new();
|
||||
pump.configure(MotionScale::NONE, None);
|
||||
|
||||
assert!(!pump.observe_nav(false), "NONE 下不许起跑轨道");
|
||||
assert_eq!(pump.nav_progress(), 0.0, "折叠档终值必须逐位是 0.0");
|
||||
assert!(!pump.is_animating(), "NONE 下一帧都不许要");
|
||||
|
||||
assert!(!pump.observe_nav(true));
|
||||
assert_eq!(pump.nav_progress(), 1.0);
|
||||
assert!(!pump.is_animating());
|
||||
}
|
||||
|
||||
/// **两条宽度轨互不干扰**(tower 否掉「借用右栗那条轨道」的那个方案,这条就是它的判据)。
|
||||
///
|
||||
/// 场景:右栗先起跑展开,**同一拍**再折叠左轨。两条轨道各自从自己的起点走,谁也不改谁的
|
||||
/// 目标;`Pump::is_animating()` 在两条都跑完之前一直为真(帧订阅不会提前关)。
|
||||
///
|
||||
/// 抓得住:①把 `observe_nav` 接回 `rail`/`rail_open`(那会当场改掉右栗的目标 ⇒ 第二条断言红);
|
||||
/// ②`expire` 只清一条轨道(先跑完的那条被留下 ⇒ 最后一条「谁都不在动」红);
|
||||
/// ③`is_animating` 漏了新轨道(跑到一半就 false)。
|
||||
/// 抓不住:产品里两个按钮同时被点到的时序(那归 app 侧的 `ToggleNav` 用例)。
|
||||
#[test]
|
||||
fn the_two_width_tracks_never_retarget_each_other() {
|
||||
let mut pump = standard();
|
||||
pump.observe_rail(true);
|
||||
pump.advance(FRAME_MS);
|
||||
|
||||
assert!(pump.observe_nav(false), "左轨折叠必须自己起一条轨道");
|
||||
// 右栗的目标没被左轨改掉:它继续往 1.0 走,而不是跳到左轨的方向。
|
||||
let right = pump.rail_progress();
|
||||
pump.advance(FRAME_MS);
|
||||
assert!(
|
||||
pump.rail_progress() > right,
|
||||
"右栗的进度必须继续上涨({} → {})",
|
||||
right,
|
||||
pump.rail_progress()
|
||||
);
|
||||
assert!(pump.nav_progress() < 1.0, "左轨同时在往下走");
|
||||
|
||||
// 两条轨道都跑完:一条也不许留下(否则空闲烧帧)。
|
||||
for _ in 0..200 {
|
||||
if !pump.is_animating() {
|
||||
break;
|
||||
}
|
||||
pump.advance(FRAME_MS);
|
||||
}
|
||||
assert!(!pump.is_animating(), "两条轨道都必须到期删除");
|
||||
assert_eq!(pump.rail_progress(), 1.0);
|
||||
assert_eq!(pump.nav_progress(), 0.0);
|
||||
}
|
||||
|
||||
/// **中途反向不跳变**:折叠到一半改主意展开,下一帧从当前值继续(不是回 0/1)。
|
||||
#[test]
|
||||
fn reversing_the_nav_rail_mid_flight_continues_from_the_current_progress() {
|
||||
let mut pump = standard();
|
||||
pump.observe_nav(false);
|
||||
// **走到真正的中间**再反向:收起走的是 `EmphasizedAccelerate`(起步很慢),固定推 4 帧
|
||||
// 只到 6%(实测 0.941),那不是「半路改主意」而是「刚起跑」。
|
||||
let mut guard = 0;
|
||||
while !(0.2..=0.8).contains(&pump.nav_progress()) {
|
||||
pump.advance(FRAME_MS);
|
||||
guard += 1;
|
||||
assert!(guard < 40, "没走到中间:{}", pump.nav_progress());
|
||||
}
|
||||
let before = pump.nav_progress();
|
||||
|
||||
pump.observe_nav(true);
|
||||
let after = pump.nav_progress();
|
||||
assert!(
|
||||
(after - before).abs() < 1e-6,
|
||||
"反向那一帧不许跳变:{before} → {after}"
|
||||
);
|
||||
}
|
||||
|
||||
/// **启动对表**:上次折叠着退出(落盘 true)时,第一次**展开**仍然有动画。
|
||||
///
|
||||
/// 这条守的是 `snap_nav` 存在的理由:泵的目标出厂是「展开」,若不对表,`observe_nav(true)`
|
||||
/// 会走「目标没变」的早退分支 ⇒ 展开变成一次硬跳(T37:数值对,但没人让它跑)。
|
||||
///
|
||||
/// 抓得住:删掉 `snap_nav` 的调用点(`app/update/motion_sync.rs` 那一句)后,本用例在泵层面
|
||||
/// 仍绿 ⇒ 真正的接线判据在 app 侧(`update::nav::tests`),这里只钉住泵的语义。
|
||||
#[test]
|
||||
fn snapping_to_the_persisted_state_keeps_the_first_expand_animated() {
|
||||
let mut pump = standard();
|
||||
// 出厂:目标=展开、没有轨道。模拟「上次折叠着退出」。
|
||||
assert_eq!(pump.nav_progress(), 1.0);
|
||||
pump.snap_nav(false);
|
||||
assert_eq!(pump.nav_progress(), 0.0, "对表之后必须严格是折叠档");
|
||||
assert!(!pump.is_animating(), "对表不许插轨道");
|
||||
|
||||
assert!(pump.observe_nav(true), "第一次展开必须真的起跑");
|
||||
pump.advance(FRAME_MS);
|
||||
assert!(
|
||||
pump.nav_progress() > 0.0 && pump.nav_progress() < 1.0,
|
||||
"起跑之后必须处在中间,而不是直接到位"
|
||||
);
|
||||
settle(&mut pump);
|
||||
assert_eq!(pump.nav_progress(), 1.0);
|
||||
}
|
||||
Reference in new issue
Block a user