`interaction::disabled` 的四个入口一直收一个「底」,但 8 个调用点全传死 `Role::Surface`:卡片(SurfaceContainer)/ 面板(SurfaceContainerLow)里的禁用控件 把 10%(按钮族)/ 12%(面状控件族)叠在画布色上 —— M223「CR 1.05、形丢失」的根因。 - 新增 `interaction::Base`(画布 / 容器底的一等参数,`From<Role>` 兼容历史调用点); 四个入口改成 `(ui, base, …)`,内部不再出现任何写死的 `Role::Surface`。 - 容器导出自己的 `BASE`:`surfaces::elevated::BASE`(卡)、`inputs::menu::PANEL_BASE` (菜单面板,项的 38% 与状态层都改叠它)、`navigation::drawer::BASE`。 - 控件收 `.base(..)`:Button / icon_on / Field / SearchBar / Slider / Checkbox / Radio / Switch / Chip / ListItem / Tabs;Fab 与 icon() 是画布默认值。 - `feedback/action.rs` 的本地 α 合成删掉,改走 `disabled_content_over_role` 并给真实容器底 (Snackbar `InverseSurface` / Dialog `SurfaceContainerHigh`)。 - 判据:`interaction::disabled::tests`(卡片/面板/画布三档逐值)、 `buttons::tests::base`(Button::base → StyleSpec → style_closure 接线)、 `gallery` 新增 `disabledbase` 段 + `gallery::probe::bases` 精确像素断言 (±10 容差区分不了 1/255 的差,所以自己数逐位相等的像素)。 - 变异自证:assemble 的调用点改回画布底后,上面两条都红(见 artifacts/m234/README.md)。 已知未尽:`crates/app-iced/src/pages/**` 不在 scope 内,页面侧「把控件放进卡片/面板」的 调用点尚未接 `.base(..)`(行为与本 mission 之前一致);`pages/…/java/tests/marker.rs` 有一行被迫更新(入口多了一个实参,语义不变 = 画布底)。
148 lines
5.7 KiB
Rust
148 lines
5.7 KiB
Rust
//! 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::super::interaction::Base;
|
||
use super::geometry::{NavLayout, NavPaneGeometry};
|
||
|
||
mod item;
|
||
use item::{line, nav_row};
|
||
pub use item::{NavGroup, NavItem};
|
||
|
||
/// 导航容器的底:§21.3 标准(常驻)= `Surface` + L0 —— 所以项的禁用内容叠在画布色上。
|
||
///
|
||
/// 与 [`navigation`] 的 `ui.layer(MaterialLayer::Canvas)` 同源(`Canvas` 的 surface_role 就是
|
||
/// `Surface`),M234 之后「底」由这里给,项自己不猜。
|
||
pub const BASE: Base = Base::of(Role::Surface);
|
||
|
||
/// 导航容器(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, BASE, 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;
|