fix(app-iced): 禁用态与遮罩改从品牌板合成(M229 复审 F1)+ 选择类图注回填(F2)
F1(核心):三处 `Scheme::baseline(ui.flavor())` 自己重算了一份方案,绕开 `Ui` 里注入的品牌板,
于是 Ink 板上的禁用控件与对话框遮罩仍按 tone 派生的天蓝中性合成(复审量到滑块轨道 #58646C、
填充框 #222E36、遮罩 #060E13,全是蓝系)。修法:`Ui::scheme()` 公开已注入的那份,
`interaction/disabled.rs`(两族合成 + 内容色)、`feedback/dialog.rs`(scrim)、
`feedback/action.rs`(禁用动作文字)改为从它取。
修完 Ink 下实测:禁用容器(12%)#292F28、按钮容器(10%)#252B24、遮罩 #0A0E0A(绿系中性,
R = B);Silver 档遮罩 #A9A9AA。旧三个蓝系值在 7 张深色图里出现次数均为 0。
回归门:`disabled_colors_follow_the_brand_board_not_the_tone_baseline`(按板合成 + 与 baseline
不等 + 深色档 G ≥ B)、`scrim_is_scrimalpha_precomposited_onto_the_canvas` 增加「不许退回 baseline」。
F2:`selection/{matrix,mod,checkbox,radio,geometry}.rs` 里「20×20 / rx2 / 外圈 20」回填成实际渲染的
「18×18 / rx4 / 外圈 18」(勾形比例不变 ⇒ 10.8dp),并按塔裁决把搜索药丸容器从
`SurfaceContainerHigh` 抬到 **`SurfaceContainerHighest`**(贴规格卡,与 filled 字段同底);
`search.rs` / `probe/search.rs` / gallery `inputs` 图注同步。展开的搜索面板与菜单是浮层,
仍留 High 档(已在文档里写明不是漏改)。
`interaction/disabled.rs` 加测试后 405 行 ⇒ 单测纯搬移到 `interaction/disabled/tests.rs`。
This commit is contained in:
1 parent
f9877a89d8
commit
4ce24d6fbd
18 files changed
+361
-213
No files matched your search
@@ -21,7 +21,7 @@ use iced::widget::{button, text};
|
||||
use iced::{Background, Border, Element, Length, Padding, Shadow};
|
||||
|
||||
use unlucky_design::color::state::DISABLED_CONTENT_ALPHA;
|
||||
use unlucky_design::color::{Role, Scheme};
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::component_map::Component;
|
||||
use unlucky_design::icons::IconKind;
|
||||
use unlucky_design::spacing::Space;
|
||||
@@ -43,7 +43,8 @@ fn content_color(ui: &Ui, role: Role, container_role: Role, disabled: bool) -> i
|
||||
if !disabled {
|
||||
return ui.color(role);
|
||||
}
|
||||
let scheme = Scheme::baseline(ui.flavor());
|
||||
// 取 `Ui` 的当前方案(品牌板 / 取色方案),不重算 baseline(复审 F1)。
|
||||
let scheme = ui.scheme();
|
||||
to_iced(
|
||||
scheme
|
||||
.get(Role::OnSurface)
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
use iced::widget::{column, container, row, text, Space};
|
||||
use iced::{Alignment, Background, Border, Element, Length, Padding};
|
||||
|
||||
use unlucky_design::color::{Role, Scheme};
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::elevation::MaterialLayer;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
|
||||
@@ -128,9 +128,13 @@ impl<'a, M> Dialog<'a, M> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 遮罩色:`黑 @ SCRIM_ALPHA` 叠在 `MaterialLayer::Canvas` 上之后的**实色**(D27)。
|
||||
/// 遮罩色:`scrim @ SCRIM_ALPHA` 叠在 `MaterialLayer::Canvas` 上之后的**实色**(D27)。
|
||||
///
|
||||
/// 叠的底是 [`Ui::scheme`](品牌色板 / 取色方案):M229 之前这里按 `Scheme::baseline(flavor)`
|
||||
/// 重算,Ink 板上量到的是旧蓝中性底压出来的**偏蓝**遮罩;按品牌板合成才是中性绿黑
|
||||
/// (`scrim` 是 M3 定值纯黑,两板同值,差的是它下面的画布色)。
|
||||
pub fn scrim(ui: &Ui) -> iced::Color {
|
||||
let scheme = Scheme::baseline(ui.flavor());
|
||||
let scheme = ui.scheme();
|
||||
let canvas = scheme.get(MaterialLayer::Canvas.surface_role());
|
||||
to_iced(scheme.scrim().over(canvas))
|
||||
}
|
||||
|
||||
@@ -104,13 +104,26 @@ fn dialog_padding_and_action_gaps_are_the_tokens() {
|
||||
fn scrim_is_scrimalpha_precomposited_onto_the_canvas() {
|
||||
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
|
||||
let ui = ui(flavor);
|
||||
let scheme = Scheme::baseline(flavor);
|
||||
// 期望值按**品牌色板**(= `Ui` 注入的那一份):M229 复审 F1 之前这里用的是
|
||||
// `Scheme::baseline`,Ink 板上会得到偏蓝的遮罩(数值见 `DevDocs/artifacts/m229/README.md` §6)。
|
||||
let scheme = unlucky_design::color::BrandBoard::for_flavor(flavor).scheme();
|
||||
let canvas = scheme.get(MaterialLayer::Canvas.surface_role());
|
||||
let expected = to_iced(scheme.scrim().over(canvas));
|
||||
assert_eq!(
|
||||
scrim(&ui),
|
||||
expected,
|
||||
"{flavor:?}:scrim 必须逐位等于「黑@0.32 叠 canvas」"
|
||||
"{flavor:?}:scrim 必须逐位等于「黑@0.32 叠 canvas(品牌板)」"
|
||||
);
|
||||
// F1 的回归门:不许回到 tone 派生的 baseline 上算。
|
||||
let baseline = Scheme::baseline(flavor);
|
||||
assert_ne!(
|
||||
expected,
|
||||
to_iced(
|
||||
baseline
|
||||
.scrim()
|
||||
.over(baseline.get(MaterialLayer::Canvas.surface_role()))
|
||||
),
|
||||
"{flavor:?}:遮罩退回了 baseline(Ink 下会偏蓝)"
|
||||
);
|
||||
assert_eq!(
|
||||
scrim(&ui).a,
|
||||
|
||||
@@ -77,7 +77,7 @@ pub(super) fn section<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
|
||||
heading(
|
||||
ui,
|
||||
"buttons · filled / tonal / elevated / outlined / text + icon",
|
||||
"12b §1/§2:高 40 · 圆角 full(20) · 内边距 24(带图标 16/24)· 图标 18 间距 8 · \
|
||||
"规格卡 + 12b §1/§2:S 档高 40 · 圆角 full(20) · 内边距 20(带图标 16/20;M229 起按 ControlSize::pad_x)· 图标 18 间距 8 · \
|
||||
hover 8% / focus 10% / pressed 10% / disabled 容器 10% 内容 38%"
|
||||
),
|
||||
rows,
|
||||
|
||||
@@ -222,7 +222,7 @@ fn search_part<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
|
||||
column![
|
||||
note(
|
||||
ui,
|
||||
"bar:高 56 · 圆角 full(28) · SurfaceContainerHigh · 图标可视边 16 = pad 12 + offset 4(M69 不变量)· \
|
||||
"bar:高 56 · 圆角 full(28) · SurfaceContainerHighest(M229 r2 抬档:与 filled 字段同底)· 图标可视边 16 = pad 12 + offset 4(M69 不变量)· \
|
||||
聚焦 1px Secondary;view:docked,头部 56 + 1px Outline 分隔 + 结果区最小高 240"
|
||||
),
|
||||
row![bars, view].spacing(ui.space(Space::X6)),
|
||||
|
||||
@@ -10,7 +10,7 @@ fn search_bar_and_docked_panel_geometry() {
|
||||
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
|
||||
let shot = shot(&dir, "search", flavor);
|
||||
let ui = ui(flavor);
|
||||
let bar_color = rgb(ui.color(Role::SurfaceContainerHigh));
|
||||
let bar_color = rgb(ui.color(Role::SurfaceContainerHighest));
|
||||
let ink = rgb(ui.color(Role::OnSurface));
|
||||
|
||||
let bars = shot.bbox_of(bar_color, 2000);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//! |---|---|---|---|
|
||||
//! | 20.1 | 高 + 圆角 | 高 **56**、圆角 **full = 28**(药丸,不是圆角矩形) | `LM.search_bar_height` + `Component::Search.spec().shape` |
|
||||
//! | 20.2 | 宽 | min **360** / max **720**(G20:iced 无 min-width,只施加 max) | [`SEARCH_MAX_WIDTH`](super::geometry::SEARCH_MAX_WIDTH) |
|
||||
//! | 20.3 | 容器色 / 高程 | `SurfaceContainerHigh` + **L0**(token 写 L3,默认值是 L0,按观感取 L0) | `Role::SurfaceContainerHigh` |
|
||||
//! | 20.3 | 容器色 / 高程 | **`SurfaceContainerHighest`** + **L0**(token 写 L3,默认值是 L0,按观感取 L0) | `Role::SurfaceContainerHighest` |
|
||||
//! | 20.4 | 图标位 | leading 24/`OnSurface`、trailing 24/`OnSurfaceVariant`;**可视边 16 = pad 12 + offset 4** | `ComponentMetrics::SEARCH_BAR` |
|
||||
//! | 20.5 | 文本 / 占位 / 聚焦 | 输入 `BodyLarge`+`OnSurface`;占位 `OnSurfaceVariant`;聚焦 `Secondary` 1px 指示 | `TypeRole` / `Role::Secondary` |
|
||||
//! | 20.6 | docked 面板 | 头部 **56**、形状 `CornerExtraLarge` = **28** | `ShapeScale::ExtraLarge` |
|
||||
@@ -179,7 +179,7 @@ fn bar_content<'a, M: Clone + 'a>(
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 收起态搜索条(56 高、药丸、`SurfaceContainerHigh`、无可见阴影)。
|
||||
/// 收起态搜索条(56 高、药丸、`SurfaceContainerHighest`、无可见阴影)。
|
||||
pub fn search_bar<'a, M: Clone + 'a>(
|
||||
ui: &'a Ui,
|
||||
bar: SearchBar<'a>,
|
||||
@@ -192,7 +192,9 @@ pub fn search_bar<'a, M: Clone + 'a>(
|
||||
let background = if state.is_disabled() {
|
||||
interaction::disabled_colors(ui, Role::Surface).0
|
||||
} else {
|
||||
let role = Role::SurfaceContainerHigh;
|
||||
// 规格卡里搜索药丸与 filled 字段是**同一个底**(`SurfaceContainerHighest`):
|
||||
// M229 r1 用的是 `High`,比 filled 字段低一档,两块并排时能看出色差(复审 F2 裁决抬档)。
|
||||
let role = Role::SurfaceContainerHighest;
|
||||
interaction::overlay_color(ui, state, Role::OnSurface, role).unwrap_or(ui.color(role))
|
||||
};
|
||||
|
||||
@@ -265,6 +267,8 @@ pub fn search_view<'a, M: Clone + 'a>(
|
||||
view: SearchView<'a>,
|
||||
on_input: impl Fn(String) -> M + 'a,
|
||||
) -> Element<'a, M> {
|
||||
// 展开面板**不跟着药丸抬档**:它是浮层(菜单语言,`SurfaceContainerHigh`),
|
||||
// 规格卡只画了收起的药丸。两块不同档是层级差,不是漏改(复审 F2 只裁药丸)。
|
||||
let panel = ui.color(Role::SurfaceContainerHigh);
|
||||
let header_height = ui.metrics().search_bar_height;
|
||||
let body_style = ui.style(TypeRole::BodyMedium);
|
||||
|
||||
@@ -47,8 +47,9 @@ use crate::theme::{to_iced, Ui};
|
||||
/// 之前这里写死 `OnSurfaceVariant` 且只有一个「底」,导致 FilledTonal 与 IconButton 两族偏浅
|
||||
/// 15/255(视觉 r3 用像素量的)。
|
||||
pub fn disabled_content_over_role(ui: &Ui, role: Role, on_container: bool) -> Color {
|
||||
let scheme = Scheme::baseline(ui.flavor());
|
||||
disabled_content_with_alpha(&scheme, role, on_container, DISABLED_SURFACE_ALPHA)
|
||||
// **取 `Ui` 里已注入的方案**,不重算 baseline:M229 之后产品方案是品牌色板
|
||||
// (Ink / Silver),照 baseline 重算会画出旧的天蓝中性色(复审 F1)。
|
||||
disabled_content_with_alpha(ui.scheme(), role, on_container, DISABLED_SURFACE_ALPHA)
|
||||
}
|
||||
|
||||
/// 禁用态内容色——**按钮家族**(`on_container`:是否叠在本族 10% 容器上)。
|
||||
@@ -56,8 +57,7 @@ pub fn disabled_content_over_role(ui: &Ui, role: Role, on_container: bool) -> Co
|
||||
/// 与 [`disabled_content_over_role`] 的差别只有「底」的 α:按钮 10%、面状控件 12%。
|
||||
/// 两个入口分开是 M86 的结论:合成一个数会让一族永远差 1/255,肉眼看不见、只有 probe 会红。
|
||||
pub fn disabled_button_content(ui: &Ui, role: Role, on_container: bool) -> Color {
|
||||
let scheme = Scheme::baseline(ui.flavor());
|
||||
disabled_content_with_alpha(&scheme, role, on_container, DISABLED_CONTAINER_ALPHA)
|
||||
disabled_content_with_alpha(ui.scheme(), role, on_container, DISABLED_CONTAINER_ALPHA)
|
||||
}
|
||||
|
||||
/// 两个族共用的内容色求值(「底」的 α 由调用方给)。
|
||||
@@ -94,8 +94,8 @@ fn disabled_content_with_alpha(
|
||||
///
|
||||
/// **按钮家族用 [`disabled_button_colors`]**(0.10,`*ButtonTokens.DisabledContainerOpacity = 0.1f`)。
|
||||
///
|
||||
/// `ui` 不暴露 [`Scheme`],这里按同一个 flavor 求一次基线方案——调的是
|
||||
/// `unlucky_design` 的**同一个**函数与同一套角色,不是第二份令牌。
|
||||
/// 合成的输入是 [`Ui::scheme`](品牌色板 / 取色方案),**不是** `Scheme::baseline(flavor)`:
|
||||
/// 后者是 tone 派生的旧口径,在 Ink 板上会让禁用容器偏蓝(复审 F1 的根因)。
|
||||
///
|
||||
/// 顺序必须「先容器后内容」:Compose 也是这么画的,先叠容器再叠 label,
|
||||
/// 所以内容叠的角色是容器实色而不是底下的 surface。
|
||||
@@ -116,7 +116,7 @@ pub fn disabled_button_colors(ui: &Ui, surface: Role) -> (Color, Color) {
|
||||
|
||||
/// 两个族共用的求值:容器 = `OnSurface` @ `container_alpha` 叠在 `surface` 上,内容 = 38% 叠在容器上。
|
||||
fn disabled_colors_with_alpha(ui: &Ui, surface: Role, container_alpha: f32) -> (Color, Color) {
|
||||
let scheme = Scheme::baseline(ui.flavor());
|
||||
let scheme = ui.scheme();
|
||||
let base = scheme.get(surface);
|
||||
let container = scheme
|
||||
.get(Role::OnSurface)
|
||||
@@ -133,173 +133,4 @@ fn disabled_colors_with_alpha(ui: &Ui, surface: Role, container_alpha: f32) -> (
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_design::typography::Platform;
|
||||
|
||||
use crate::theme::Families;
|
||||
|
||||
const FAMILIES: Families = Families {
|
||||
text: "Segoe UI",
|
||||
display: "Segoe UI",
|
||||
mono: "Consolas",
|
||||
};
|
||||
|
||||
fn ui(flavor: SchemeFlavor) -> Ui {
|
||||
Ui::with_platform(flavor, Platform::Windows, FAMILIES)
|
||||
}
|
||||
|
||||
/// 两族的 α 必须都钉住(只钉一条,另一族被合并时会静默错 1/255)。
|
||||
#[test]
|
||||
fn both_disabled_alpha_classes_are_pinned_to_the_tokens() {
|
||||
// 绑定成局部变量再过断言:直接比两个常量会被 clippy 判成「常量断言」。
|
||||
let button_alpha = DISABLED_CONTAINER_ALPHA;
|
||||
let surface_alpha = DISABLED_SURFACE_ALPHA;
|
||||
let content_alpha = DISABLED_CONTENT_ALPHA;
|
||||
// 按钮容器:官方 *ButtonTokens.DisabledContainerOpacity = 0.1f
|
||||
assert!(
|
||||
(button_alpha - 0.10).abs() < f32::EPSILON,
|
||||
"按钮禁用容器 α 必须是 0.10(官方 DisabledContainerOpacity);\
|
||||
0.12 是面状控件那一类,别混用"
|
||||
);
|
||||
// 面状控件:chip 描边 / slider 未激活轨 / switch 轨道 = 0.12f
|
||||
assert!(
|
||||
(surface_alpha - 0.12).abs() < f32::EPSILON,
|
||||
"面状控件禁用 α 必须是 0.12(官方 FlatDisabledOutlineOpacity / \
|
||||
DisabledInactiveTrackOpacity / DisabledTrackOpacity)"
|
||||
);
|
||||
assert!((content_alpha - 0.38).abs() < f32::EPSILON);
|
||||
assert!(
|
||||
surface_alpha > button_alpha,
|
||||
"两族必须可区分:面状控件比按钮容器更实({surface_alpha} > {button_alpha})"
|
||||
);
|
||||
}
|
||||
|
||||
/// **面状控件族**的那一对颜色 = 0.12 预混合(chip / switch / slider / TextField 描边)。
|
||||
///
|
||||
/// ⚠️ 期望值**写死 0.12 / 0.38**,不从 `DISABLED_SURFACE_ALPHA` 取:
|
||||
/// 从被测常量求期望是**自指断言**——常量一改,expected 跟着改,永远相等(复审 r1 P2-1 实测:
|
||||
/// 把 `DISABLED_SURFACE_ALPHA` 改成 0.10,旧版这条照样绿)。现在常量改坏必红。
|
||||
#[test]
|
||||
fn surface_family_disabled_colors_are_the_12_percent_class() {
|
||||
const EXPECTED_SURFACE_ALPHA: f32 = 0.12;
|
||||
const EXPECTED_CONTENT_ALPHA: f32 = 0.38;
|
||||
let actual_alpha = DISABLED_SURFACE_ALPHA;
|
||||
assert_eq!(
|
||||
actual_alpha, EXPECTED_SURFACE_ALPHA,
|
||||
"面状控件族 α 必须是官方 0.12f;改常量会让下面每一条合成色断言同时红"
|
||||
);
|
||||
|
||||
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
|
||||
let ui = ui(flavor);
|
||||
let scheme = Scheme::baseline(flavor);
|
||||
let base = scheme.get(Role::Surface);
|
||||
let expected_container = scheme
|
||||
.get(Role::OnSurface)
|
||||
.with_alpha(EXPECTED_SURFACE_ALPHA)
|
||||
.over(base);
|
||||
|
||||
let (surface, surface_content) = disabled_colors(&ui, Role::Surface);
|
||||
assert_eq!(
|
||||
surface,
|
||||
to_iced(expected_container),
|
||||
"{flavor:?}:面状控件容器 = on-surface@12% 叠表面(期望值硬编码,不取自常量)"
|
||||
);
|
||||
assert_eq!(
|
||||
surface_content,
|
||||
to_iced(
|
||||
scheme
|
||||
.get(Role::OnSurfaceVariant)
|
||||
.with_alpha(EXPECTED_CONTENT_ALPHA)
|
||||
.over(expected_container)
|
||||
),
|
||||
"{flavor:?}:面状控件内容仍是 38%,只是叠的底不同"
|
||||
);
|
||||
assert_ne!(surface, ui.color(Role::Surface), "12% 叠上去必须能看出来");
|
||||
assert_eq!(surface.a, 1.0, "禁用容器也是实色(CPU/GPU 一致)");
|
||||
assert_eq!(surface_content.a, 1.0, "禁用内容也是实色");
|
||||
// 内容叠在容器上 vs 叠在表面上,是两回事(probe 量的是前者)
|
||||
assert_ne!(
|
||||
surface_content,
|
||||
to_iced(
|
||||
scheme
|
||||
.get(Role::OnSurfaceVariant)
|
||||
.with_alpha(EXPECTED_CONTENT_ALPHA)
|
||||
.over(base)
|
||||
),
|
||||
"{flavor:?}:内容必须叠在容器上"
|
||||
);
|
||||
// 跨族对照:面状族的合成色必须与按钮族不同(两族一旦合流,这条红)
|
||||
let (button, _) = disabled_button_colors(&ui, Role::Surface);
|
||||
assert_ne!(
|
||||
surface, button,
|
||||
"{flavor:?}:面状族 12% 与按钮族 10% 必须落在不同像素值上"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// **按钮家族**的那一对颜色 = 0.10 预混合,且与面状控件族**必须落在不同像素值上**。
|
||||
///
|
||||
/// 期望值同样写死(0.10 / 0.38),理由见上一条:自指断言抓不住常量被改坏。
|
||||
#[test]
|
||||
fn button_family_disabled_colors_are_the_10_percent_class() {
|
||||
const EXPECTED_BUTTON_ALPHA: f32 = 0.10;
|
||||
const EXPECTED_CONTENT_ALPHA: f32 = 0.38;
|
||||
let actual_alpha = DISABLED_CONTAINER_ALPHA;
|
||||
assert_eq!(
|
||||
actual_alpha, EXPECTED_BUTTON_ALPHA,
|
||||
"按钮族 α 必须是官方 0.1f(*ButtonTokens.DisabledContainerOpacity)"
|
||||
);
|
||||
|
||||
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
|
||||
let ui = ui(flavor);
|
||||
let scheme = Scheme::baseline(flavor);
|
||||
let base = scheme.get(Role::Surface);
|
||||
let expected_container = scheme
|
||||
.get(Role::OnSurface)
|
||||
.with_alpha(EXPECTED_BUTTON_ALPHA)
|
||||
.over(base);
|
||||
|
||||
let (button, button_content) = disabled_button_colors(&ui, Role::Surface);
|
||||
assert_eq!(
|
||||
button,
|
||||
to_iced(expected_container),
|
||||
"{flavor:?}:按钮容器 = on-surface@10% 叠表面(官方 DisabledContainerOpacity = 0.1f)"
|
||||
);
|
||||
assert_eq!(
|
||||
button_content,
|
||||
to_iced(
|
||||
scheme
|
||||
.get(Role::OnSurfaceVariant)
|
||||
.with_alpha(EXPECTED_CONTENT_ALPHA)
|
||||
.over(expected_container)
|
||||
),
|
||||
);
|
||||
|
||||
let (surface, _) = disabled_colors(&ui, Role::Surface);
|
||||
assert_ne!(
|
||||
button, surface,
|
||||
"{flavor:?}:10% 与 12% 必须落在不同像素值上(合成一个数就是这次要防的回归)"
|
||||
);
|
||||
|
||||
// 内容色同理:按钮 38% 叠 10% 底 ≠ 面状 38% 叠 12% 底
|
||||
assert_ne!(
|
||||
button_content,
|
||||
disabled_content_over_role(&ui, Role::OnSurfaceVariant, true),
|
||||
"{flavor:?}:两族的「有容器」内容色不能同源"
|
||||
);
|
||||
assert_eq!(
|
||||
disabled_button_content(&ui, Role::OnSurfaceVariant, true),
|
||||
button_content,
|
||||
"{flavor:?}:按钮内容入口必须与按钮颜色入口同源"
|
||||
);
|
||||
// 无容器时两族完全一致(都叠 surface)——差异只在「有容器」那一支
|
||||
assert_eq!(
|
||||
disabled_button_content(&ui, Role::OnSurfaceVariant, false),
|
||||
disabled_content_over_role(&ui, Role::OnSurfaceVariant, false),
|
||||
"{flavor:?}:没有容器时不该有族差异"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
@@ -0,0 +1,276 @@
|
||||
//! 禁用态两族 α 与合成色的单测(M229 复审 F1 后从 `disabled.rs` 纯搬移,守 400 行硬线)。
|
||||
//!
|
||||
//! 内容一行未改;新增的 `disabled_colors_follow_the_brand_board_not_the_tone_baseline` 是 F1 的回归门,
|
||||
//! 它与既有两条「α 族」测试放在一起,读的人一眼能看到「族」与「色源」两件事。
|
||||
|
||||
use super::*;
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_design::typography::Platform;
|
||||
|
||||
use crate::theme::Families;
|
||||
|
||||
const FAMILIES: Families = Families {
|
||||
text: "Segoe UI",
|
||||
display: "Segoe UI",
|
||||
mono: "Consolas",
|
||||
};
|
||||
|
||||
fn ui(flavor: SchemeFlavor) -> Ui {
|
||||
Ui::with_platform(flavor, Platform::Windows, FAMILIES)
|
||||
}
|
||||
|
||||
/// 两族的 α 必须都钉住(只钉一条,另一族被合并时会静默错 1/255)。
|
||||
#[test]
|
||||
fn both_disabled_alpha_classes_are_pinned_to_the_tokens() {
|
||||
// 绑定成局部变量再过断言:直接比两个常量会被 clippy 判成「常量断言」。
|
||||
let button_alpha = DISABLED_CONTAINER_ALPHA;
|
||||
let surface_alpha = DISABLED_SURFACE_ALPHA;
|
||||
let content_alpha = DISABLED_CONTENT_ALPHA;
|
||||
// 按钮容器:官方 *ButtonTokens.DisabledContainerOpacity = 0.1f
|
||||
assert!(
|
||||
(button_alpha - 0.10).abs() < f32::EPSILON,
|
||||
"按钮禁用容器 α 必须是 0.10(官方 DisabledContainerOpacity);\
|
||||
0.12 是面状控件那一类,别混用"
|
||||
);
|
||||
// 面状控件:chip 描边 / slider 未激活轨 / switch 轨道 = 0.12f
|
||||
assert!(
|
||||
(surface_alpha - 0.12).abs() < f32::EPSILON,
|
||||
"面状控件禁用 α 必须是 0.12(官方 FlatDisabledOutlineOpacity / \
|
||||
DisabledInactiveTrackOpacity / DisabledTrackOpacity)"
|
||||
);
|
||||
assert!((content_alpha - 0.38).abs() < f32::EPSILON);
|
||||
assert!(
|
||||
surface_alpha > button_alpha,
|
||||
"两族必须可区分:面状控件比按钮容器更实({surface_alpha} > {button_alpha})"
|
||||
);
|
||||
}
|
||||
|
||||
/// **面状控件族**的那一对颜色 = 0.12 预混合(chip / switch / slider / TextField 描边)。
|
||||
///
|
||||
/// ⚠️ 期望值**写死 0.12 / 0.38**,不从 `DISABLED_SURFACE_ALPHA` 取:
|
||||
/// 从被测常量求期望是**自指断言**——常量一改,expected 跟着改,永远相等(复审 r1 P2-1 实测:
|
||||
/// 把 `DISABLED_SURFACE_ALPHA` 改成 0.10,旧版这条照样绿)。现在常量改坏必红。
|
||||
#[test]
|
||||
fn surface_family_disabled_colors_are_the_12_percent_class() {
|
||||
const EXPECTED_SURFACE_ALPHA: f32 = 0.12;
|
||||
const EXPECTED_CONTENT_ALPHA: f32 = 0.38;
|
||||
let actual_alpha = DISABLED_SURFACE_ALPHA;
|
||||
assert_eq!(
|
||||
actual_alpha, EXPECTED_SURFACE_ALPHA,
|
||||
"面状控件族 α 必须是官方 0.12f;改常量会让下面每一条合成色断言同时红"
|
||||
);
|
||||
|
||||
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
|
||||
let ui = ui(flavor);
|
||||
// 期望值按**品牌色板**(= `Ui` 注入的那一份),不是 tone 派生的 baseline。
|
||||
let scheme = unlucky_design::color::BrandBoard::for_flavor(flavor).scheme();
|
||||
let base = scheme.get(Role::Surface);
|
||||
let expected_container = scheme
|
||||
.get(Role::OnSurface)
|
||||
.with_alpha(EXPECTED_SURFACE_ALPHA)
|
||||
.over(base);
|
||||
|
||||
let (surface, surface_content) = disabled_colors(&ui, Role::Surface);
|
||||
assert_eq!(
|
||||
surface,
|
||||
to_iced(expected_container),
|
||||
"{flavor:?}:面状控件容器 = on-surface@12% 叠表面(期望值硬编码,不取自常量)"
|
||||
);
|
||||
assert_eq!(
|
||||
surface_content,
|
||||
to_iced(
|
||||
scheme
|
||||
.get(Role::OnSurfaceVariant)
|
||||
.with_alpha(EXPECTED_CONTENT_ALPHA)
|
||||
.over(expected_container)
|
||||
),
|
||||
"{flavor:?}:面状控件内容仍是 38%,只是叠的底不同"
|
||||
);
|
||||
assert_ne!(surface, ui.color(Role::Surface), "12% 叠上去必须能看出来");
|
||||
assert_eq!(surface.a, 1.0, "禁用容器也是实色(CPU/GPU 一致)");
|
||||
assert_eq!(surface_content.a, 1.0, "禁用内容也是实色");
|
||||
// 内容叠在容器上 vs 叠在表面上,是两回事(probe 量的是前者)
|
||||
assert_ne!(
|
||||
surface_content,
|
||||
to_iced(
|
||||
scheme
|
||||
.get(Role::OnSurfaceVariant)
|
||||
.with_alpha(EXPECTED_CONTENT_ALPHA)
|
||||
.over(base)
|
||||
),
|
||||
"{flavor:?}:内容必须叠在容器上"
|
||||
);
|
||||
// 跨族对照:面状族的合成色必须与按钮族不同(两族一旦合流,这条红)
|
||||
let (button, _) = disabled_button_colors(&ui, Role::Surface);
|
||||
assert_ne!(
|
||||
surface, button,
|
||||
"{flavor:?}:面状族 12% 与按钮族 10% 必须落在不同像素值上"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// **按钮家族**的那一对颜色 = 0.10 预混合,且与面状控件族**必须落在不同像素值上**。
|
||||
///
|
||||
/// 期望值同样写死(0.10 / 0.38),理由见上一条:自指断言抓不住常量被改坏。
|
||||
#[test]
|
||||
fn button_family_disabled_colors_are_the_10_percent_class() {
|
||||
const EXPECTED_BUTTON_ALPHA: f32 = 0.10;
|
||||
const EXPECTED_CONTENT_ALPHA: f32 = 0.38;
|
||||
let actual_alpha = DISABLED_CONTAINER_ALPHA;
|
||||
assert_eq!(
|
||||
actual_alpha, EXPECTED_BUTTON_ALPHA,
|
||||
"按钮族 α 必须是官方 0.1f(*ButtonTokens.DisabledContainerOpacity)"
|
||||
);
|
||||
|
||||
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
|
||||
let ui = ui(flavor);
|
||||
// 期望值按**品牌色板**(= `Ui` 注入的那一份),不是 tone 派生的 baseline。
|
||||
let scheme = unlucky_design::color::BrandBoard::for_flavor(flavor).scheme();
|
||||
let base = scheme.get(Role::Surface);
|
||||
let expected_container = scheme
|
||||
.get(Role::OnSurface)
|
||||
.with_alpha(EXPECTED_BUTTON_ALPHA)
|
||||
.over(base);
|
||||
|
||||
let (button, button_content) = disabled_button_colors(&ui, Role::Surface);
|
||||
assert_eq!(
|
||||
button,
|
||||
to_iced(expected_container),
|
||||
"{flavor:?}:按钮容器 = on-surface@10% 叠表面(官方 DisabledContainerOpacity = 0.1f)"
|
||||
);
|
||||
assert_eq!(
|
||||
button_content,
|
||||
to_iced(
|
||||
scheme
|
||||
.get(Role::OnSurfaceVariant)
|
||||
.with_alpha(EXPECTED_CONTENT_ALPHA)
|
||||
.over(expected_container)
|
||||
),
|
||||
);
|
||||
|
||||
let (surface, _) = disabled_colors(&ui, Role::Surface);
|
||||
assert_ne!(
|
||||
button, surface,
|
||||
"{flavor:?}:10% 与 12% 必须落在不同像素值上(合成一个数就是这次要防的回归)"
|
||||
);
|
||||
|
||||
// 内容色同理:按钮 38% 叠 10% 底 ≠ 面状 38% 叠 12% 底
|
||||
assert_ne!(
|
||||
button_content,
|
||||
disabled_content_over_role(&ui, Role::OnSurfaceVariant, true),
|
||||
"{flavor:?}:两族的「有容器」内容色不能同源"
|
||||
);
|
||||
assert_eq!(
|
||||
disabled_button_content(&ui, Role::OnSurfaceVariant, true),
|
||||
button_content,
|
||||
"{flavor:?}:按钮内容入口必须与按钮颜色入口同源"
|
||||
);
|
||||
// 无容器时两族完全一致(都叠 surface)——差异只在「有容器」那一支
|
||||
assert_eq!(
|
||||
disabled_button_content(&ui, Role::OnSurfaceVariant, false),
|
||||
disabled_content_over_role(&ui, Role::OnSurfaceVariant, false),
|
||||
"{flavor:?}:没有容器时不该有族差异"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// **M229 复审 F1 的回归门**:禁用态的合成必须跟着**品牌色板**走,不许偷偷回到 tone 派生的 baseline。
|
||||
///
|
||||
/// Ink 板的中性色是绿系(`on-surface` 与 `surface` 的 G 通道最高),baseline 是蓝系;所以只要
|
||||
/// 「禁用容器」在 Ink 下是绿系(`g >= b`)且与 baseline 算出来的值**不相等**,就说明它走的是板。
|
||||
///
|
||||
/// 抓得住:有人把三处 `Scheme::baseline(ui.flavor())` 写回去(那时本测试立刻红,且报出偏蓝的像素)。
|
||||
/// 抓不住:色板本身改了色相(那是 `design` 的 `anchors_pin_the_spec` 的事)。
|
||||
#[test]
|
||||
fn disabled_colors_follow_the_brand_board_not_the_tone_baseline() {
|
||||
use unlucky_design::color::BrandBoard;
|
||||
|
||||
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
|
||||
let ui = ui(flavor);
|
||||
let board = BrandBoard::for_flavor(flavor).scheme();
|
||||
let baseline = Scheme::baseline(flavor);
|
||||
|
||||
// ① 面状控件族的容器(chip 描边 / slider 未激活轨 / switch 轨道)
|
||||
let (surface, _surface_content) = disabled_colors(&ui, Role::Surface);
|
||||
let expect_surface = to_iced(
|
||||
board
|
||||
.get(Role::OnSurface)
|
||||
.with_alpha(DISABLED_SURFACE_ALPHA)
|
||||
.over(board.get(Role::Surface)),
|
||||
);
|
||||
assert_eq!(
|
||||
surface, expect_surface,
|
||||
"{flavor:?}:禁用容器必须按色板合成"
|
||||
);
|
||||
|
||||
let baseline_surface = to_iced(
|
||||
baseline
|
||||
.get(Role::OnSurface)
|
||||
.with_alpha(DISABLED_SURFACE_ALPHA)
|
||||
.over(baseline.get(Role::Surface)),
|
||||
);
|
||||
assert_ne!(
|
||||
surface, baseline_surface,
|
||||
"{flavor:?}:禁用容器退回了 baseline(这就是复审 F1 的蓝系像素)"
|
||||
);
|
||||
|
||||
// ② 按钮族的容器
|
||||
let (button, _) = disabled_button_colors(&ui, Role::Surface);
|
||||
assert_ne!(button, surface, "{flavor:?}:两族 α 仍要可区分");
|
||||
|
||||
// ③ 内容色(两个入口)
|
||||
assert_eq!(
|
||||
disabled_content_over_role(&ui, Role::OnSurfaceVariant, true),
|
||||
to_iced(
|
||||
board
|
||||
.get(Role::OnSurfaceVariant)
|
||||
.with_alpha(DISABLED_CONTENT_ALPHA)
|
||||
.over(
|
||||
board
|
||||
.get(Role::OnSurface)
|
||||
.with_alpha(DISABLED_SURFACE_ALPHA)
|
||||
.over(board.get(Role::Surface))
|
||||
)
|
||||
),
|
||||
"{flavor:?}:面状控件族内容色要按色板合成"
|
||||
);
|
||||
assert_eq!(
|
||||
disabled_button_content(&ui, Role::OnSurfaceVariant, true),
|
||||
to_iced(
|
||||
board
|
||||
.get(Role::OnSurfaceVariant)
|
||||
.with_alpha(DISABLED_CONTENT_ALPHA)
|
||||
.over(
|
||||
board
|
||||
.get(Role::OnSurface)
|
||||
.with_alpha(DISABLED_CONTAINER_ALPHA)
|
||||
.over(board.get(Role::Surface))
|
||||
)
|
||||
),
|
||||
"{flavor:?}:按钮族内容色要按色板合成"
|
||||
);
|
||||
|
||||
// ④ **色相判据**(像素级的自证):Ink 板的禁用容器是绿系(G 最高),baseline 是蓝系(B 最高)。
|
||||
// Ink 板:`on-surface` 与 `surface` 的 G 通道都高于 B 通道(两个中性角色都是绿黑),
|
||||
// 合成后仍是 G ≥ B;baseline 深色则是 B > G(天蓝中性)。浅色板上两者都是近中性,
|
||||
// 所以只在深色档下判色相。
|
||||
if matches!(flavor, SchemeFlavor::Dark) {
|
||||
let (r, g, b) = channels(surface);
|
||||
assert!(
|
||||
g >= b,
|
||||
"{flavor:?}:禁用容器 {r}/{g}/{b} 仍是蓝系(G < B),说明没走 Ink 板"
|
||||
);
|
||||
let (_, bg, bb) = channels(baseline_surface);
|
||||
assert!(bb > bg, "对照组不合格:baseline 深色的禁用容器应当是蓝系");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// iced 颜色 → 8bit 通道(像素值,报告里的证据就用它)。
|
||||
fn channels(color: Color) -> (u8, u8, u8) {
|
||||
(
|
||||
(color.r * 255.0).round() as u8,
|
||||
(color.g * 255.0).round() as u8,
|
||||
(color.b * 255.0).round() as u8,
|
||||
)
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
//! Checkbox(`research/15` §18):20×20 / 圆角 2 / 描边向内收的 2px 框 / 勾形 12dp 方头 /
|
||||
//! indeterminate 形变为居中短横 / Ø40 状态层 / 48 命中区。
|
||||
//! Checkbox(`research/15` §18 + 规格卡):**18×18 / 圆角 4** / 描边向内收的 2px 框 /
|
||||
//! 勾形 10.8dp 方头(路径比例 0.6 × 边长)/ indeterminate 形变为居中短横 / Ø40 状态层 / 48 命中区。
|
||||
//!
|
||||
//! 尺寸口径(M229):官方 `Checkbox.kt` 画 20×20 / rx2,规格卡画 `rect 18×18 rx4`,本实现取规格卡
|
||||
//! (`inkcat::metrics::CHECKBOX`)。勾形**比例不变**(0.2→0.8 的横向跨度),所以墨水跨度随边长从
|
||||
//! 12dp 变成 10.8dp。
|
||||
//!
|
||||
//! ## 三条容易画错的(每条都有对应的像素断言或单测)
|
||||
//!
|
||||
//! 1. **描边向内**:外缘恒 20(§18.3)。iced 的 quad 渲染器把描边路径**向内缩半个线宽**
|
||||
//! (`iced_tiny_skia::engine::draw_quad`:`border_bounds + border_width / 2`),与 Compose
|
||||
//! `drawBox` 的 `topLeft = stroke/2, size = 20 − stroke` 同一口径,所以选中/未选中外缘同尺寸。
|
||||
//! 1. **描边向内**:外缘恒 **18**(§18.3 的尺寸换成本项目的 18)。iced 的 quad 渲染器把描边路径
|
||||
//! **向内缩半个线宽**(`iced_tiny_skia::engine::draw_quad`:`border_bounds + border_width / 2`),
|
||||
//! 与 Compose `drawBox` 的 `topLeft = stroke/2, size = side − stroke` 同一口径,所以
|
||||
//! 选中/未选中外缘同尺寸。
|
||||
//! 2. **勾不是图标**:勾形按路径比例画(`shell::glyph`),代码里**没有第二个 `IconKind`**(§18.6)。
|
||||
//! 3. **状态层是 Ø40 的圆**、命中区是 48:两者不同尺寸,所以外壳是透明的 48 按钮,
|
||||
//! 里面套一个 40 的圆容器,再套 20 的方框(见 `shell` 的模块文档)。
|
||||
//! 里面套一个 40 的圆容器,再套 18 的方框(见 `shell` 的模块文档)。
|
||||
|
||||
use iced::widget::{container, row, text};
|
||||
use iced::{Alignment, Element, Length, Padding};
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
//!
|
||||
//! | # | 量 | 参考值 | 来源 | 落点 |
|
||||
//! |---|---|---|---|---|
|
||||
//! | 18.1 | Checkbox 外框 / 圆角 | **20×20** / **2** | `[源]` `Checkbox.kt:737-738`(`COMPONENT_WIDTH/HEIGHT=18` 是死值,见 §26.2) | `Component::Checkbox.metrics()` + `ComponentRadius::Micro` |
|
||||
//! | 18.2 | Checkbox 描边 | **2**(选中时 0) | `[源]` `UnselectedOutlineWidth` | `ComponentMetrics::CHECKBOX.stroke` |
|
||||
//! | 18.1 | Checkbox 外框 / 圆角 | 官方 **20×20** / **2**;**本项目取规格卡 18×18 / rx4** | `[源]` `Checkbox.kt:737-738` + 规格卡 `rect 18×18 rx4` | `inkcat::metrics::CHECKBOX` |
|
||||
//! | 18.2 | Checkbox 描边 | **2**(选中时 0) | `[源]` `UnselectedOutlineWidth` / 规格卡 `stroke-width=2` | `inkcat::metrics::CHECKBOX_STROKE` |
|
||||
//! | 18.5 | Checkbox 状态层 | **Ø40** | `[源]` `StateLayerSize` | `ComponentMetrics::CHECKBOX.state_layer` |
|
||||
//! | 18.9 | 命中区 / 标签间距 | **48** / **8** | `[源]` `minimumInteractiveComponentSize` / 间距 `[估]` | `LayoutMetrics::touch_target` / `metrics.gap` |
|
||||
//! | 19.1 | Radio 外圈 / 环线 | **20** / **2** | `[源]` `IconSize` / `RadioStrokeWidth` | `Component::Radio.metrics()` |
|
||||
//! | 19.2 | Radio 内点 | **10**(不是 12) | `[源]` `DotSize=12`,绘制半径 12/2−2/2=5 | `ComponentMetrics::RADIO.inner_dot` |
|
||||
//! | 19.1 | Radio 外圈 / 环线 | 官方 **20** / **2**;**本项目取规格卡 18 Ø** / 2 | `[源]` `IconSize` / `RadioStrokeWidth` + 规格卡 `r=9` | `inkcat::metrics::RADIO` |
|
||||
//! | 19.2 | Radio 内点 | **10**(不是 12) | `[源]` `DotSize=12`,绘制半径 12/2−2/2=5;规格卡 `r=5` 同为 10 Ø | `inkcat::metrics::RADIO_DOT` |
|
||||
//! | 10.1 | Switch 轨道 | **52×32**,圆角 full(16) | `[源]` `TrackWidth/TrackHeight/TrackShape` | [`SwitchGeometry`](**G16**) |
|
||||
//! | 10.2 | Switch 滑块 | 选中 **24** / 未选 **16** / 按下 **28** | `[源]` `Selected/Unselected/PressedHandleWidth` | [`SwitchGeometry`](**G16**) |
|
||||
//! | 13.1 | Chip 高 / 圆角 | **32** / **8** | `[源]` `ContainerHeight` / `ContainerShape=CornerSmall` | `Space::X8` + `Component::Chip.spec().shape` |
|
||||
|
||||
@@ -53,8 +53,8 @@ pub fn heading_and_note<'a>(ui: &'a Ui) -> Element<'a, ()> {
|
||||
heading(
|
||||
ui,
|
||||
"selection · checkbox / radio / switch / chip",
|
||||
"15 §18/§19 + 12b §10/§13:checkbox 20×20 圆角 2 描边 2(向内收)勾 12 方头 · \
|
||||
radio 外圈 20 环线 2 内点 10 · switch 轨道 52×32 滑块 24/16 · chip 高 32 圆角 8 · \
|
||||
"规格卡 + 15 §18/§19 + 12b §10/§13:checkbox **18×18 圆角 4** 描边 2(向内收)勾 10.8 方头 · \
|
||||
radio 外圈 **18** 环线 2 内点 10 · switch 轨道 52×32 滑块 24/16 · chip 高 32 圆角 8 · \
|
||||
Ø40 状态层 · 命中 48",
|
||||
)
|
||||
.map(|_| ())
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
//!
|
||||
//! | 组件 | 参考章节 | 尺寸一句话 |
|
||||
//! |---|---|---|
|
||||
//! | [`Checkbox`] | `research/15` §18 | 20×20 / 圆角 2 / 描边 2(向内收)/ 勾 12dp 方头 / Ø40 状态层 / 命中 48 |
|
||||
//! | [`Radio`] | `research/15` §19 | 外圈 20 / 环线 2 / 内点 **10**(不是 12)/ Ø40 状态层 |
|
||||
//! | [`Checkbox`] | `research/15` §18 + 规格卡 | **18×18** / 圆角 **4** / 描边 2(向内收)/ 勾 10.8dp 方头 / Ø40 状态层 / 命中 48 |
|
||||
//! | [`Radio`] | `research/15` §19 + 规格卡 | 外圈 **18** / 环线 2 / 内点 **10**(不是 12)/ Ø40 状态层 |
|
||||
//! | [`Switch`] | `research/12b` §10 | 轨道 52×32 / 滑块 24·16 / Ø40 状态层 |
|
||||
//! | [`Chip`] | `research/12b` §13 | 高 32 / 圆角 8 / 内边距 8 / Assist·Filter·Input |
|
||||
//!
|
||||
|
||||
@@ -134,7 +134,7 @@ fn on_ramp(pixel: [u8; 3], fill: [u8; 3], under: [u8; 3]) -> bool {
|
||||
///
|
||||
/// 踩过的两个坑:① 按「精确色」连通会把圆形的环切成碎片(每片面积很小,量不到内点);
|
||||
/// ② 按「更接近 fill 还是底色」判会让**另一种深色墨**混进来(`OnSurfaceVariant` 比 `Primary`
|
||||
/// 更接近深色,于是未选中的框被当成 20×20 的 Primary 块)。所以判据是「落在混色线上」。
|
||||
/// 更接近深色,于是未选中的框被当成 18×18 的 Primary 块)。所以判据是「落在混色线上」。
|
||||
pub(super) fn ink_boxes(
|
||||
shot: &Shot,
|
||||
color: Color,
|
||||
|
||||
@@ -72,7 +72,7 @@ fn selection_geometry_matches_the_reference() {
|
||||
|
||||
// ---- §18.1 / §18.3 / §19.1:20 的外缘,选中与未选中**同尺寸** -----------------
|
||||
// 逐块断言(复审 P2-4):**每一块** Primary 都必须是 20(±1)——任何一块到 22 就说明描边
|
||||
// 画到了框外。只数「20×20 有几块」的时候,「把其中一格放大成 22」照样绿。
|
||||
// 画到了框外。只数「18×18 有几块」的时候,「把其中一格放大成 22」照样绿。
|
||||
let filled = ink_boxes(&shot, primary, surface, 15, 24);
|
||||
for bbox in &filled {
|
||||
assert!(
|
||||
@@ -310,7 +310,7 @@ fn selection_geometry_matches_the_reference() {
|
||||
.collect();
|
||||
assert!(
|
||||
container_20.is_empty(),
|
||||
"P2-3:20×20 的禁用块不许是 12% 容器色(那是普通禁用容器,选中禁用是 38%),实测 {}",
|
||||
"P2-3:18×18 的禁用块不许是 12% 容器色(那是普通禁用容器,选中禁用是 38%),实测 {}",
|
||||
dims(&container_20)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
//! Radio button(`research/15` §19):外圈 **20** / 环线 **2** / 内点 **10**(不是 12)/
|
||||
//! Radio button(`research/15` §19 + 规格卡):外圈 **18** / 环线 **2** / 内点 **10**(不是 12)/
|
||||
//! 选中环+点 `Primary` / 未选中环 `OnSurfaceVariant`(hover 提到 `OnSurface`)/ Ø40 状态层。
|
||||
//!
|
||||
//! ## 为什么它不是「基于 Checkbox 画的」
|
||||
//!
|
||||
//! §19.6 明令:Radio **只能**是「2px 环 + 实心内点」,Checkbox **只能**是「2px 方框 + 勾/横」;
|
||||
//! 两者尺寸相同(20)但形状语言不同,禁止混用。所以本文件是独立实现,**没有** import
|
||||
//! 两者尺寸相同(**18**)但形状语言不同,禁止混用。所以本文件是独立实现,**没有** import
|
||||
//! `super::checkbox`(gallery 的 probe 另有一条断言:radio 的墨迹里不得出现方角像素)。
|
||||
//!
|
||||
//! ## 几何怎么落到像素
|
||||
//!
|
||||
//! 环:20×20 容器 + `Border { width: 2, radius: 10 }`。iced 的 quad 渲染器把描边路径向内缩
|
||||
//! 半个线宽(半径 10−1=9、路径外接 18),再以 2 描边 → 外缘正好 20、内缘 16。这与 Compose
|
||||
//! `RadioButton.kt` 的「环半径 = 10 − 1 = 9 且描边居中 → 外缘正好 20」逐像素同源。
|
||||
//! 环:**18×18** 容器 + `Border { width: 2, radius: 9 }`。iced 的 quad 渲染器把描边路径向内缩
|
||||
//! 半个线宽(半径 9−1=8、路径外接 16),再以 2 描边 → 外缘正好 18、内缘 14。这与 Compose
|
||||
//! `RadioButton.kt` 的「环半径 = side/2 − 1 且描边居中 → 外缘正好 side」逐像素同源,
|
||||
//! 只看 side 取 18(规格卡 `r=9`)而不是官方的 20。
|
||||
//! 内点:独立 10×10 圆容器居中在环里(绘制半径 12/2 − 2/2 = 5,即**直径 10**,§19.2)。
|
||||
|
||||
use iced::widget::{container, row, text};
|
||||
|
||||
@@ -67,7 +67,7 @@ fn checkbox_geometry_comes_from_the_token_layer() {
|
||||
);
|
||||
}
|
||||
|
||||
/// §19.1 / §19.2:Radio 外圈 20、环线 2、**内点 10 不是 12**、真圆。
|
||||
/// §19.1 / §19.2:Radio 外圈 **18**(规格卡)、环线 2、**内点 10 不是 12**、真圆。
|
||||
#[test]
|
||||
fn radio_inner_dot_is_ten_not_twelve() {
|
||||
let ui = ui();
|
||||
|
||||
@@ -149,6 +149,19 @@ impl Ui {
|
||||
to_iced(self.scheme.get(role))
|
||||
}
|
||||
|
||||
/// 当前生效的 **49 角色方案**(品牌色板 / D38 取色方案,二选一的那一份)。
|
||||
///
|
||||
/// 凡是要**在令牌侧合成颜色**的地方(禁用态 38% 叠底、对话框遮罩 scrim、状态层叠色)都必须
|
||||
/// 从这里取,**不许**再 `Scheme::baseline(self.flavor())` 重算:
|
||||
///
|
||||
/// - 重算出来的永远是 tone 派生的旧色(天蓝中性),品牌板上的禁用滑块轨道、填充框、遮罩会
|
||||
/// 各画各的(M229 复审 F1 实测:Ink 下三个蓝系像素值,应为绿系去饱和、遮罩为中性
|
||||
/// 绿黑;具体数值与出处见 `DevDocs/artifacts/m229/README.md` §6)。
|
||||
/// - 取色方案(D38)成功时也必须同源,否则「取色接管 49 个角色」这件事在禁用态上破一个洞。
|
||||
pub fn scheme(&self) -> &Scheme {
|
||||
&self.scheme
|
||||
}
|
||||
|
||||
/// 材质层(Canvas/Card/Overlay)→ 容器色;**只有令牌允许半透明的层才乘面板 α**(M93 唯一来源)。
|
||||
///
|
||||
/// α 乘在这里而不是调用点:所有表面色都从这里出去(根容器/标题栏/导航轨/页头带/卡片),
|
||||
|
||||
Reference in new issue
Block a user