fix(app-iced): 三处 Pixel 偏离按角色语义收敛(M248)

P2:Elevated 的容器改由高程令牌推导(Elevation::Level1.surface_role),解析值仍是
SurfaceContainerLow,图上逐像素不变;「比 Surface 高一档」从此只有 elevation.rs 一处定义,
组件里不再手抄角色名。

P3:Outlined 的 1dp 描边 OutlineVariant → Outline。design/color/mod.rs 的对比度纪律里
Outline 才是「非文字元素(目标 3:1)」,OutlineVariant 是纯装饰分隔线;图标按钮那一式本来
就是 Outline,文字按钮此前与它不一致(全站两套描边)。实测 Outline/Surface:Ink 5.08、
Silver 4.24(OutlineVariant 1.97 / 1.62)。

P4:进度轨道(线性 + 环形)SecondaryContainer → SurfaceContainerHighest——轨道是「还剩多少」
的背景尺,强调色只属于指示条。与 A1(LinearProgressIndicatorTokens.TrackColor)的差异在
progress.rs 模块文档与 geometry.rs 的 12.2 行登记,gallery 图注同步改成角色名。

判据:新增 buttons/tests/roles.rs 与进度轨道用例,断言的是「取到的角色」与 Ink / Silver /
Scheme::from_seed 三套方案下解析出的颜色,不钉任何 hex;三处各做一次变异自证(改回旧值即红)。

另修 feedback probe 的定位判据:轨道换成中性容器面后,「画布→轨道」的混色轴穿过中性文字,
按「计数」会把图注行数成轨道,改按「连续段」判(bands_by_run);顺带修掉 indeterminate_row
把标题行当主色带的既有问题(用改前的图 + 改前的探针复现过,与本次换色无关)。
This commit is contained in:
AskaEth committed 2026-11-11 14:32:26 +08:00
1 parent 81710ba38c
commit 8f5dac2ed6
10 files changed
+402 -34

No files matched your search

