fix(design): M69 r1 —— SEARCH_BAR 内边距语义收口 + ComponentRadius 取值口上闸

P2-1(语义缝隙):`SEARCH_BAR` 的 pad_start / pad_end 原为 16,与
icon_offset = 4 按字面相加得 20,比官方图标可视边 16 多 4px(越过 rubric
±2px 容差);而被加数的 12 不在 CM 里,消费方只能自己写 12.0 —— 正是 G10
要消灭的字面量。改为 12(文本框内容内边距,Space::X3)+ 4,两侧同式:
图标可视边 = pad + icon_offset = 16。字段文档写明求和公式;测试钉死
pad + offset == 16 且 pad != 16(防有人把 16 写回 pad 再叠加)。
顺带给 SEARCH_BAR.gap 补 [估] 出处(官方无图标↔文字 token)。

P2-2(绕过路径):ComponentRadius::dp() 收成 pub(crate)。它不看尺寸、
无条件返回 2.0,公开等于把 D32 的尺寸闸交给自觉。对外唯一取值口是
radius_px(size) -> Option<f32>;ALL 只暴露变体名、不暴露数值。
tokext_pins 原用 dp(),改为紧邻的 radius_px(20.0) == Some(2.0),覆盖不丢。

顺手:修掉本轮新引入的 rustdoc warning(component_map 文档链到私有模块
metrics,改指 ComponentMetrics);Divider 行标注 motion/duration 为占位,
并在 ComponentSpec 的 text / motion 字段文档写明「无文字 / 无动效的组件
这里是占位值,消费方不得据此排版 / 排动效」。

门禁:test 165 全绿、fmt --check、clippy -D warnings、check --workspace
--all-targets、doc(warning 2 → 1,只剩既有的 typography→emphasized 一条)。
This commit is contained in:
AskaEth committed 2026-11-01 17:05:19 +08:00
1 parent 141e9b12c7
commit 16bdad1b98
5 files changed
+50 -17

No files matched your search

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