Files
Unlucky-Launcher/crates/design/src/component_map/metrics.rs
T
AskaEth 16bdad1b98 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 一条)。
2026-11-01 17:05:19 +08:00

180 lines
8.9 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.
//! 组件级微度量(`ComponentMetrics`):**单个控件内部的几何**,不是窗口骨架。
//!
//! 为什么要有这个结构(M69 / G10):Checkbox 的 20×20、圆角 2、描边 2、状态层 Ø40 这类值,
//! `ShapeScale`(刻度)装不了、`LayoutMetrics`(骨架)也不该装——M66 对账时它们只能写在组件
//! 实现里,而 D30 禁止组件层自定义数值。这里给它们一个**唯一落点**。
//!
//! ## 与 `LayoutMetrics` 的分工(两把尺子,量不同的东西)
//!
//! | | [`crate::spacing::LayoutMetrics`] | `ComponentMetrics`(本模块) |
//! |---|---|---|
//! | 管什么 | 窗口骨架与布局节奏:标题栏/页头/行高、dock 宽度、命中区 | 单个控件方框内的几何:外框、微圆角、描边/环线宽、状态层、内点、图标偏移 |
//! | 随什么变 | 窗口尺寸、列宽、用户折叠导航 | 只随组件自身规格变 |
//! | 例子 | `nav_drawer_width = 240`、`search_bar_height = 56` | checkbox 20×20、描边 2、状态层 Ø40 |
//! | 判据 | 决定「一块面板/一行占多宽多高」 | 决定「画进这个方框里的那几个像素」 |
//!
//! 冲突时的规则是**同一个量只许有一份**:搜索条高 56 属骨架级,所以
//! [`ComponentMetrics::SEARCH_BAR`] 的 `height` 留 `0.0` 并在文档里指回 LM;drawer 宽同理。
//! 测试 `component_metrics_never_duplicate_layout_metrics` 把这条钉死。
//!
//! 数值来源:`androidx.compose.material3:material3-android:1.4.0` 源码(A 级,M66 逐行核对,
//! 见 `research/15` §18~§23)。逐字段的出处写在下面各组件的关联常量上;**团队口径**(官方没有
//! token 的)单独注明。
use super::Component;
use crate::icons::IconSize;
use crate::shape::ComponentRadius;
use crate::spacing::Space;
/// 单个控件的内部几何。
///
/// `0.0` 表示**该控件没有这一项**(不是「默认值 0」)——拿 `0.0` 反推数值一定错。
/// 骨架级尺寸(条高、面板宽)不在这里,去 [`crate::spacing::LayoutMetrics`] 拿。
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ComponentMetrics {
/// 可见外框宽(dp;`0.0` = 由内容或骨架决定)。
pub width: f32,
/// 可见外框高(dp;`0.0` = 由内容或骨架决定)。
pub height: f32,
/// 微控件圆角(D32 第二层)。`None` = 该控件的圆角走 `ShapeScale`(见 `ComponentSpec::shape`)。
pub radius: Option<ComponentRadius>,
/// 描边 / 环线宽度(dp)。**禁用态也不许变细**——官方 disabled 只改透明度。
pub stroke: f32,
/// 状态层(hover / focus / pressed)圆的直径(dp;`0.0` = 不画状态层)。
pub state_layer: f32,
/// 内点直径(dp):radio 选中时的实心点,其余控件 `0.0`。
pub inner_dot: f32,
/// 控件相对容器的外距(dp):drawer 项 = 12,决定选中指示器左右各内缩 12。
pub inset: f32,
/// 内容起始侧内边距(dp)。**图标不算在内**:图标可视边 = `pad_start + icon_offset`。
pub pad_start: f32,
/// 内容结束侧内边距(dp)。同起始侧,图标可视边 = `pad_end + icon_offset`。
pub pad_end: f32,
/// 图标边长(dp;`0.0` = 无图标)。
pub icon: f32,
/// 图标相对内边距的**额外**偏移(dp)。图标可视边 = 内边距 + 本值(search bar:12 + 4 = 16),
/// **不要**把内边距本身当图标边用,也不要再手抄那个 12 / 16。
pub icon_offset: f32,
/// 图标与文字(或相邻元素)的间距(dp)。
pub gap: f32,
}
impl ComponentMetrics {
/// Checkbox(`research/15` §18 / §23)。
///
/// A 级出处:`Checkbox.kt:737 CheckboxSize = 20.dp`、`:738 RadiusSize = 2.dp`(圆角走 D32 的
/// [`ComponentRadius::Micro`])、`CheckboxTokens.UnselectedOutlineWidth = 2`、
/// `StateLayerSize = 40` + `StateLayerShape = CornerFull`。
/// 注意 `CheckboxTokens.ContainerWidth/Height = 18` 是**死值**(全仓 0 引用),Compose 实际画
/// 20(§26.2 矛盾 ①)——判据取 20。
/// 勾选控件 ↔ 标签间距 8 是**团队口径**:官方没有「框 + 标签」组合的 token(§18.9,估)。
pub const CHECKBOX: Self = Self {
width: 20.0,
height: 20.0,
radius: Some(ComponentRadius::Micro),
stroke: 2.0,
state_layer: 40.0,
inner_dot: 0.0,
inset: 0.0,
pad_start: 0.0,
pad_end: 0.0,
icon: 0.0,
icon_offset: 0.0,
gap: Space::X2.dp(),
};
/// Radio(`research/15` §19 / §23)。
///
/// A 级出处:`RadioButtonTokens.IconSize = 20`、`RadioStrokeWidth = 2.dp`;内点名义
/// `RadioButtonDotSize = 12`,但绘制半径 = 12/2 − stroke/2 = 5 → **实际内点直径 10**
/// (§19.2:抄 12 会明显偏大)。圆角走 `ShapeScale::Full`(真圆),不是微半径。
/// 标签间距 8 同 Checkbox(团队口径)。
pub const RADIO: Self = Self {
width: 20.0,
height: 20.0,
radius: None,
stroke: 2.0,
state_layer: 40.0,
inner_dot: 10.0,
inset: 0.0,
pad_start: 0.0,
pad_end: 0.0,
icon: 0.0,
icon_offset: 0.0,
gap: Space::X2.dp(),
};
/// Search bar(`research/15` §20A / §23)。
///
/// 高度与宽度**不在这里**:条高 56 是骨架级(`LayoutMetrics::search_bar_height`),
/// 最小宽 360 同样属骨架级(尚未进 LM,M69 报告里登记为待办)。这里只装画进 bar 里的几何:
/// 图标 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,
radius: None,
stroke: 0.0,
state_layer: 0.0,
inner_dot: 0.0,
inset: 0.0,
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(),
};
/// Navigation drawer(`research/15` §21 / §23)。
///
/// 宽度与项高**不在这里**:drawer 240 / rail 88 在 `LayoutMetrics`,项高 44 =
/// `LayoutMetrics::nav_item_height`。这里装项内几何(§21.6,A 级:
/// `NavigationDrawerItemDefaults.ItemPadding = 12`、项内 start 16 / end 24、
/// `IconSize = 24`、图标 ↔ label 12)。
/// 容器形状 `CornerLargeEnd`(只有靠内容那一侧两角 16)由 `ComponentSpec::shape` 的
/// `Large` + 组件实现按侧位决定哪两个角,度量层不表达「部分角」。
pub const NAVIGATION_DRAWER: Self = Self {
width: 0.0,
height: 0.0,
radius: None,
stroke: 0.0,
state_layer: 0.0,
inner_dot: 0.0,
inset: 12.0,
pad_start: Space::X4.dp(),
pad_end: Space::X6.dp(),
icon: IconSize::Toolbar.px(),
icon_offset: 0.0,
gap: Space::X3.dp(),
};
/// drawer 选中指示器宽度:容器宽 − 2 × [`ComponentMetrics::inset`]。
///
/// 240 − 24 = **216**;官方 360 − 24 = 336 是同一个公式(§21.5)——写死 336 就是忘了推导。
pub fn indicator_width(&self, container_width: f32) -> f32 {
(container_width - self.inset * 2.0).max(0.0)
}
}
/// 组件 → 组件级微度量。`None` = 该组件没有控件内部几何(只有骨架值,去 LM 拿)。
///
/// Divider 是 `None` 的显式例子:它的唯一几何量是厚度,而厚度是**一像素线**
/// (`LayoutMetrics::divider_thickness` = `stroke::HAIRLINE`),不是「控件内部」的东西。
pub(super) const fn metrics_of(component: Component) -> Option<ComponentMetrics> {
match component {
Component::Checkbox => Some(ComponentMetrics::CHECKBOX),
Component::Radio => Some(ComponentMetrics::RADIO),
Component::Search => Some(ComponentMetrics::SEARCH_BAR),
Component::NavigationDrawer => Some(ComponentMetrics::NAVIGATION_DRAWER),
_ => None,
}
}