+12 -7
View File
@@ -12,7 +12,8 @@
//! | 1.6 | 图标 18 + 图标↔文字 8 | **18** = `IconSize::Button`(AAR `ButtonDefaults.IconSize`);间距 `Space::X2` | [`Geometry`] |
//! | 1.7 | 最小宽 58 | **G15**:iced 的 `Length` 无 min-width,不施加(实测见报告) | — |
//! | 1.8 | 状态层 + 禁用 | hover 8 / focus 10 / pressed 10;容器 **10%**(官方 `DisabledContainerOpacity`;面状控件的 12% 是另一类)& 内容 38%;**内容角色按变体**([`Variant::disabled_content`]) | [`super::interaction`] |
//! | 1.9 | 五式色彩角色 | Filled `Primary`/`OnPrimary`;Tonal `SecondaryContainer`/`OnSecondaryContainer`;Elevated `SurfaceContainerLow`/`Primary`;Outlined 透明 + 1dp `OutlineVariant` + `OnSurfaceVariant`;Text 透明 + `OnSurfaceVariant` | [`Variant::roles`] |
//! | 1.9 | 五式色彩角色 | Filled `Primary`/`OnPrimary`;Tonal `SecondaryContainer`/`OnSecondaryContainer`;Elevated = **`Elevation::Level1.surface_role()`**(= `SurfaceContainerLow`)/`Primary`;Outlined 透明 + 1dp `Outline` + `OnSurfaceVariant`;Text 透明 + `OnSurfaceVariant` | [`Variant::roles`] |
//! | 1.9b | 与 A1 的登记差异(产品口径,M248) | Outlined 描边 A1 是 `OutlineVariant`,本项目取 **`Outline`**:1dp 边界属于**非文字元素**(目标 3:1,`design/color/mod.rs` 的对比度纪律),`OutlineVariant` 是装饰分隔线的角色;且 `IconVariant::Outlined` 一直是 `Outline`,两族由此统一 | [`Variant::roles`] |
//! | 2.1 | IconButton 容器 | **40×40**、圆角 full | [`IconGeometry`] |
//! | 2.2 | IconButton 图标 / 边距 | 图标 **24**、边距 **8** | [`IconGeometry`] |
//! | 2.4 | Standard 各态**图标色不变** | 只有状态层变化 | [`Variant::roles`] 与态无关 |
@@ -77,9 +78,9 @@ pub enum Variant {
Filled,
/// 次强调(tonal):`SecondaryContainer`。
Tonal,
/// 带高程:`SurfaceContainerLow` + `Primary` 文本(L1 用色调差,不画阴影)。
/// 带高程:L1 的容器面(`Elevation::Level1.surface_role()` = `SurfaceContainerLow`)+ `Primary` 文本。
Elevated,
/// 描边:透明容器 + 1dp `OutlineVariant`。
/// 描边:透明容器 + 1dp `Outline`(非文字元素角色;装饰分隔线才用 `OutlineVariant`)。
Outlined,
/// 纯文本:最低权重。
Text,
@@ -158,8 +159,12 @@ impl Variant {
Role::OnSecondaryContainer,
None,
),
Variant::Elevated => (Some(Role::SurfaceContainerLow), Role::Primary, None),
Variant::Outlined => (None, Role::OnSurfaceVariant, Some(Role::OutlineVariant)),
// 容器面由**高程令牌**推导(L1 → `SurfaceContainerLow`):不许在组件里再抄一遍角色名,
// 换层级口径只改 `elevation.rs` 一处(M248:P2 的落地方式是改来源,不是改色值)。
Variant::Elevated => (Some(self.elevation().surface_role()), Role::Primary, None),
// 1dp 边界取 `Outline`(非文字元素、目标 3:1);`OutlineVariant` 是装饰分隔线的角色,
// 拿它画边界在 Ink/Silver 上都到不了 3:1,也和 `IconVariant::Outlined` 不一致(M248:P3)。
Variant::Outlined => (None, Role::OnSurfaceVariant, Some(Role::Outline)),
Variant::Text => (None, Role::OnSurfaceVariant, None),
}
}
@@ -270,12 +275,12 @@ pub fn tonal<'a, M: Clone + 'a>(ui: &'a Ui, button: Button<'a, M>) -> Element<'a
assemble(ui, Variant::Tonal, button)
}
/// Elevated 按钮(`SurfaceContainerLow` + `Primary` 文本,L1 用色调差)。
/// Elevated 按钮(L1 的容器面 + `Primary` 文本,用色调差表达高程、不画阴影)。
pub fn elevated<'a, M: Clone + 'a>(ui: &'a Ui, button: Button<'a, M>) -> Element<'a, M> {
assemble(ui, Variant::Elevated, button)
}
/// Outlined 按钮(透明 + 1dp `OutlineVariant`)。
/// Outlined 按钮(透明 + 1dp `Outline`)。
pub fn outlined<'a, M: Clone + 'a>(ui: &'a Ui, button: Button<'a, M>) -> Element<'a, M> {
assemble(ui, Variant::Outlined, button)
}
@@ -124,6 +124,7 @@ fn icon_button_geometry_matches_the_reference() {
mod base;
mod icon;
mod roles;
/// 视觉 reviewer P2-3:同一控件里不许混用填充字形与描边字形(线宽语汇会打架)。
///
@@ -188,8 +189,9 @@ fn five_variants_have_distinct_colour_roles() {
(
None,
unlucky_design::color::Role::OnSurfaceVariant,
Some(unlucky_design::color::Role::OutlineVariant)
)
Some(unlucky_design::color::Role::Outline)
),
"描边取 Outline(M248:非文字元素角色;OutlineVariant 是装饰分隔线的)"
);
assert_eq!(
Variant::Text.roles(),
@@ -275,7 +277,7 @@ fn outlined_style_draws_hairline_border_and_full_radius() {
assert_eq!(idle.border.radius.top_left, 20.0, "full 圆角");
assert_eq!(
idle.border.color,
ui.color(unlucky_design::color::Role::OutlineVariant)
ui.color(unlucky_design::color::Role::Outline)
);
assert_eq!(idle.shadow, Shadow::default(), "L0 不许有阴影");
@@ -0,0 +1,190 @@
//! 角色语义 + 「可主题化」断言(M248:Elevated 底 / Outlined 描边)。
//!
//! **判据不是「等于某个 hex」**:品牌色板本来就会被换(D38 的取色方案会整表接管 49 个角色),
//! 钉死色值等于把「配色可自定义」这条路焊上。所以这里钉两件互相补位的事:
//!
//! 1. [`role_table_is_named_roles_not_colours`]:`Variant::roles()` 吐的是**角色名**;Elevated 的
//! 容器还必须由**高程令牌**推导(`Elevation::Level1.surface_role()`),不许在组件里手抄一遍;
//! 2. [`styles_follow_the_scheme_across_boards_and_custom_accents`]:同一变体在 Ink / Silver /
//! 自定义种子方案(`Scheme::from_seed`)三套方案下,`button::Style` 里解出的颜色**分别等于
//! 各自方案的该角色色值**,且三者互不相同。把「取角色」改成「取死值(`Role::Surface`)」
//! 或「写死某套方案」都会被这条打红。
//!
//! 抓不住什么:
//! - **令牌表里两个角色同值**时,换角色的变异只有第 1 条会红;
//! - **像素**:角色写对了、图上却是别的颜色(样式闭包被别处覆盖)——那是 `gallery/probe` 与出图的事。
use iced::widget::button;
use iced::Background;
use unlucky_design::color::{BrandBoard, Role, Scheme, SchemeFlavor};
use unlucky_design::elevation::Elevation;
use unlucky_design::typography::Platform;
use super::super::{style_closure, IconVariant, Variant};
use super::spec_of;
use crate::theme::{Ui, STROKE_PX};
/// 三套配色来源:两块出厂品牌板 + 一套 `Scheme::from_seed` 自定义种子方案。
///
/// 第三套是关键:品牌板是固化数据,**换不了**;只有「同一份组件代码吃进另一套方案就换色」
/// 才叫可自定义(D38 的取色链就是这么接管 49 个角色的)。种子用 design 里的
/// [`DEFAULT_SEED`](取色链默认的天蓝)——它是令牌,不是本文件写下的色值字面量
/// (`tests/no_hardcoded_tokens.rs` 拦 `#RRGGBB`,这条门禁是对的)。
fn themes() -> Vec<(&'static str, Ui)> {
vec![
(
"ink",
Ui::with_board(BrandBoard::Ink, Platform::Windows, super::FAMILIES),
),
(
"silver",
Ui::with_board(BrandBoard::Silver, Platform::Windows, super::FAMILIES),
),
(
"custom",
Ui::from_scheme(
Scheme::from_seed(SchemeFlavor::Dark, unlucky_design::color::DEFAULT_SEED),
super::FAMILIES,
),
),
]
}
#[test]
fn role_table_is_named_roles_not_colours() {
// ---- P2:Elevated 的容器是**高程令牌的推论**,不是一个手抄的角色名 ----------
assert_eq!(Variant::Elevated.elevation(), Elevation::Level1, "L1");
assert_eq!(
Variant::Elevated.roles().0,
Some(Elevation::Level1.surface_role()),
"Elevated 的底必须由高程令牌推导(L1 → surface_role = SurfaceContainerLow),\
组件里不许再写一遍这个角色名"
);
assert_ne!(
Elevation::Level1.surface_role(),
Role::Surface,
"L1 的容器面必须比画布高一档(否则 Elevated 与画布同色)"
);
// ---- P3:1dp 边界取 `Outline`;`OutlineVariant` 是装饰分隔线的角色 ----------
assert_eq!(
Variant::Outlined.roles(),
(None, Role::OnSurfaceVariant, Some(Role::Outline)),
"Outlined 的 1dp 描边取 Outline(`design/color/mod.rs` 的对比度纪律:\
Outline 是「非文字元素」角色、目标 3:1;OutlineVariant 是纯装饰分隔线)"
);
// 同族一致性:图标按钮那一式一直是 Outline,此前文字按钮是 OutlineVariant ⇒ 全站两套描边
assert_eq!(
IconVariant::Outlined.roles().2,
Some(Role::Outline),
"图标按钮 Outlined 与文字按钮 Outlined 必须是同一个描边角色"
);
}
/// 非文字元素的对比度下限 3:1(WCAG 1.4.11;`design/color/mod.rs` 的对比度纪律同一口径)。
///
/// 这条是 P3 的**理由**而不只是判据:`OutlineVariant` 在深/浅两块板上都到不了 3:1,
/// 拿它画按钮边界等于「看得见形状、量不出边界」。别把下限写死成某个角色的实测值——
/// 换品牌板或换取色方案时,比例是**算出来的**,不是抄出来的。
#[test]
fn outlined_boundary_meets_the_non_text_contrast_bar() {
for (name, ui) in themes() {
let scheme = ui.scheme();
let boundary = scheme.contrast(Role::Outline, Role::Surface);
assert!(
boundary >= 3.0,
"{name}: Outline/Surface = {boundary:.2},非文字元素下限是 3:1"
);
// 报告用:装饰角色差多少(`--nocapture` 可见),不改判据
eprintln!(
"[{name}] Outline/Surface={boundary:.2} · OutlineVariant/Surface={:.2}",
scheme.contrast(Role::OutlineVariant, Role::Surface)
);
}
}
#[test]
fn styles_follow_the_scheme_across_boards_and_custom_accents() {
let elevated = spec_of(Variant::Elevated, Some(super::Interaction::Enabled));
let outlined = spec_of(Variant::Outlined, Some(super::Interaction::Enabled));
let mut elevated_fills = Vec::new();
let mut outlined_strokes = Vec::new();
for (name, ui) in themes() {
let style = style_closure(&ui, elevated);
let idle = style(&ui.theme(), button::Status::Active);
let expected = ui.color(Elevation::Level1.surface_role());
assert_eq!(
idle.background,
Some(Background::Color(expected)),
"{name}: Elevated 的底 = 本方案的 L1 容器面"
);
assert_ne!(
idle.background,
Some(Background::Color(ui.color(Role::Surface))),
"{name}: Elevated 退化成画布色了(取死值 Surface 的变异会打红这条)"
);
assert_eq!(
idle.text_color,
ui.color(Role::Primary),
"{name}: Elevated 标签 = Primary"
);
let style = style_closure(&ui, outlined);
let idle = style(&ui.theme(), button::Status::Active);
assert_eq!(
idle.border.color,
ui.color(Role::Outline),
"{name}: Outlined 描边 = Outline"
);
assert_eq!(idle.border.width, STROKE_PX, "{name}: 描边 1dp");
assert_eq!(
idle.text_color,
ui.color(Role::OnSurfaceVariant),
"{name}: Outlined 标签 = OnSurfaceVariant"
);
elevated_fills.push(expected);
outlined_strokes.push(idle.border.color);
}
// 三套方案两两不同 ⇒「跟着配色来源走」这件事有实据(不是三套恰好同色)
for (index, (name, _)) in themes().iter().enumerate() {
for other in index + 1..elevated_fills.len() {
assert_ne!(
elevated_fills[index], elevated_fills[other],
"{name} 与第 {other} 套方案的 Elevated 底同色,可主题化没有实据"
);
assert_ne!(
outlined_strokes[index], outlined_strokes[other],
"{name} 与第 {other} 套方案的 Outlined 描边同色"
);
}
}
}
/// 基线条:Ink(深)与 Silver(浅)两块**出厂**板必须给出不同的 Elevated 底与 Outlined 描边。
///
/// 与上一条分开写:上一条的三套方案里有一套是自定义的,这条只钉出厂外观——
/// 哪怕有人把 `themes()` 改成只跑自定义方案,出厂这块的回归也还在。
#[test]
fn ink_and_silver_resolve_the_same_roles_to_different_colours() {
let ink = Ui::with_board(BrandBoard::Ink, Platform::Windows, super::FAMILIES);
let silver = Ui::with_board(BrandBoard::Silver, Platform::Windows, super::FAMILIES);
for role in [
Elevation::Level1.surface_role(),
Role::Outline,
Role::OnSurfaceVariant,
] {
assert_ne!(
ink.color(role),
silver.color(role),
"{role:?} 在 Ink 与 Silver 上同色,说明它没跟着色板走"
);
}
// 浅色板的 L1 容器必须比画布暗、深色板必须比画布亮(层级方向不能反)
assert!(silver.color(Role::Surface).r > silver.color(Role::SurfaceContainerLow).r);
assert!(ink.color(Role::Surface).r < ink.color(Role::SurfaceContainerLow).r);
}