feat(app-iced): 禁用态合成底改成容器给的 Base(M234)

`interaction::disabled` 的四个入口一直收一个「底」,但 8 个调用点全传死
`Role::Surface`:卡片(SurfaceContainer)/ 面板(SurfaceContainerLow)里的禁用控件
把 10%(按钮族)/ 12%(面状控件族)叠在画布色上 —— M223「CR 1.05、形丢失」的根因。

- 新增 `interaction::Base`(画布 / 容器底的一等参数,`From<Role>` 兼容历史调用点);
  四个入口改成 `(ui, base, …)`,内部不再出现任何写死的 `Role::Surface`。
- 容器导出自己的 `BASE`:`surfaces::elevated::BASE`(卡)、`inputs::menu::PANEL_BASE`
  (菜单面板,项的 38% 与状态层都改叠它)、`navigation::drawer::BASE`。
- 控件收 `.base(..)`:Button / icon_on / Field / SearchBar / Slider / Checkbox / Radio /
  Switch / Chip / ListItem / Tabs;Fab 与 icon() 是画布默认值。
- `feedback/action.rs` 的本地 α 合成删掉,改走 `disabled_content_over_role` 并给真实容器底
  (Snackbar `InverseSurface` / Dialog `SurfaceContainerHigh`)。
- 判据:`interaction::disabled::tests`(卡片/面板/画布三档逐值)、
  `buttons::tests::base`(Button::base → StyleSpec → style_closure 接线)、
  `gallery` 新增 `disabledbase` 段 + `gallery::probe::bases` 精确像素断言
  (±10 容差区分不了 1/255 的差,所以自己数逐位相等的像素)。
- 变异自证:assemble 的调用点改回画布底后,上面两条都红(见 artifacts/m234/README.md)。

已知未尽:`crates/app-iced/src/pages/**` 不在 scope 内,页面侧「把控件放进卡片/面板」的
调用点尚未接 `.base(..)`(行为与本 mission 之前一致);`pages/…/java/tests/marker.rs`
有一行被迫更新(入口多了一个实参,语义不变 = 画布底)。
This commit is contained in:
AskaEth committed 2026-11-11 06:34:23 +08:00
1 parent a9d1cb4c44
commit 35dbefb08d
48 files changed
+1690 -496

No files matched your search

