Files
Unlucky-Launcher/crates/app-iced/src/components/navigation/drawer.rs
T
AskaEth 81e34d77da feat(app-iced): M79 导航家族 —— ListItem / Tabs / NavigationDrawer / AppBar
四件组件 + gallery 状态矩阵 + 渲染层几何断言,全部只落在
`components/navigation/**` 与 `gallery/navigation.rs`(不改 mod.rs / interaction / layout)。

- ListItem(12b §4):一行/两行/三行 44/60/76(A1 56/72/88,登记偏离)、leading/trailing 槽位、
  hover/focus/pressed 只叠状态层(内容色不变)、禁用是修饰器且内容角色取 A1 的 `OnSurface`@38%。
- Tabs(12b §6):Primary 3px 指示条(用 `Stack` 分层让 Fill 条拿到「该 tab 的宽」)、
  Secondary 1px `SurfaceVariant` 分隔线、带图标时容器 64、横向滚动;选中色按变体分(Primary / OnSurface)。
- NavigationDrawer(15 §21 + D33):**同一个容器两个宽度** —— rail 88 ⇄ drawer 240;
  药丸从容器宽派生(56×32 / 216×56,左右各缩 16 / 12,A1 `ItemPadding`);
  分组标题 TitleSmall、组间 1px OutlineVariant;drawer 项高 56(G24)。
- AppBar small(12b §16):页头 56(A1 64,登记偏离)、TitleLarge emphasized + LabelMedium 副标题、
  leading/trailing 槽位、滚动只换 `SurfaceContainer` + L2(A1 无 OnScroll…Height)。
- gallery:`ULL_NAV_PART=list|tabs|drawer|appbar|all` 分块出图;四块矩阵(ListItem 三行高 × 三态、
  Tabs 三个选中位置 + 五态、rail/drawer 同尺度并排、AppBar 静息/滚动)。
- `navigation/probe.rs`:读真截图量像素的 4 条断言(行高 44/60/76、禁用 OnSurface@38%、
  指示条宽 = 标签墨迹 + 2×16、rail 88 / drawer 240 与药丸内缩 16/12、AppBar 56),
  已用两次变异(行高改 56/72/88;drawer 内缩改 16)验证能被打红。

门禁:fmt ✓ · clippy -p unlucky-app-iced --all-targets -D warnings ✓ · test -p unlucky-app-iced 195 passed ✓
2026-11-02 11:55:59 +08:00

