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
@@ -10,7 +10,7 @@ use unlucky_design::color::Role;
|
||||
use unlucky_design::elevation::MaterialLayer;
|
||||
use unlucky_design::icons::IconKind;
|
||||
use unlucky_design::shape::ShapeScale;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_design::spacing::{LayoutMetrics, Space};
|
||||
use unlucky_design::typography::TypeRole;
|
||||
|
||||
use crate::app::components::interaction::{self, Interaction};
|
||||
@@ -20,13 +20,24 @@ use crate::icons;
|
||||
use crate::shell::overridden;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::{nav_item_height, nav_rail_width, NavItem};
|
||||
use super::tip::tip;
|
||||
use super::{nav_item_height, NavItem};
|
||||
|
||||
/// 导航轨底部的**账户头像位**状态(M114)。
|
||||
/// 导航轨底部的**账户头像位**状态(M114)+ 轨自身的折叠态(M300)。
|
||||
///
|
||||
/// 为什么单独传而不是塞进 `NavItem`:它不是主序列里的第 6 项(那会让轨道越堆越长),
|
||||
/// 而是固定在**底部**的一个头像位;未登录/已登录两态要在**形状上**就不同(描边剪影 vs 首字母块)。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
///
|
||||
/// ## 为什么折叠态也搭这个结构(M300)
|
||||
///
|
||||
/// `shell::NavRail`(`shell/mod.rs`)是 `nav::rail(..)` 唯一的入参通道,而那个文件不在本
|
||||
/// mission 的 scope 里;`RailAccount` 是**唯一**由 `app/view/rail.rs`(scope 内)构造、又直接递给
|
||||
/// `nav::rail`(scope 内)的值。于是轨的「这一帧多宽 / 标签收不收」搭它进来,一个字段都不新增到
|
||||
/// `NavRail` 上。代价是这个结构现在有两个职责——已经比「动 `shell/mod.rs` 去加第三个字段」便宜,
|
||||
/// 且它们确实是同一块几何的两个输入(轨的宽与轨底的内容)。
|
||||
///
|
||||
/// `f32` 的进度进场后 `Eq` 不再成立(`PartialEq` 仍在):调用方只比字段,不做整结构相等。
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RailAccount {
|
||||
/// 是否已登录(至少有一个账户)
|
||||
pub signed_in: bool,
|
||||
@@ -36,6 +47,10 @@ pub struct RailAccount {
|
||||
pub label: String,
|
||||
/// 窄高度/窄宽下的图标条形态(隐藏文字标签,只留头像位)
|
||||
pub compact: bool,
|
||||
/// 用户把轨折叠了(M300):只留图标 + tooltip。判据只有一处(`ItemView::labels_visible`)。
|
||||
nav_collapsed: bool,
|
||||
/// 折叠进度的**这一帧**取值(0 = 折叠 64,1 = 展开 88)。中间帧由 `motion::nav(open)` 给。
|
||||
nav_progress: f32,
|
||||
}
|
||||
|
||||
impl RailAccount {
|
||||
@@ -46,6 +61,9 @@ impl RailAccount {
|
||||
initial: String::new(),
|
||||
label: label.into(),
|
||||
compact: false,
|
||||
// 出厂:轨展开(`UiPrefs::nav_collapsed` 的出厂值也是 false)
|
||||
nav_collapsed: false,
|
||||
nav_progress: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +74,8 @@ impl RailAccount {
|
||||
initial: initial.into(),
|
||||
label: label.into(),
|
||||
compact: false,
|
||||
nav_collapsed: false,
|
||||
nav_progress: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +85,33 @@ impl RailAccount {
|
||||
self.compact = compact;
|
||||
self
|
||||
}
|
||||
|
||||
/// 轨的折叠档 + **这一帧**的过渡进度(M300)。
|
||||
///
|
||||
/// 两个值必须一起给:只给 `collapsed` 会让布局按一个过期的进度算宽,只给进度会让标签在
|
||||
/// 折叠档里还画着。`progress` 会被夹到 `0..=1`(`nav_width` 里再夹一次,这里只是让调用方
|
||||
/// 传什么都不会把轨画成负数宽)。
|
||||
#[must_use]
|
||||
pub fn nav(mut self, collapsed: bool, progress: f32) -> Self {
|
||||
self.nav_collapsed = collapsed;
|
||||
self.nav_progress = progress.clamp(0.0, 1.0);
|
||||
self
|
||||
}
|
||||
|
||||
/// 轨现在是不是折叠档(标签该不该收)。
|
||||
#[must_use]
|
||||
pub fn nav_collapsed(&self) -> bool {
|
||||
self.nav_collapsed
|
||||
}
|
||||
|
||||
/// 这一帧的轨宽(展开 88 ⇄ 折叠 64;中间帧按动效进度插值)。
|
||||
///
|
||||
/// **唯一实现点是 `super::nav_width`**:`nav::rail`、`account_item`、`toggle_button` 与用例
|
||||
/// 都读它,于是「谁把这四个调用点各算一遍」在结构上不可能。
|
||||
#[must_use]
|
||||
pub fn nav_width(&self, metrics: LayoutMetrics) -> f32 {
|
||||
super::nav_width(metrics, self.nav_progress)
|
||||
}
|
||||
}
|
||||
|
||||
/// 轨底的账户头像位:未登录 = 描边剪影,已登录 = 首字母块(两态在**形状**上就不同)。
|
||||
@@ -133,12 +180,16 @@ pub(super) fn account_item<'a>(
|
||||
.into()
|
||||
};
|
||||
|
||||
let width = account.nav_width(ui.metrics());
|
||||
let height = nav_item_height(ui, indicator_height);
|
||||
// M300:折叠档与「窄高图标条」是同一条判据——名字("Not signed in" / 账户名)在 64 宽的轨里
|
||||
// 放不下,收起来之后可读名改由 tooltip 给(与主序列项同一条纪律)。
|
||||
let labels_visible = !account.compact && !account.nav_collapsed();
|
||||
let mut stack = column![avatar]
|
||||
.spacing(ui.space(Space::X1))
|
||||
.width(nav_rail_width(ui.metrics()))
|
||||
.width(width)
|
||||
.align_x(Alignment::Center);
|
||||
if !account.compact {
|
||||
if labels_visible {
|
||||
// 用**拥有的**字符串画标签:`RailAccount` 是调用方临时算出来的状态,
|
||||
// 借它的 `&str` 会让整棵树跟着那个临时值一起活(E0515)。
|
||||
let style = ui.style(TypeRole::LabelSmall);
|
||||
@@ -154,7 +205,7 @@ pub(super) fn account_item<'a>(
|
||||
}
|
||||
|
||||
let clickable: Element<'a, Message> = button(stack)
|
||||
.width(nav_rail_width(ui.metrics()))
|
||||
.width(width)
|
||||
.height(height)
|
||||
.padding(0)
|
||||
.style(move |_theme: &IcedTheme, _status| button::Style {
|
||||
@@ -168,10 +219,16 @@ pub(super) fn account_item<'a>(
|
||||
.on_press(Message::Select(NavItem::Account))
|
||||
.into();
|
||||
// A8:运行期态套状态取样控件(强制态 = 取证,**不套**:取证图不许有过渡中间帧)。
|
||||
if forced.is_none() {
|
||||
let clickable = if forced.is_none() {
|
||||
interaction::tracked(clickable, true)
|
||||
} else {
|
||||
clickable
|
||||
};
|
||||
// M300:名字不画的时候,头像位也必须说得出自己是谁(否则折叠态的轨底就是一个孤零零的剪影)。
|
||||
if labels_visible {
|
||||
clickable
|
||||
} else {
|
||||
tip(ui, clickable, account.label.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,10 +334,8 @@ mod tests {
|
||||
let width = super::super::indicator_size(ui.metrics()).0;
|
||||
let size = avatar_size(&ui, width);
|
||||
assert!(size > 0.0);
|
||||
assert!(
|
||||
size <= nav_rail_width(ui.metrics()),
|
||||
"头像位不能比导航轨还宽({size} > {})",
|
||||
nav_rail_width(ui.metrics())
|
||||
);
|
||||
// 展开档是两档里宽的那一档:头像位连它都放得下,折叠档才不需要另算一遍。
|
||||
let rail = ui.metrics().nav_rail_width;
|
||||
assert!(size <= rail, "头像位不能比导航轨还宽({size} > {rail})");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user