@@ -71,7 +71,7 @@ fn surface_family_disabled_colors_are_the_12_percent_class() {
.with_alpha(EXPECTED_SURFACE_ALPHA)
.over(base);
let (surface, surface_content) = disabled_colors(&ui, Role::Surface);
let (surface, surface_content) = disabled_colors(&ui, Base::CANVAS);
assert_eq!(
surface,
to_iced(expected_container),
@@ -102,7 +102,7 @@ fn surface_family_disabled_colors_are_the_12_percent_class() {
"{flavor:?}:内容必须叠在容器上"
);
// 跨族对照:面状族的合成色必须与按钮族不同(两族一旦合流,这条红)
let (button, _) = disabled_button_colors(&ui, Role::Surface);
let (button, _) = disabled_button_colors(&ui, Base::CANVAS);
assert_ne!(
surface, button,
"{flavor:?}:面状族 12% 与按钮族 10% 必须落在不同像素值上"
@@ -133,7 +133,7 @@ fn button_family_disabled_colors_are_the_10_percent_class() {
.with_alpha(EXPECTED_BUTTON_ALPHA)
.over(base);
let (button, button_content) = disabled_button_colors(&ui, Role::Surface);
let (button, button_content) = disabled_button_colors(&ui, Base::CANVAS);
assert_eq!(
button,
to_iced(expected_container),
@@ -149,7 +149,7 @@ fn button_family_disabled_colors_are_the_10_percent_class() {
),
);
let (surface, _) = disabled_colors(&ui, Role::Surface);
let (surface, _) = disabled_colors(&ui, Base::CANVAS);
assert_ne!(
button, surface,
"{flavor:?}:10% 与 12% 必须落在不同像素值上(合成一个数就是这次要防的回归)"
@@ -158,18 +158,18 @@ fn button_family_disabled_colors_are_the_10_percent_class() {
// 内容色同理:按钮 38% 叠 10% 底 ≠ 面状 38% 叠 12% 底
assert_ne!(
button_content,
disabled_content_over_role(&ui, Role::OnSurfaceVariant, true),
disabled_content_over_role(&ui, Base::CANVAS, Role::OnSurfaceVariant, true),
"{flavor:?}:两族的「有容器」内容色不能同源"
);
assert_eq!(
disabled_button_content(&ui, Role::OnSurfaceVariant, true),
disabled_button_content(&ui, Base::CANVAS, Role::OnSurfaceVariant, true),
button_content,
"{flavor:?}:按钮内容入口必须与按钮颜色入口同源"
);
// 无容器时两族完全一致(都叠 surface)——差异只在「有容器」那一支
assert_eq!(
disabled_button_content(&ui, Role::OnSurfaceVariant, false),
disabled_content_over_role(&ui, Role::OnSurfaceVariant, false),
disabled_button_content(&ui, Base::CANVAS, Role::OnSurfaceVariant, false),
disabled_content_over_role(&ui, Base::CANVAS, Role::OnSurfaceVariant, false),
"{flavor:?}:没有容器时不该有族差异"
);
}
@@ -192,7 +192,7 @@ fn disabled_colors_follow_the_brand_board_not_the_tone_baseline() {
let baseline = Scheme::baseline(flavor);
// ① 面状控件族的容器(chip 描边 / slider 未激活轨 / switch 轨道)
let (surface, _surface_content) = disabled_colors(&ui, Role::Surface);
let (surface, _surface_content) = disabled_colors(&ui, Base::CANVAS);
let expect_surface = to_iced(
board
.get(Role::OnSurface)
@@ -216,12 +216,12 @@ fn disabled_colors_follow_the_brand_board_not_the_tone_baseline() {
);
// ② 按钮族的容器
let (button, _) = disabled_button_colors(&ui, Role::Surface);
let (button, _) = disabled_button_colors(&ui, Base::CANVAS);
assert_ne!(button, surface, "{flavor:?}:两族 α 仍要可区分");
// ③ 内容色(两个入口)
assert_eq!(
disabled_content_over_role(&ui, Role::OnSurfaceVariant, true),
disabled_content_over_role(&ui, Base::CANVAS, Role::OnSurfaceVariant, true),
to_iced(
board
.get(Role::OnSurfaceVariant)
@@ -236,7 +236,7 @@ fn disabled_colors_follow_the_brand_board_not_the_tone_baseline() {
"{flavor:?}:面状控件族内容色要按色板合成"
);
assert_eq!(
disabled_button_content(&ui, Role::OnSurfaceVariant, true),
disabled_button_content(&ui, Base::CANVAS, Role::OnSurfaceVariant, true),
to_iced(
board
.get(Role::OnSurfaceVariant)
@@ -267,6 +267,108 @@ fn disabled_colors_follow_the_brand_board_not_the_tone_baseline() {
}
}
/// **M234 的主判据**:禁用态的合成必须用**给的底**,不是画布色。
///
/// 卡片(`SurfaceContainer`)与面板(`SurfaceContainerLow`)里各放一个禁用控件时,
/// 10%(按钮族)/ 12%(面状族)那一层要叠在**卡/面板的底**上;叠在 `Surface` 上就是 M223
/// 记的「CR 1.05、形丢失」。
///
/// 抓得住:① 入口把 `base` 丢了(内部又写死 `Role::Surface`);② 入口把两个族的 α 合成一个数;
/// ③ 调用点给了画布底(这条测试只证「给了卡片底 → 算出卡片色」,调用点是否真的给了由
/// `buttons::tests::a_disabled_button_composites_over_the_base_it_is_given` 与 gallery 的像素
/// probe 分别守)。
///
/// 抓不住:① 容器画的是**合成色**(半透明面板 α、叠过状态层的底)——`Base` 只装一个 `Role`;
/// ② 卡/面板组件的 `BASE` 常量与它自己画的色不一致(那条由 `surfaces::elevated::tests` 与
/// 像素 probe 守);③ GPU(wgpu) 档(本测试是纯值,两档同源同值)。
#[test]
fn disabled_paint_composites_over_the_base_it_is_given() {
use unlucky_design::color::BrandBoard;
use unlucky_design::color::Role;
const BUTTON_ALPHA: f32 = 0.10;
const SURFACE_ALPHA: f32 = 0.12;
const CONTENT_ALPHA: f32 = 0.38;
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
let ui = ui(flavor);
let scheme = BrandBoard::for_flavor(flavor).scheme();
let card = Base::of(Role::SurfaceContainer);
let panel = Base::of(Role::SurfaceContainerLow);
// 期望值一律**独立算**(写死 α + 直接取色板角色),不调被测常量。
let over = |base: Role, alpha: f32| {
to_iced(
scheme
.get(Role::OnSurface)
.with_alpha(alpha)
.over(scheme.get(base)),
)
};
// ① 按钮族 10%:卡片底 / 面板底 / 画布底三档互不相等
let (card_button, card_button_ink) = disabled_button_colors(&ui, card);
let (panel_button, _) = disabled_button_colors(&ui, panel);
let (canvas_button, canvas_button_ink) = disabled_button_colors(&ui, Base::CANVAS);
assert_eq!(
card_button,
over(Role::SurfaceContainer, BUTTON_ALPHA),
"{flavor:?}:卡片里的禁用按钮容器 = on-surface@10% 叠**卡片底**"
);
assert_eq!(
panel_button,
over(Role::SurfaceContainerLow, BUTTON_ALPHA),
"{flavor:?}:面板里的禁用按钮容器 = on-surface@10% 叠**面板底**"
);
assert_ne!(card_button, canvas_button, "{flavor:?}:卡片底 ≠ 画布底");
assert_ne!(panel_button, canvas_button, "{flavor:?}:面板底 ≠ 画布底");
assert_ne!(card_button, panel_button, "{flavor:?}:卡片底 ≠ 面板底");
// ② 面状控件族 12%(字段容器 / chip 容器 / switch 轨道)
let (card_surface, _) = disabled_colors(&ui, card);
let (panel_surface, _) = disabled_colors(&ui, panel);
let (canvas_surface, _) = disabled_colors(&ui, Base::CANVAS);
assert_eq!(
card_surface,
over(Role::SurfaceContainer, SURFACE_ALPHA),
"{flavor:?}:卡片里的禁用字段容器 = on-surface@12% 叠**卡片底**"
);
assert_eq!(
panel_surface,
over(Role::SurfaceContainerLow, SURFACE_ALPHA),
"{flavor:?}:面板里的禁用字段容器 = on-surface@12% 叠**面板底**"
);
assert_ne!(card_surface, canvas_surface);
assert_ne!(panel_surface, canvas_surface);
// ③ 内容色也跟着底走(38% 叠在容器上 / 直接叠在底上,两支都要换底)
assert_eq!(
disabled_button_content(&ui, card, Role::OnSurfaceVariant, true),
to_iced(
scheme
.get(Role::OnSurfaceVariant)
.with_alpha(CONTENT_ALPHA)
.over(
scheme
.get(Role::OnSurface)
.with_alpha(BUTTON_ALPHA)
.over(scheme.get(Role::SurfaceContainer))
)
),
"{flavor:?}:有容器的内容色 = 38% 叠(10% 叠卡片底)"
);
assert_ne!(
card_button_ink, canvas_button_ink,
"{flavor:?}:内容叠的容器换了底"
);
assert_ne!(
disabled_content_over_role(&ui, card, Role::OnSurface, false),
disabled_content_over_role(&ui, Base::CANVAS, Role::OnSurface, false),
"{flavor:?}:无容器时 38% 也必须叠在**给的底**上"
);
}
}
/// iced 颜色 → 8bit 通道(像素值,报告里的证据就用它)。
fn channels(color: Color) -> (u8, u8, u8) {
(