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:
AskaEth committed 2026-11-11 01:49:14 +08:00
1 parent f9877a89d8
commit 4ce24d6fbd
18 files changed
+361 -213

No files matched your search

@@ -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,
)
}