feat(design): M69 令牌补齐 —— 圆角分层 / 组件微度量 / 骨架值与一像素线

G13(D32 分层):新增 shape::ComponentRadius(白名单只有 Micro=2)、
MICRO_CONTROL_MAX_SIZE=24 与纯函数 radius_layer_for();微半径走
radius_px(size) -> Option<f32>,尺寸 >24dp 一律 None,靠签名而不是自觉拦。
ShapeScale 与 rank() 一个字未动,2 不进刻度。

G11:LayoutMetrics 加 search_bar_height=56 / nav_rail_width=88 /
nav_drawer_width=240 / divider_thickness=1;删除语义相反的
nav_expanded_width=88 / nav_collapsed_width=48(D33:展开 drawer 240 /
折叠 rail 88;值 88 的后继是新名字 nav_rail_width,48 无后继)。

G10:新增 component_map::ComponentMetrics(12 字段 + 4 个命名常量 +
indicator_width),入口 Component::metrics();框内几何与 LayoutMetrics 的
骨架值分工写进模块文档,骨架级尺寸在 CM 里留 0 不许重复。

G9:Component 末尾追加 Checkbox / Radio / Search / NavigationDrawer /
Divider(20 -> 25),同步 table.rs 槽位、ALL 与 name();ComponentSpec 新增
micro_radius(Some 时 shape 必须是占位 None,由构造器 + 测试钉死)。

G12:新增 spacing::stroke::HAIRLINE = 1.0(crate 根再导出为
unlucky_design::stroke),divider_thickness 直接引用它。

其余:lib.rs 的「OKLCh 近似」文案改回真实口径(真 HCT / ColorSpec2021);
component_map 单测拆到 tests.rs 以免 mod.rs 超 400 行;测试 145 -> 165。
This commit is contained in:
AskaEth committed 2026-11-01 16:53:30 +08:00
1 parent b89cc91b78
commit 141e9b12c7
9 files changed
+1066 -145

No files matched your search