141 lines
5.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! NavigationDrawer / NavigationRail(`research/15` §21 + 12b §5):**同一个容器的两个宽度**(D33)。
//!
//! 本文件只装「容器」:宽度(88 ⇄ 240)、`CornerLargeEnd` 圆角、分组标题、组间分隔线。
//! 一项长什么样(`NavItem` / `NavGroup` / 颜色求值 / 药丸排布)在 `drawer/item.rs`。
//!
//! ## 参考值与 D33(详见 `geometry.rs` 的表)
//!
//! ```text
//! 折叠 = Rail 88 = LayoutMetrics::nav_rail_width 项高 52 = 药丸 32 + 4 + 标签 16
//! 展开 = Drawer 240 = LayoutMetrics::nav_drawer_width 项高 56 = A1 ActiveIndicatorHeight(G24)
//! 药丸:rail 88 − 2×16 = 56(M68 的 56×32);drawer 240 − 2×12 = 216(= A1 的 360 − 2×12)
//! ```
//!
//! 两者共用 [`navigation`] 一个入口、一套颜色角色、一套 full 圆角药丸;切换时圆角语言不变,
//! 一屏导航容器数恒为 1(§21.9)。**不是两套组件**——D33 的可验收形式就是「同一函数两个宽度」。
//!
//! ## Q8 的现场
//!
//! M68 的药丸写 56 宽后用 `center_x(Length::Fill)` 居中,容器尺寸被 Fill 覆盖成 88 宽(图上横贯全轨)。
//! 这里药丸宽由 [`NavPaneGeometry::pill_width`] 给出,居中一律交给父级 `align_x`。
//!
//! ## 分组标题(只 drawer 侧,§21.8)
//!
//! `TitleSmall` + `OnSurfaceVariant`,**不可点**;组与组之间 1px `OutlineVariant`
//! (复用 [`crate::app::components::divider`]),不用背景色块分区。
use iced::widget::container;
use iced::{Background, Border, Element, Length, Padding};
use unlucky_design::color::Role;
use unlucky_design::elevation::MaterialLayer;
use unlucky_design::typography::TypeRole;
use crate::theme::Ui;
use super::super::divider;
use super::geometry::{NavLayout, NavPaneGeometry};
mod item;
use item::{line, nav_row};
pub use item::{NavGroup, NavItem};
/// 导航容器(D33:`layout` 决定 88 还是 240,其余全同)。
#[derive(Debug, Clone)]
pub struct Navigation<'a, M> {
layout: NavLayout,
groups: Vec<NavGroup<'a, M>>,
}
impl<'a, M> Navigation<'a, M> {
/// 折叠态:rail(88)。
pub fn rail() -> Self {
Self {
layout: NavLayout::Rail,
groups: Vec::new(),
}
}
/// 展开态:标准常驻 drawer(240)。
pub fn drawer() -> Self {
Self {
layout: NavLayout::Drawer,
groups: Vec::new(),
}
}
/// 追加分组(组间自动画 1px `OutlineVariant`)。
pub fn group(mut self, group: NavGroup<'a, M>) -> Self {
self.groups.push(group);
self
}
}
/// 导航容器(rail ⇄ drawer 的唯一入口)。
pub fn navigation<'a, M: Clone + 'a>(ui: &'a Ui, nav: Navigation<'a, M>) -> Element<'a, M> {
let geometry = NavPaneGeometry::of(ui, nav.layout);
let gap = match nav.layout {
NavLayout::Rail => ui.metrics().nav_item_gap,
// drawer 的项是紧贴的(56 已经含了内部留白,§21.4)
NavLayout::Drawer => 0.0,
};
let mut body = iced::widget::Column::new().spacing(gap).width(Length::Fill);
for (index, group) in nav.groups.into_iter().enumerate() {
// 组间分隔线只属于 drawer:rail 88 宽里一条通栏线只会切碎药丸的节奏(§21.8 的分区是 drawer 语义)
if index > 0 && matches!(nav.layout, NavLayout::Drawer) {
body = body.push(container(divider::full::<M>(ui)).padding(Padding {
top: ui.space(unlucky_design::spacing::Space::X2),
bottom: ui.space(unlucky_design::spacing::Space::X2),
left: 0.0,
right: 0.0,
}));
}
// 标题只在 drawer 侧出现(rail 88 宽放不下文字,§21.8 的分区是 drawer 语义)
if let (NavLayout::Drawer, Some(title)) = (nav.layout, group.title) {
body = body.push(
container(line::<M>(
ui,
title,
TypeRole::TitleSmall,
ui.color(Role::OnSurfaceVariant),
))
.width(Length::Fill)
.padding(Padding {
top: ui.space(unlucky_design::spacing::Space::X2),
right: geometry.pad_start,
bottom: ui.space(unlucky_design::spacing::Space::X1),
left: geometry.pill_inset + geometry.pad_start,
}),
);
}
for item in group.items {
body = body.push(nav_row(ui, item, geometry));
}
}
container(body)
.width(Length::Fixed(geometry.width))
.height(Length::Fill)
// 轨顶留白走 padding(不是 spacer 元素:会被 column 的 spacing 再加一档,M68 P3-5)
.padding(Padding {
top: ui.space(unlucky_design::spacing::Space::X6),
right: 0.0,
bottom: 0.0,
left: 0.0,
})
.style(move |_theme| container::Style {
// §21.3 标准(常驻)= Surface + L0;形状 CornerLargeEnd(内侧两角 16)
background: Some(Background::Color(ui.layer(MaterialLayer::Canvas))),
border: Border {
radius: geometry.radius,
..Border::default()
},
..container::Style::default()
})
.into()
}
#[cfg(test)]
mod tests;