diff --git a/crates/design/src/component_map/metrics.rs b/crates/design/src/component_map/metrics.rs index 17af086..add93f8 100644 --- a/crates/design/src/component_map/metrics.rs +++ b/crates/design/src/component_map/metrics.rs @@ -46,13 +46,14 @@ pub struct ComponentMetrics { pub inner_dot: f32, /// 控件相对容器的外距(dp):drawer 项 = 12,决定选中指示器左右各内缩 12。 pub inset: f32, - /// 内容起始侧内边距(dp)。 + /// 内容起始侧内边距(dp)。**图标不算在内**:图标可视边 = `pad_start + icon_offset`。 pub pad_start: f32, - /// 内容结束侧内边距(dp)。 + /// 内容结束侧内边距(dp)。同起始侧,图标可视边 = `pad_end + icon_offset`。 pub pad_end: f32, /// 图标边长(dp;`0.0` = 无图标)。 pub icon: f32, - /// 图标相对内边距的**额外**偏移(dp)。 + /// 图标相对内边距的**额外**偏移(dp)。图标可视边 = 内边距 + 本值(search bar:12 + 4 = 16), + /// **不要**把内边距本身当图标边用,也不要再手抄那个 12 / 16。 pub icon_offset: f32, /// 图标与文字(或相邻元素)的间距(dp)。 pub gap: f32, @@ -107,9 +108,14 @@ impl ComponentMetrics { /// /// 高度与宽度**不在这里**:条高 56 是骨架级(`LayoutMetrics::search_bar_height`), /// 最小宽 360 同样属骨架级(尚未进 LM,M69 报告里登记为待办)。这里只装画进 bar 里的几何: - /// 图标 24(`IconSize::Toolbar`)、可视左缘 16 = 文本框默认 12 + `SearchBarIconOffsetX = 4`、 - /// 右端同样 16(§20.4)。圆角走 `ShapeScale::Full`(56 → 28 = 官方 `SearchBarCornerRadius`), + /// 图标 24(`IconSize::Toolbar`)、**图标可视边 = `pad_start + icon_offset`** = 12 + 4 = **16** + /// (右侧同式为 16,§20.4)、圆角走 `ShapeScale::Full`(56 → 28 = 官方 `SearchBarCornerRadius`), /// 所以 `radius` 是 `None`。 + /// + /// 为什么要拆成 12 + 4 而不是直接写 16:官方 = 文本框内容内边距 **12**(`TextFieldImpl` 默认) + /// 加上 `SearchBarIconOffsetX = 4`。拆开才能让「文本从 12 起、图标从 16 起」都被消费方拿到; + /// 只写 16 的话文本内边距没有落点,消费方只能自己写 `12.0`——正是 G10 要消灭的字面量。 + /// 图标位置一律按 `pad + icon_offset` 求和,不要单独把 `pad` 当图标边用。 pub const SEARCH_BAR: Self = Self { width: 0.0, height: 0.0, @@ -118,10 +124,12 @@ impl ComponentMetrics { state_layer: 0.0, inner_dot: 0.0, inset: 0.0, - pad_start: Space::X4.dp(), - pad_end: Space::X4.dp(), + pad_start: Space::X3.dp(), + pad_end: Space::X3.dp(), icon: IconSize::Toolbar.px(), icon_offset: 4.0, + // 图标 ↔ 文本 8 是**团队口径 [估]**:`research/15` §20.4 只给了图标尺寸/位置与 48 命中框, + // 没有「图标 ↔ 文字」的 token(Compose 由 `TextFieldImpl` 内部排版决定)。 gap: Space::X2.dp(), }; diff --git a/crates/design/src/component_map/mod.rs b/crates/design/src/component_map/mod.rs index a9e63de..8a7629c 100644 --- a/crates/design/src/component_map/mod.rs +++ b/crates/design/src/component_map/mod.rs @@ -13,7 +13,8 @@ //! [`crate::shape::ComponentRadius`](落在 [`ComponentSpec::micro_radius`])。 //! //! 档位(形状/高程/字级/动效)之外,**控件内部的微几何**(20×20、描边 2、状态层 Ø40 …)在 -//! [`ComponentMetrics`] 里,入口是 [`Component::metrics`]——两者分工见 [`metrics`] 模块文档。 +//! [`ComponentMetrics`] 里,入口是 [`Component::metrics`]——两者分工见该项的文档 +//! (`component_map::metrics` 模块)。 //! //! **改这里的值 = 改全站观感**,必须同步 `DevDocs/SubAgentsTalk/design-令牌约定.md`; //! 页面线不许在页面里覆盖这些档位。 @@ -43,11 +44,14 @@ pub struct ComponentSpec { pub micro_radius: Option, /// 高程级别(决定 surface container 角色与是否画阴影)。 pub elevation: Elevation, - /// 主要文字角色。 + /// 主要文字角色。**无文字的组件(如 `Divider`)这里是占位值**,消费方不得据此排版。 pub text: TypeRole, /// 辅助文字角色。 pub support: Option, /// 状态过渡缓动。 + /// + /// 该字段不是 `Option`,所以**没有动效语义的组件(如 `Divider`)这里是占位值**—— + /// 读之前先确认组件本身有状态过渡,无过渡的组件不得据此排动效。 pub motion: Easing, /// 状态过渡时长。 pub duration: DurationToken, diff --git a/crates/design/src/component_map/table.rs b/crates/design/src/component_map/table.rs index 84bc75b..26f846e 100644 --- a/crates/design/src/component_map/table.rs +++ b/crates/design/src/component_map/table.rs @@ -356,6 +356,8 @@ pub(super) const SPECS: [ComponentSpec; 25] = [ // Divider:无高程、无圆角、**无文字**(`text` 是占位值,消费方不得据此排版); // 厚度是一像素线 `LayoutMetrics::divider_thickness`,颜色 `OutlineVariant` // (search view 头部分隔是 `Outline`,两个不同角色,§20.8)。 + // **`motion` / `duration` 也是占位值**(`ComponentSpec.motion` 不是 `Option`):Divider 没有 + // 官方状态过渡令牌,消费方**不得据此排动效**;下轮把该字段改成 `Option` 更彻底。 spec( Component::Divider, ShapeScale::None, diff --git a/crates/design/src/shape.rs b/crates/design/src/shape.rs index 49f2230..9126982 100644 --- a/crates/design/src/shape.rs +++ b/crates/design/src/shape.rs @@ -119,6 +119,10 @@ pub const MICRO_CONTROL_MAX_SIZE: f32 = 24.0; /// /// 用法:先 [`radius_layer_for`] 判层,微控件再 `radius_px(size)`。**返回 `None` 说明尺寸超界, /// 必须改查 [`ShapeScale`]**,不许把微半径硬套到容器上。 +/// +/// 公开的**唯一取值口是 [`ComponentRadius::radius_px`]**(带尺寸闸):本类型的 `dp()` 是 +/// `pub(crate)`,故意不对外开放——否则任何调用方都能无条件拿到 `2.0`,「只许 ≤24dp」就成了 +/// 自觉而不是约束(D32 的原意是拦住,不是提醒)。 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ComponentRadius { /// 2dp:官方 Checkbox 容器圆角。 @@ -127,10 +131,15 @@ pub enum ComponentRadius { impl ComponentRadius { /// 白名单里的全部微半径(D32 当前只有一项;加值必须先在 `design-令牌约定` 登记)。 + /// + /// 枚举得出的只是**变体**(`Micro` 这个名字),拿不到数值——取值必须走 [`Self::radius_px`]。 pub const ALL: [ComponentRadius; 1] = [ComponentRadius::Micro]; - /// 半径(dp)。 - pub const fn dp(self) -> f32 { + /// 半径(dp)。**`pub(crate)`:crate 内推导与自己用,不对外开放。** + /// + /// 对任何尺寸都返回同一个数,所以它天然绕开 D32 的尺寸闸;对外只留 + /// [`Self::radius_px`](`Option`,>24dp 返回 `None`)。 + pub(crate) const fn dp(self) -> f32 { match self { ComponentRadius::Micro => 2.0, } diff --git a/crates/design/tests/tokext_pins.rs b/crates/design/tests/tokext_pins.rs index c71fa11..3f3b32e 100644 --- a/crates/design/tests/tokext_pins.rs +++ b/crates/design/tests/tokext_pins.rs @@ -42,17 +42,19 @@ fn shape_scale_is_unchanged_and_never_contains_two() { #[test] fn component_radius_is_a_one_item_whitelist() { assert_eq!(ComponentRadius::ALL, [ComponentRadius::Micro]); + // 2dp 只能通过带尺寸闸的 `radius_px` 拿到(`dp()` 是 crate 内部口,外部看不见)。 assert_eq!( - ComponentRadius::Micro.dp(), - 2.0, + ComponentRadius::Micro.radius_px(20.0), + Some(2.0), "官方 Checkbox RadiusSize = 2.dp" ); // 尺寸闸:>24dp 一律拿不到微半径(D32 的「只允许 ≤24dp 微控件」)。 - assert_eq!(ComponentRadius::Micro.radius_px(20.0), Some(2.0)); assert_eq!(ComponentRadius::Micro.radius_px(24.0), Some(2.0)); assert_eq!(ComponentRadius::Micro.radius_px(25.0), None); assert_eq!(ComponentRadius::Micro.radius_px(240.0), None); assert_eq!(ComponentRadius::Micro.radius_px(0.0), None); + // 边长小于两倍半径时按边长一半折算(2dp 元素不能有 2dp 圆角)。 + assert_eq!(ComponentRadius::Micro.radius_px(2.0), Some(1.0)); } #[test] @@ -149,10 +151,16 @@ fn search_bar_metrics_are_pinned() { // 高 56 属骨架级:CM 里必须留 0,不许重复一份。 assert_eq!((cm.width, cm.height), (0.0, 0.0)); assert_eq!(cm.icon, IconSize::Toolbar.px(), "leading/trailing 图标 24"); + // A 级(§20.4):图标可视左缘 16 = 文本框内容内边距 12 + `SearchBarIconOffsetX = 4`。 + assert_eq!(cm.pad_start, Space::X3.dp(), "内容内边距 12"); assert_eq!(cm.icon_offset, 4.0, "SearchBarIconOffsetX = 4"); - // 可视左缘 16 = 文本框默认 12 + 偏移 4。 - assert_eq!(cm.pad_start, Space::X4.dp()); - assert_eq!(cm.pad_end, Space::X4.dp()); + assert_eq!(cm.pad_start + cm.icon_offset, 16.0, "图标可视左缘必须是 16"); + // 右侧同式:图标右缘也是 16。 + assert_eq!(cm.pad_end, Space::X3.dp()); + assert_eq!(cm.pad_end + cm.icon_offset, 16.0, "图标可视右缘必须是 16"); + // 守门:不许把 pad 本身写成 16(否则 pad + offset = 20,越过 rubric 的 ±2px)。 + assert_ne!(cm.pad_start, 16.0); + assert_ne!(cm.pad_end, 16.0); // 搜索条圆角 = 高/2 = 28,等于官方 SearchBarCornerRadius。 assert_eq!( ShapeScale::Full.radius_px(LayoutMetrics::DESKTOP.search_bar_height), @@ -171,6 +179,8 @@ fn navigation_drawer_metrics_are_pinned() { assert_eq!(cm.pad_start, Space::X4.dp(), "项内 start 16"); assert_eq!(cm.pad_end, Space::X6.dp(), "项内 end 24"); assert_eq!(cm.icon, IconSize::Toolbar.px()); + // 与 search bar 同一条公式:图标边 = pad + icon_offset(抽屉的 offset 是 0)。 + assert_eq!(cm.pad_start + cm.icon_offset, 16.0, "图标起点 = 项内 start"); assert_eq!(cm.gap, Space::X3.dp(), "图标 ↔ label 12"); // 指示器宽 = 容器宽 − 24;官方 360 − 24 = 336,本项目 240 − 24 = 216。 assert_eq!(cm.indicator_width(360.0), 336.0);