+125
View File
@@ -2,6 +2,17 @@
//!
//! 形状是「唯一真源」的一部分:组件不许写裸半径(`border_radius(8)`),一律引用
//! [`ShapeScale`];需要动起来的(按下、选中、展开)用 [`ShapeMorph`] 的 from→to 数据。
//!
//! ## 圆角分两层(D32)
//!
//! 官方实现与官方刻度表并不总一致:Checkbox 的圆角是 **2dp**,但 `ShapeScale` 的刻度是
//! `{0,4,8,12,16,20,28,32,48,full}`——2 不在里面。D32 的判决是**分层**而不是放宽判据:
//!
//! - **容器级**(卡片、弹窗、抽屉、按钮、面板、输入框外壳)只许用 [`ShapeScale`] 的刻度值;
//! - **微控件级**(≤24dp 的 Checkbox 等)用 [`ComponentRadius`] 的白名单值。
//!
//! 一个元素该查哪一层由 [`radius_layer_for`] 判定(按尺寸,唯一入口);微控件层取值走
//! [`ComponentRadius::radius_px`],**尺寸超界时返回 `None`**——这不是文档约定,是签名保证。
use crate::motion::Spring;
@@ -87,6 +98,79 @@ impl ShapeScale {
}
}
/// 微控件(≤24dp)的尺寸上限(dp)。
///
/// 超过这条线的元素属**容器级**,圆角必须查 [`ShapeScale`] 的严格刻度;[`ComponentRadius`]
/// 只在这条线以内有效——由 [`ComponentRadius::radius_px`] 在运行期拦住(传大尺寸拿不到微半径)。
/// 24 这个门槛不是官方数字,是 D32 给「微控件」划的界(Checkbox 20、指示点、徽标都在以内)。
pub const MICRO_CONTROL_MAX_SIZE: f32 = 24.0;
/// 微控件圆角(D32 第二层):**来源官方组件实现,不是 [`ShapeScale`] 的成员**。
///
/// 为什么要有这一层:官方 Checkbox 的圆角是 **2dp**(`Checkbox.kt:738 RadiusSize = 2.dp`,
/// `CheckboxTokens.ContainerShape = RoundedCornerShape(2)`),而 M3 形状刻度只有
/// `{0,4,8,12,16,20,28,32,48,full}`。把 2 塞进刻度会废掉「无任意圆角」这条守门(D30 rubric K1),
/// 不收又只能在组件里抄 `2.0` 字面量。D32 的判决是**分层**,不是放宽:
///
/// | 层 | 管什么 | 取值 |
/// |---|---|---|
/// | [`ShapeScale`] | 容器级:卡片/弹窗/抽屉/按钮/面板/输入框外壳 | 严格刻度,不许自造 |
/// | `ComponentRadius` | 微控件级:Checkbox 等 ≤24dp 的元素 | 白名单,当前只有 [`ComponentRadius::Micro`] |
///
/// 用法:先 [`radius_layer_for`] 判层,微控件再 `radius_px(size)`。**返回 `None` 说明尺寸超界,
/// 必须改查 [`ShapeScale`]**,不许把微半径硬套到容器上。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ComponentRadius {
/// 2dp:官方 Checkbox 容器圆角。
Micro,
}
impl ComponentRadius {
/// 白名单里的全部微半径(D32 当前只有一项;加值必须先在 `design-令牌约定` 登记)。
pub const ALL: [ComponentRadius; 1] = [ComponentRadius::Micro];
/// 半径(dp)。
pub const fn dp(self) -> f32 {
match self {
ComponentRadius::Micro => 2.0,
}
}
/// 微控件的实际半径:尺寸超过 [`MICRO_CONTROL_MAX_SIZE`] 时返回 **`None`**。
///
/// 这是 D32 里「能编译期拦住就别靠自觉」的落点——文档约定拦不住,签名拦得住:
/// 拿到 `None` 的调用点没法「顺手用一下 2dp」,只能改查 [`ShapeScale`]。
/// 另外半径不超过边长一半(2dp 半径放到更小的元素上会溢成圆)。
pub fn radius_px(self, size_px: f32) -> Option<f32> {
if size_px > 0.0 && size_px <= MICRO_CONTROL_MAX_SIZE {
Some(self.dp().min(size_px / 2.0))
} else {
None
}
}
}
/// 一个元素的圆角该查哪一层([`radius_layer_for`] 的返回)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RadiusLayer {
/// 容器级:查 [`ShapeScale`] 的严格刻度。
Scale,
/// 微控件级(≤24dp):查 [`ComponentRadius`] 的白名单。
Component,
}
/// 纯函数:按元素尺寸判定圆角该查哪一层。
///
/// 「某尺寸的元素该查哪一个」是 D32 里唯一需要判断的地方,所以只留这一个入口——
/// 组件层不许自己写 `if size <= 24.0`,那等于把阈值抄了第二份。
pub fn radius_layer_for(size_px: f32) -> RadiusLayer {
if size_px > 0.0 && size_px <= MICRO_CONTROL_MAX_SIZE {
RadiusLayer::Component
} else {
RadiusLayer::Scale
}
}
/// 一次形状形变(Expressive 的「按下胀开」「展开成药丸」等)。
///
/// 数据形态是纯的:起止档位 + 弹簧参数。适配层负责跑时钟,`progress` 由弹簧/缓动给出。
@@ -165,4 +249,45 @@ mod tests {
assert_eq!(morph.radius_px(200.0, -2.0), 8.0);
assert_eq!(morph.radius_px(200.0, 3.0), 20.0);
}
#[test]
fn component_radius_is_whitelisted_and_refuses_containers() {
// D32:微半径白名单当前只有 2dp,且永远不是 ShapeScale 的成员。
assert_eq!(ComponentRadius::ALL.len(), 1);
assert_eq!(ComponentRadius::Micro.dp(), 2.0);
for scale in [
ShapeScale::None,
ShapeScale::ExtraSmall,
ShapeScale::Small,
ShapeScale::Medium,
ShapeScale::Large,
ShapeScale::LargeIncreased,
ShapeScale::ExtraLarge,
ShapeScale::ExtraLargeIncreased,
ShapeScale::ExtraExtraLarge,
] {
assert_ne!(scale.dp(), 2.0, "{scale:?} 不该等于微半径(2 不在刻度里)");
}
// 20×20 的 checkbox 拿 2dp;超 24 的元素必须拿不到。
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.5), None);
assert_eq!(ComponentRadius::Micro.radius_px(200.0), None);
assert_eq!(ComponentRadius::Micro.radius_px(0.0), None);
// 半径不超过边长一半。
assert_eq!(ComponentRadius::Micro.radius_px(2.0), Some(1.0));
}
#[test]
fn radius_layer_follows_the_size_threshold() {
assert_eq!(radius_layer_for(20.0), RadiusLayer::Component);
assert_eq!(
radius_layer_for(MICRO_CONTROL_MAX_SIZE),
RadiusLayer::Component
);
assert_eq!(radius_layer_for(24.5), RadiusLayer::Scale);
assert_eq!(radius_layer_for(320.0), RadiusLayer::Scale);
assert_eq!(radius_layer_for(0.0), RadiusLayer::Scale);
}
}