Files
Unlucky-Launcher/crates/app-iced/src/components/buttons/tests.rs
T
AskaEth 8f5dac2ed6 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
把标题行当主色带的既有问题(用改前的图 + 改前的探针复现过,与本次换色无关)。
2026-11-11 14:32:26 +08:00

361 lines
12 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.
//! 按钮家族的单测(几何值 / 色彩角色 / 样式闭包)。
//!
//! 这里测的是「函数与样式结构」;**像素级**的几何(40 高、20 圆角、24 图标盒)
//! 由 `gallery/probe.rs` 对着真截图量——M68 的教训是「函数对、图错」,两者缺一不可。
use iced::widget::button;
use iced::{Background, Shadow};
use unlucky_design::color::SchemeFlavor;
use unlucky_design::elevation::Elevation;
use unlucky_design::icons::IconKind;
use unlucky_design::spacing::Space;
use super::super::interaction::{self, Base, Interaction};
use super::assemble::StyleSpec;
use super::{style_closure, Geometry, IconGeometry, IconVariant, Variant};
use crate::theme::{Families, Ui, STROKE_PX};
const FAMILIES: Families = Families {
text: "Segoe UI",
display: "Segoe UI",
mono: "Consolas",
};
fn ui(flavor: SchemeFlavor) -> Ui {
Ui::with_platform(
flavor,
unlucky_design::typography::Platform::Windows,
FAMILIES,
)
}
fn light() -> Ui {
ui(SchemeFlavor::Light)
}
#[derive(Debug, Clone, PartialEq)]
enum Msg {
Pressed(&'static str),
}
#[test]
fn every_variant_builds_and_disabled_has_no_message() {
let ui = light();
let _ = super::filled(&ui, super::Button::new("Filled", Msg::Pressed("f")));
let _ = super::tonal(&ui, super::Button::new("Tonal", Msg::Pressed("t")));
let _ = super::elevated(&ui, super::Button::new("Elevated", Msg::Pressed("e")));
let _ = super::outlined(&ui, super::Button::new("Outlined", Msg::Pressed("o")));
let _ = super::text(&ui, super::Button::new("Text", Msg::Pressed("x")));
let _ = super::filled::<Msg>(&ui, super::Button::disabled("Disabled"));
let _ = super::filled(
&ui,
super::Button::new("Icons", Msg::Pressed("i"))
.leading(IconKind::Plus)
.trailing(IconKind::ChevronRight),
);
}
/// 12b 1.1~1.6 + 2.1~2.2:几何逐值钉住(每个值都要能指回令牌)。
#[test]
fn geometry_matches_the_reference_values() {
let ui = light();
let geometry = Geometry::of(&ui, Variant::Filled, false);
assert_eq!(geometry.height, 40.0, "12b 1.1 可见高 40");
assert_eq!(geometry.height, ui.metrics().control_height);
assert_eq!(
ui.radius(unlucky_design::shape::ShapeScale::Full, geometry.height)
.top_left,
20.0,
"12b 1.2 full 圆角 = 高/2"
);
// M229:内距取规格卡的 `ControlSize::pad_x`(S 档 = 20),不再是 12b 的 24。
assert_eq!(geometry.padding.left, 20.0, "S 档 pad_x 20");
assert_eq!(geometry.padding.right, 20.0, "S 档 pad_x 20");
assert_eq!(geometry.padding.top, 0.0);
assert_eq!(geometry.gap, 8.0, "12b 1.6 图标↔文字 8");
assert_eq!(geometry.gap, ui.space(Space::X2));
assert_eq!(geometry.stroke, 0.0, "实心按钮无描边");
let with_icon = Geometry::of(&ui, Variant::Filled, true);
assert_eq!(
with_icon.padding.left, 16.0,
"带图标左侧再收 4(20 − 4):与 12b 1.4 的 16 恰好同值"
);
assert_eq!(with_icon.padding.right, 20.0, "带图标右内边距不动");
let outlined = Geometry::of(&ui, Variant::Outlined, false);
assert_eq!(outlined.stroke, STROKE_PX, "rubric G7 全站 1px");
assert_eq!(outlined.stroke, 1.0);
// 图标 18 是**令牌值**(`IconSize::Button`,AAR `ButtonDefaults.IconSize`);
// 12b 1.6 的 18 与它同源。**别按 `ButtonSmallTokens.IconSize = 20` 改**:那是零消费者的死令牌。
assert_eq!(
geometry.icon, 18.0,
"IconSize::Button(官方 ButtonDefaults.IconSize)"
);
assert_eq!(
geometry.icon,
unlucky_design::icons::IconSize::Button.px(),
"组件侧必须只从令牌取值(不许写字面量)"
);
assert_eq!(geometry.height % 4.0, 0.0, "可见高必须在 4px 栅格上(D1)");
}
#[test]
fn icon_button_geometry_matches_the_reference() {
let ui = light();
let geometry = IconGeometry::of(&ui);
assert_eq!((geometry.side, geometry.icon), (40.0, 24.0), "12b 2.1/2.2");
assert_eq!(
ui.radius(unlucky_design::shape::ShapeScale::Full, geometry.side)
.top_left,
20.0,
"40×40 的 full 圆角 = 20"
);
assert_eq!(geometry.padding, 8.0, "12b 2.2 默认边距 8");
// 命中区 = 容器 40 + 四周各 4(12b 2.8);按钮自身不撑高
assert_eq!(
geometry.side + 2.0 * ui.space(Space::X1),
ui.metrics().touch_target,
"40 + 8 = 48 命中下限"
);
}
mod base;
mod icon;
mod roles;
/// 视觉 reviewer P2-3:同一控件里不许混用填充字形与描边字形(线宽语汇会打架)。
///
/// gallery 的样例图标(前置/后置)必须同为 `IconPaint::Stroke`;允许实心字形,但只能作为
/// 「主操作」单独出场的图标(例如播放键),不能与描边图标同处一颗按钮。
#[test]
fn sample_icons_never_mix_fill_and_stroke() {
use unlucky_design::icons::IconPaint;
let sample = [IconKind::Download, IconKind::ChevronRight];
for kind in sample {
assert_eq!(
kind.icon().paint,
IconPaint::Stroke,
"{} 是填充字形,不能和描边图标混在同一颗按钮里",
kind.name()
);
}
// 已知的实心字形(留档,下次有人拿它们当按钮图标时这条会提醒)
assert_eq!(IconKind::Play.icon().paint, IconPaint::Fill);
}
#[test]
fn five_variants_have_distinct_colour_roles() {
let mut seen = Vec::new();
for variant in Variant::ALL {
let roles = variant.roles();
assert!(
!seen.contains(&roles),
"{variant:?} 与别的变体色彩角色重复了"
);
seen.push(roles);
}
// 12b 1.9 逐条角色名核对
assert_eq!(
Variant::Filled.roles(),
(
Some(unlucky_design::color::Role::Primary),
unlucky_design::color::Role::OnPrimary,
None
)
);
assert_eq!(
Variant::Tonal.roles(),
(
Some(unlucky_design::color::Role::SecondaryContainer),
unlucky_design::color::Role::OnSecondaryContainer,
None
)
);
assert_eq!(
Variant::Elevated.roles(),
(
Some(unlucky_design::color::Role::SurfaceContainerLow),
unlucky_design::color::Role::Primary,
None
)
);
assert_eq!(
Variant::Outlined.roles(),
(
None,
unlucky_design::color::Role::OnSurfaceVariant,
Some(unlucky_design::color::Role::Outline)
),
"描边取 Outline(M248:非文字元素角色;OutlineVariant 是装饰分隔线的)"
);
assert_eq!(
Variant::Text.roles(),
(None, unlucky_design::color::Role::OnSurfaceVariant, None)
);
for variant in Variant::ALL {
let expected = if variant == Variant::Elevated {
Elevation::Level1
} else {
Elevation::Level0
};
assert_eq!(variant.elevation(), expected, "{variant:?} 高程");
}
}
#[test]
fn icon_button_variants_match_the_reference_roles() {
use unlucky_design::color::Role;
assert_eq!(
IconVariant::Standard.roles(),
(None, Role::OnSurfaceVariant, None)
);
assert_eq!(
IconVariant::Filled.roles(),
(Some(Role::Primary), Role::OnPrimary, None)
);
assert_eq!(
IconVariant::Tonal.roles(),
(
Some(Role::SecondaryContainer),
Role::OnSecondaryContainer,
None
)
);
let ui = light();
for variant in IconVariant::ALL {
let _ = super::icon::<Msg>(&ui, variant, IconKind::Settings, None, None);
let _ = super::icon(
&ui,
variant,
IconKind::Settings,
Some(Msg::Pressed("i")),
None,
);
// 五态取证开关都能出元素(gallery 矩阵依赖它)
for state in Interaction::ALL {
let _ = super::icon(
&ui,
variant,
IconKind::Settings,
Some(Msg::Pressed("i")),
Some(state),
);
}
}
}
fn spec_of(variant: Variant, forced: Option<Interaction>) -> StyleSpec {
let (fill, content, border) = variant.roles();
StyleSpec {
fill,
content,
border,
shape: variant.shape(),
base: Base::CANVAS,
stroke: Geometry::of(&light(), variant, false).stroke,
radius_side: Geometry::of(&light(), variant, false).height,
forced,
}
}
/// **渲染层**:样式闭包吐给 iced 的圆角/描边/阴影必须是令牌推导值。
#[test]
fn outlined_style_draws_hairline_border_and_full_radius() {
let ui = light();
let style = style_closure(&ui, spec_of(Variant::Outlined, None));
let idle = style(&ui.theme(), button::Status::Active);
assert_eq!(idle.background, None, "Outlined 静息容器必须透明");
assert_eq!(idle.border.width, STROKE_PX, "1dp 描边");
assert_eq!(idle.border.radius.top_left, 20.0, "full 圆角");
assert_eq!(
idle.border.color,
ui.color(unlucky_design::color::Role::Outline)
);
assert_eq!(idle.shadow, Shadow::default(), "L0 不许有阴影");
// 透明容器也要看得见 hover:状态层叠到表面色上
let hovered = style(&ui.theme(), button::Status::Hovered);
assert_ne!(hovered.background, None, "透明容器的 hover 必须有可见变化");
assert_ne!(hovered.background, idle.background);
let disabled = style(&ui.theme(), button::Status::Disabled);
let (container, content) = interaction::disabled_button_colors(&ui, Base::CANVAS);
assert_eq!(disabled.text_color, content, "禁用内容 = 令牌 38%");
assert_eq!(
disabled.border.color, container,
"禁用描边 = 按钮族的令牌 10%(OutlinedButtonTokens.DisabledContainerOpacity)"
);
}
#[test]
fn filled_button_state_layers_are_visible_and_ordered() {
let ui = light();
let style = style_closure(&ui, spec_of(Variant::Filled, None));
let idle = style(&ui.theme(), button::Status::Active);
let hovered = style(&ui.theme(), button::Status::Hovered);
let pressed = style(&ui.theme(), button::Status::Pressed);
assert_eq!(
idle.background,
Some(Background::Color(
ui.color(unlucky_design::color::Role::Primary)
))
);
assert_ne!(hovered.background, idle.background, "hover 8%");
assert_ne!(
pressed.background, hovered.background,
"pressed 10% ≠ hover 8%"
);
assert_eq!(
hovered.text_color,
ui.color(unlucky_design::color::Role::OnPrimary),
"12b 2.4/4.7:状态层不换内容色"
);
assert_eq!(idle.border.width, 0.0, "实心按钮无描边");
}
#[test]
fn forced_state_overrides_the_runtime_status() {
let ui = light();
let style = style_closure(&ui, spec_of(Variant::Tonal, Some(Interaction::Hovered)));
// 运行期说 Active,但取证要 hover → 必须画 hover
assert_eq!(
style(&ui.theme(), button::Status::Active).background,
style(&ui.theme(), button::Status::Hovered).background
);
assert_ne!(
style(&ui.theme(), button::Status::Active).background,
Some(Background::Color(
ui.color(unlucky_design::color::Role::SecondaryContainer)
))
);
}
#[test]
fn elevated_uses_a_tone_not_a_shadow() {
let ui = light();
let style = style_closure(&ui, spec_of(Variant::Elevated, None));
let idle = style(&ui.theme(), button::Status::Active);
assert_eq!(idle.shadow, Shadow::default(), "rubric §7:L1 不画阴影");
assert_eq!(
idle.background,
Some(Background::Color(
ui.color(unlucky_design::color::Role::SurfaceContainerLow)
)),
"L1 由 SurfaceContainerLow 的色调差表达"
);
assert_eq!(
idle.text_color,
ui.color(unlucky_design::color::Role::Primary)
);
}