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

+28 -3
View File
@@ -1,8 +1,9 @@
//! 导航轨底部**账户头像位**的状态换算(M114)。
//! 导航轨底部**账户头像位**的状态换算(M114)+ 轨自身的折叠档(M300)。
//!
//! 为什么单独一个文件:`app/view.rs` 贴着 400 行硬线,而这块逻辑与页面树无关——
//! 它只回答「轨底那个头像位现在该显示成未登录,还是某个账户」。
//! 它只回答「轨底那个头像位现在该显示成未登录,还是某个账户」,以及「这一帧轨多宽」。
use crate::app::components::motion;
use crate::shell;
use super::super::state::App;
@@ -16,12 +17,17 @@ const RAIL_COMPACT_HEIGHT: f32 = 720.0;
impl App {
/// 导航轨底部头像位的状态(M114):未登录 / 已登录两态要一眼能分。
///
/// M300:这里同时是轨的折叠档与过渡进度的**唯一构造点**——`shell::NavRail`(不在 scope 内)
/// 只负责把它转交给 `nav::rail`,几何全部来自 [`shell::RailAccount::nav_width`]。
pub(super) fn rail_account(&self) -> shell::RailAccount {
let compact = self.height < RAIL_COMPACT_HEIGHT;
let (collapsed, progress) = self.nav_state();
match self.state.active_account() {
Some(account) => {
shell::RailAccount::signed_in(account.monogram(), account.name.clone())
.compact(compact)
.nav(collapsed, progress)
}
// M106:未登录标签跟着**生效语言**走(早期钉死 `Lang::En` 的写法已删)。
None => shell::RailAccount::signed_out(
@@ -29,7 +35,26 @@ impl App {
.catalog()
.text(unlucky_i18n::Key::AccountRailSignedOut),
)
.compact(compact),
.compact(compact)
.nav(collapsed, progress),
}
}
/// 轨的折叠档 + **这一帧**的过渡进度(0 = 折叠 64,1 = 展开 88)。
///
/// 进度是**采样**来的,不是自己算的:`motion::nav(open)` 在「帧泵的目标与 `open` 不符」时
/// 直接给终值,于是截图 harness / 单测直接摆状态也不会把界面卡在中间那一帧。
fn nav_state(&self) -> (bool, f32) {
let collapsed = self.state.prefs.nav_collapsed;
(collapsed, motion::nav(!collapsed))
}
/// 这一帧导航轨的宽度(展开 88 ⇄ 折叠 64,中间帧按动效进度插值)。
///
/// **给用例用**:它读的就是 `view()` 递给 `nav::rail` 的那个值(同一个 `RailAccount`),
/// 所以「点击后宽度真的变了」这条断言不会与视图各算一套而假绿。
#[cfg(test)]
pub(in crate::app) fn nav_rail_width(&self) -> f32 {
self.rail_account().nav_width(self.ui().metrics())
}
}