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:
AskaEth committed 2026-11-13 16:15:16 +08:00
1 parent 1ce494631c
commit fa53d72f73
26 files changed
+1640 -320

No files matched your search

@@ -126,6 +126,22 @@ pub(super) fn env_motion_fixture_at() -> Option<u32> {
.filter(|beats| *beats > 0)
}
/// `ULL_NAV_COLLAPSED=1`:出图通道直接把左轨钉在**折叠档**(M300)。
///
/// ## 为什么出图必须靠它,而不是靠配置文件
///
/// 截图通道**不读用户配置**(D30:同一份代码在两台机器上要出同一张图,见 `construct.rs` 里
/// 外观/语言/主题那几处同样的处理)。而折叠态与展开态是两张必须同页同主题的交付图 ⇒ 需要一条
/// 只影响这一次进程的显式开关,与 `ULL_APPEARANCE_*` / `ULL_TOAST_FIXTURE` 同族。
///
/// 产品路径读不到它:只有 `--screenshot` 时 `construct.rs` 才会问这个函数。
pub(in crate::app) fn env_nav_collapsed() -> bool {
matches!(
std::env::var("ULL_NAV_COLLAPSED").ok().as_deref(),
Some("1" | "true" | "yes")
)
}
/// 把启动那一拍的**事实**写进启动器自己的日志(M187 / r1 复审 P2-4)。
///
/// ## 为什么这一拍只能「先记、后落」