From 9bb3c33b8c5747e771f23a3d76ce6648728efbbd Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 11 Nov 2026 06:14:41 +0800 Subject: [PATCH] =?UTF-8?q?fix(app-iced):=20=E5=A4=8D=E5=AE=A1=20r1=20?= =?UTF-8?q?=E5=9B=9B=E6=9D=A1=20P2=20=E6=94=B6=E5=8F=A3=EF=BC=88M234?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - P2-1 判据恒绿:`gallery/probe/bases.rs` 的「反向检查」把字段档计数(`:172` 遮蔽后的 `hits`)拿去比按钮档 stray ⇒ 恒绿。改成 `button_hits` / `field_hits` 两个计数,各比各的 stray,失败信息的数值与单位对齐量测对象。变异自证:卡列里留一颗忘了 `.base(..)` 的按钮 ⇒ 该断言单独红(2137 px vs 3178 px,退出码 101)。 - P2-2 FAB 的底错了:禁用态背景仍是满档 `PrimaryContainer`(`Disabled.layer() == None`), 38% 内容却合在画布上。改成 `Base::of(PrimaryContainer)` + `on_container = false` (那一层 10% 在 FAB 身上不存在),两处调用收进一个 `content_color()`;新增值判据 `disabled_fab_ink_composites_over_its_own_container`(修前 #8CA0AA / 修后 #548FBC 量级)。 - P2-3 漏改:`selection::paint::state_layer()`(hover/focus/pressed)与 `list_item` 的 状态层叠底仍写死 `Role::Surface`,改成跟 `base`;同类两处(`assemble::transparent_state_color` 的 Outlined/Text hover、drawer 项药丸)一并跟上。 - P2-4 文档失真:`feedback/{action,mod}.rs` 删掉「本地 α 合成 / 规则只认两档」的旧描述 与失效链接;`list_item` 的旧签名;三处 `probe/base.rs` → `probe/bases.rs`; 新增 `surfaces::elevated::tests::the_card_base_constant_is_the_role_the_card_paints` 真断言(原来只在文档里声称那条测试守 `BASE`),并把这句描述改准。 - nit:`disabledbase.rs` 字符串里塌成一行的续行空格、probe 汇总行把列左缘横坐标说成 「像素计数」的措辞。 门禁(CARGO_TARGET_DIR=C:/Temp/ull-target/wt-234):fmt / check --all-targets / test -p unlucky-app-iced(896 + 6 通过,0 失败)/ clippy --workspace --all-targets -D warnings / 文件 ≤400 全绿;probe 浅色与深色都过。 --- .../src/components/buttons/assemble.rs | 10 ++- crates/app-iced/src/components/buttons/fab.rs | 43 +++++++++---- .../src/components/buttons/fab/tests.rs | 63 ++++++++++++++++++- .../src/components/feedback/action.rs | 17 ++--- .../app-iced/src/components/feedback/mod.rs | 7 +-- .../src/components/gallery/disabledbase.rs | 14 ++--- .../app-iced/src/components/gallery/probe.rs | 2 +- .../src/components/gallery/probe/bases.rs | 42 +++++++------ .../components/interaction/disabled/tests.rs | 6 +- .../src/components/navigation/drawer/item.rs | 2 +- .../src/components/navigation/list_item.rs | 10 +-- .../src/components/selection/checkbox.rs | 7 ++- .../src/components/selection/paint/chip.rs | 2 +- .../src/components/selection/paint/mod.rs | 12 ++-- .../src/components/selection/radio.rs | 2 +- .../src/components/selection/tests/chip.rs | 8 ++- .../src/components/surfaces/elevated/tests.rs | 15 +++++ 17 files changed, 190 insertions(+), 72 deletions(-) diff --git a/crates/app-iced/src/components/buttons/assemble.rs b/crates/app-iced/src/components/buttons/assemble.rs index 10b00f3..411b902 100644 --- a/crates/app-iced/src/components/buttons/assemble.rs +++ b/crates/app-iced/src/components/buttons/assemble.rs @@ -208,7 +208,7 @@ pub(super) fn style_closure( .or(Some(ui.color(role))), None => Some(ui.color(role)), }, - None => transparent_state_color(ui, state, spec.content), + None => transparent_state_color(ui, spec.base, state, spec.content), }; (background, ui.color(spec.content)) } @@ -246,14 +246,18 @@ pub(super) fn style_closure( } /// 透明容器的状态层:`Outlined` / `Text` / `StandardIcon` 没有填充,可见反馈只能来自 -/// 叠在**内容角色**上的状态层——把它们当作「叠在表面色上」求值,得到不透明实色。 +/// 叠在**内容角色**上的状态层——把它们当作「叠在 `base`(脚下那一层)上」求值,得到不透明实色。 /// /// 之所以单独一个函数:这是 M3 里唯一「容器不存在但状态层存在」的情形, /// 三个变体共用一条口径,免得各自发挥。 +/// +/// M234 复审 P2-3:`base` 之前写死 `Role::Surface`,卡片里的 Text/Outlined 按钮抬起时 +/// 状态层叠在画布色上(与禁用支的口径不一致)。 pub(super) fn transparent_state_color( ui: &Ui, + base: Base, state: Interaction, content: Role, ) -> Option { - interaction::overlay_color(ui, state, content, Role::Surface) + interaction::overlay_color(ui, state, content, base.role()) } diff --git a/crates/app-iced/src/components/buttons/fab.rs b/crates/app-iced/src/components/buttons/fab.rs index 158218f..4bf5d8d 100644 --- a/crates/app-iced/src/components/buttons/fab.rs +++ b/crates/app-iced/src/components/buttons/fab.rs @@ -86,6 +86,34 @@ impl<'a, M> Fab<'a, M> { } } +/// 禁用态的内容色:`OnPrimaryContainer` @38% 叠在 **FAB 自己的容器**(`PrimaryContainer`)上。 +/// +/// ## 为什么底是 FAB 自己的容器,不是画布(M234 复审 P2-2) +/// +/// 五式按钮的禁用容器是「`OnSurface`@10% 叠在脚下那一层」——透明的那种甚至没有容器;所以它们的 +/// 内容要叠 `0.10 容器 → 父底`。FAB 不一样:`Interaction::Disabled.layer()` 是 `None`, +/// 样式闭包走 `None => Some(ui.color(Role::PrimaryContainer))`,**禁用时背景仍是满档 +/// `PrimaryContainer`**(官方 `FloatingActionButtonTokens` 也没有 DisabledContainerOpacity)。 +/// 所以内容只能叠 `PrimaryContainer` 本身,`on_container = false`——那一层 10% 在 FAB 身上 +/// 根本不存在,给了反而多算一层灰。 +/// +/// 修前传 `Base::CANVAS`:38% 的 `OnPrimaryContainer` 合在画布色上,出来是一块与 FAB 底无关的灰 +/// (Silver 浅色实测是一块灰 `rgb(0.549,0.627,0.667)`),而不是「深色字压在淡蓝容器上」 +/// 的应有结果(同板上正确值是 `rgb(0.329,0.561,0.737)`;数字见 artifacts/m234 的 README)。 +/// +/// `button()` 与样式闭包**共用这一个函数**:早先两处各写一遍,两次都传了同一个错值。 +fn content_color(ui: &Ui, forced: Option) -> iced::Color { + match forced { + Some(Interaction::Disabled) => interaction::disabled_button_content( + ui, + Base::of(Role::PrimaryContainer), + Role::OnPrimaryContainer, + false, + ), + _ => ui.color(Role::OnPrimaryContainer), + } +} + /// 画一颗 FAB。 pub fn fab<'a, M: Clone + 'a>(ui: &'a Ui, spec: Fab<'a, M>) -> Element<'a, M> { let size = spec.size; @@ -94,13 +122,8 @@ pub fn fab<'a, M: Clone + 'a>(ui: &'a Ui, spec: Fab<'a, M>) -> Element<'a, M> { } else { None }); - let ink = match forced { - // 禁用:38% 的内容色叠在容器上(与按钮族的容器档同一口径) - Some(Interaction::Disabled) => { - interaction::disabled_button_content(ui, Base::CANVAS, Role::OnPrimaryContainer, true) - } - _ => ui.color(Role::OnPrimaryContainer), - }; + // 内容色(含禁用档)只走 `content_color` 一处 + let ink = content_color(ui, forced); let glyph = layout::fixed( size.icon_px(), @@ -159,11 +182,7 @@ pub fn fab<'a, M: Clone + 'a>(ui: &'a Ui, spec: Fab<'a, M>) -> Element<'a, M> { .or(Some(ui.color(Role::PrimaryContainer))), None => Some(ui.color(Role::PrimaryContainer)), }; - let text_color = if state.is_disabled() { - interaction::disabled_button_content(ui, Base::CANVAS, Role::OnPrimaryContainer, true) - } else { - ui.color(Role::OnPrimaryContainer) - }; + let text_color = content_color(ui, Some(state)); button::Style { background: background.map(Background::Color), diff --git a/crates/app-iced/src/components/buttons/fab/tests.rs b/crates/app-iced/src/components/buttons/fab/tests.rs index 20973d3..717c74f 100644 --- a/crates/app-iced/src/components/buttons/fab/tests.rs +++ b/crates/app-iced/src/components/buttons/fab/tests.rs @@ -1,9 +1,29 @@ //! FAB 的几何断言(四档尺寸/圆角/图标/内距,全部对着规格卡原值)。 //! //! 抓得住:档位与规格卡错位(例如把 Default 写成 48)、圆角被写成半高、扩展态内距悄悄改成 M3 的 16。 -//! 抓不住:配色与状态层(那走 `interaction` 的既有测试)、以及图上好不好看(D30)。 +//! 抓不住:图上好不好看(D30),以及 hover/pressed 的状态层(那走 `interaction` 的既有测试)。 +//! +//! 配色只钉**一条**(禁用内容色,M234 复审 P2-2):FAB 没有禁用容器档,所以 38% 必须合在 +//! 自己的 `PrimaryContainer` 上,而不是画布色。其余配色仍由 `interaction` 的测试覆盖。 +use unlucky_design::color::{Role, SchemeFlavor}; use unlucky_design::inkcat::size::FabSize; +use unlucky_design::typography::Platform; + +use super::super::super::interaction::Interaction; +use crate::theme::{to_iced, Families, Ui}; + +fn ui(flavor: SchemeFlavor) -> Ui { + Ui::with_platform( + flavor, + Platform::Windows, + Families { + text: "Segoe UI", + display: "Segoe UI", + mono: "Consolas", + }, + ) +} #[test] fn the_four_states_are_the_spec_sizes() { @@ -52,3 +72,44 @@ fn the_extended_pad_end_is_a_named_constant() { "左右对称:规格卡只量到起始 28,右侧取同值" ); } + +/// **M234 复审 P2-2 的判据**:禁用 FAB 的内容色必须合在**自己的容器**上。 +/// +/// 走的是真实现(`fab::content_color`,`button()` 与样式闭包共用它)。抓得住:底又改回画布 +/// (那就是修前那块与 FAB 底无关的灰)、或有人把 `on_container` 打开(会多算一层 +/// 10% 的 `OnSurface` 灰)。抓不住:容器本身该不该在禁用时保持满档 `PrimaryContainer` +/// (那是 M3 FAB token 的口径问题,不由本测试裁决)。 +#[test] +fn disabled_fab_ink_composites_over_its_own_container() { + for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] { + let ui = ui(flavor); + let scheme = unlucky_design::color::BrandBoard::for_flavor(flavor).scheme(); + // 期望值独立算:38% 的 OnPrimaryContainer 叠在满档 PrimaryContainer 上 + let expected = to_iced( + scheme + .get(Role::OnPrimaryContainer) + .with_alpha(0.38) + .over(scheme.get(Role::PrimaryContainer)), + ); + let actual = super::content_color(&ui, Some(Interaction::Disabled)); + assert_eq!( + actual, expected, + "{flavor:?}:禁用 FAB 的内容色 = on-primary-container@38% 叠 PrimaryContainer" + ); + + // 反向:不许等于「叠画布」那一档(修前的错值) + let over_canvas = to_iced( + scheme + .get(Role::OnPrimaryContainer) + .with_alpha(0.38) + .over(scheme.get(Role::Surface)), + ); + assert_ne!(actual, over_canvas, "{flavor:?}:又合到画布上了"); + + // 非禁用档不受影响 + assert_eq!( + super::content_color(&ui, Some(Interaction::Enabled)), + ui.color(Role::OnPrimaryContainer) + ); + } +} diff --git a/crates/app-iced/src/components/feedback/action.rs b/crates/app-iced/src/components/feedback/action.rs index 07a0090..e30c7d8 100644 --- a/crates/app-iced/src/components/feedback/action.rs +++ b/crates/app-iced/src/components/feedback/action.rs @@ -13,9 +13,10 @@ //! ## 禁用是状态不是构造器 //! //! 照 `selection/chip.rs` 修好的模式:`on_press: Option`,`None` = 禁用;文字色降到 -//! `OnSurface@38%`,标签本身**不删**。禁用色的**底**必须是这两个动作件自己的容器角色 -//! (`InverseSurface` / `SurfaceContainerHigh`)——`interaction::disabled_content_over_role` -//! 只支持「叠在 Surface / 叠在 12% 容器上」两档,拿它合成会得到「38% 深字叠深底」(复审 P2-4)。 +//! `OnSurface@38%`,标签本身**不删**。禁用色的**底**是这两个动作件自己的容器角色 +//! (`InverseSurface` / `SurfaceContainerHigh`)——M234 起 +//! [`interaction::disabled_content_over_role`] 收任意底([`interaction::Base`]),所以这里 +//! **直接传容器角色**,不再自己写一遍 α 合成(M234 之前那段本地合成会得到「38% 深字叠深底」,复审 P2-4)。 use iced::widget::{button, text}; use iced::{Background, Border, Element, Length, Padding, Shadow}; @@ -32,12 +33,12 @@ use crate::theme::Ui; use super::super::interaction::{self, Interaction}; use super::super::layout; -/// 禁用态内容色:`OnSurface@38%` 叠在**真实容器色**上(复审 P2-4)。 +/// 禁用态内容色:`OnSurface@38%` 叠在**真实容器色**上。 /// -/// `interaction::disabled_content_over_role` 的 `on_container` 只有「叠在 Surface / 叠在 -/// 12% 容器上」两档,而这两个动作件的底是 `InverseSurface`(Snackbar)与 -/// `SurfaceContainerHigh`(Dialog)——直接调它会得到「38% 深字叠深底」。 -/// 这里按同一令牌口径([`DISABLED_CONTENT_ALPHA`])在本文件合成一次,底换成真实容器角色。 +/// 这两个动作件的底是 `InverseSurface`(Snackbar)与 `SurfaceContainerHigh`(Dialog), +/// M234 之后 `interaction::disabled_content_over_role` 收任意 [`interaction::Base`],所以 +/// 这里只把容器角色包成 `Base` 交给它——α(0.38)与叠法仍由 `interaction::disabled` 一处给, +/// 本文件零 α 字面量、也没有第二份合成实现。 fn content_color(ui: &Ui, role: Role, container_role: Role, disabled: bool) -> iced::Color { if !disabled { return ui.color(role); diff --git a/crates/app-iced/src/components/feedback/mod.rs b/crates/app-iced/src/components/feedback/mod.rs index c3860b8..e53f62c 100644 --- a/crates/app-iced/src/components/feedback/mod.rs +++ b/crates/app-iced/src/components/feedback/mod.rs @@ -17,10 +17,9 @@ //! 1. **数值来自哪里**:颜色/字级/圆角/内距走令牌(`Role` / `TypeRole` / `ShapeScale` / `Space`), //! 进度厚度、Snackbar 宽高、Dialog 宽、空态块这四组令牌层没有 → 登记 **G17**([`geometry`])。 //! 2. **状态层只有一处**:hover/pressed 的叠色一律经 [`interaction::overlay_color`]; -//! 禁用内容默认经 [`interaction::disabled_content_over_role`],但**动作件是个例外**—— -//! `action.rs` 的 `content_color()` 把 38% 内容色叠在它**自己的容器角色**上 -//! (`InverseSurface` / `SurfaceContainerHigh`),因为 `interaction` 那两档只认 -//! 「叠在 Surface / 12% 容器上」; +//! 禁用内容一律经 [`interaction::disabled_content_over_role`],**底由容器给** +//! ([`interaction::Base`]):`action.rs` 传的是这两个动作件自己的容器角色 +//! (`InverseSurface` / `SurfaceContainerHigh`),其余组件传 `Base::CANVAS`; //! 禁用是**状态**(`on_press: Option` 为 `None`),不是另一个构造器(M77 之前 Chip 踩过)。 //! 3. **精确尺寸经 `layout`**:图标/几何图元一律 `layout::fixed` / `layout::centered_fixed`(Q9), //! 居中一律父级 `align_x/align_y`(Q8,不许 `center_*`)。 diff --git a/crates/app-iced/src/components/gallery/disabledbase.rs b/crates/app-iced/src/components/gallery/disabledbase.rs index f02d178..f258f91 100644 --- a/crates/app-iced/src/components/gallery/disabledbase.rs +++ b/crates/app-iced/src/components/gallery/disabledbase.rs @@ -10,21 +10,21 @@ //! 这里把三列**并排**摆出来,每列一块板、板底就是控件真踩的那层底: //! //! ```text -//! canvas Surface ← 页面底色 -//! card SurfaceContainer ← `surfaces::elevated::card` 的底(`elevated::BASE`) -//! panel SurfaceContainerLow ← Card 层(取景板 / 面板) +//! canvas Surface ← 页面底色 +//! card SurfaceContainer ← `surfaces::elevated::card` 的底(`elevated::BASE`) +//! panel SurfaceContainerLow ← Card 层(取景板 / 面板) //! ``` //! //! 每列都是**同一族控件**:Filled/Outlined 禁用按钮、Filled 禁用字段、禁用开关与 chip、 //! 禁用搜索条、禁用列表项、禁用滑块、禁用 tab。控件拿的都是这一列的 [`Base`], //! 所以列与列之间的差别只有「底」。 //! -//! ## 判据(数值在 `probe/base.rs`) +//! ## 判据(数值在 `probe/bases.rs`) //! //! 像素判据判的是**精确值**:卡列里的禁用按钮容器必须等于 `on-surface@10% 叠卡片底`, //! 且**不等于**画布底那一档。注意量级:Ink/Silver 板上 `Surface` 与 `SurfaceContainer` //! 只差个位数 RGB,10% 之后差 **≈1/255**——肉眼看不出来,所以这张图的用处是**取证**, -//! 不是「看出区别」。真判据在 `interaction::disabled::tests`(逐值)与 `probe/base.rs`(像素)。 +//! 不是「看出区别」。真判据在 `interaction::disabled::tests`(逐值)与 `probe/bases.rs`(像素)。 //! //! ```text //! ULL_GALLERY_SECTION=disabledbase $EXE --software-renderer --screen=gallery \ @@ -94,9 +94,7 @@ pub(super) fn section<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> { ), row(columns).spacing(ui.space(Space::X2)), text( - "三列的控件完全相同,只有脚下的底不同。禁用容器保留 90% 的底差(10% 只叠 on-surface), - 所以列间的差别 = 底差的九成(浅色 Silver 底差 11/255、深色 Ink 底差 6/255)。 - 这张图是取证;判据在 `interaction::disabled::tests`(逐值)与 `gallery::probe` 的 disabledbase 段(精确像素)", + "三列的控件完全相同,只有脚下的底不同。禁用容器保留 90% 的底差(10% 只叠 on-surface),所以列间差别 = 底差的九成(浅色 Silver 11/255、深色 Ink 6/255)。这张图是取证;判据在 `interaction::disabled::tests`(逐值)与 `gallery::probe` 的 disabledbase 段(精确像素)", ) .size(note.size_px) .font(ui.font_of(note)) diff --git a/crates/app-iced/src/components/gallery/probe.rs b/crates/app-iced/src/components/gallery/probe.rs index 6543e7d..fb74e7e 100644 --- a/crates/app-iced/src/components/gallery/probe.rs +++ b/crates/app-iced/src/components/gallery/probe.rs @@ -155,7 +155,7 @@ fn gallery_geometry_matches_the_tokens() { check_divider(&shot, &ui); return; } - // 禁用态合成底(M234):三块板各按自己的底合成,判据在 `probe/base.rs`。 + // 禁用态合成底(M234):三块板各按自己的底合成,判据在 `probe/bases.rs`。 if section == "disabledbase" { bases::check(&shot, &ui, flavor); return; diff --git a/crates/app-iced/src/components/gallery/probe/bases.rs b/crates/app-iced/src/components/gallery/probe/bases.rs index beb6490..bbd1823 100644 --- a/crates/app-iced/src/components/gallery/probe/bases.rs +++ b/crates/app-iced/src/components/gallery/probe/bases.rs @@ -105,7 +105,7 @@ fn find_plate(shot: &Shot, role: Role, ui: &Ui, name: &str) -> Rect { assert_eq!( columns.len(), 1, - "{name} 列({role:?} 底,{color:?})应当恰好是 1 列,实测 {}: 板子尺寸判据 MIN {MIN_PLATE_W}×{MIN_PLATE_H}", + "{name} 列({role:?} 底,{color:?})应当恰好是 1 列,实测 {}: 板子尺寸判据 MIN {MIN_PLATE_W}×{MIN_PLATE_H}", columns.len() ); columns.remove(0) @@ -162,30 +162,34 @@ pub(super) fn check(shot: &Shot, ui: &Ui, flavor: unlucky_design::color::SchemeF (panel_rect, panel, panel_button, panel_field, "panel"), ] { let (x0, x1, y0, y1) = rect; - let hits = count_exact(shot, wanted_button, x0, x1, y0, y1); + // 两个计数**分开命名**:它们是不同量测对象,拿字段数去比按钮 stray 会让断言恒绿 + // (M234 复审 P2-1 的现场)。 + let button_hits = count_exact(shot, wanted_button, x0, x1, y0, y1); assert!( - hits >= MIN_BUTTON_PX, - "{name} 列里「叠 {:?}」的禁用按钮容器只有 {hits} px(应 ≥{MIN_BUTTON_PX}):\ - 这一列的禁用控件没把真实父底传下去", + button_hits >= MIN_BUTTON_PX, + "{name} 列里「叠 {:?}」的禁用按钮容器只有 {button_hits} px(应 ≥{MIN_BUTTON_PX}): 这一列的禁用控件没把真实父底传下去", base.role() ); - let hits = count_exact(shot, wanted_field, x0, x1, y0, y1); + let field_hits = count_exact(shot, wanted_field, x0, x1, y0, y1); assert!( - hits >= MIN_FIELD_PX, - "{name} 列里「叠 {:?}」的禁用字段容器只有 {hits} px(应 ≥{MIN_FIELD_PX})", + field_hits >= MIN_FIELD_PX, + "{name} 列里「叠 {:?}」的禁用字段容器只有 {field_hits} px(应 ≥{MIN_FIELD_PX})", base.role() ); - // 反向:这一列里不许出现「叠画布底」的那一档 - let stray = count_exact(shot, canvas_button, x0, x1, y0, y1); + + // 反向 ①:**按钮**档 —— 这一列里「叠画布」的按钮容器不许和「叠该列底」的比肩。 + // 这条抓的是「只修了一半」:同列里一部分按钮仍走画布底时,正确档与错误档的像素量接近。 + let stray_button = count_exact(shot, canvas_button, x0, x1, y0, y1); assert!( - hits > stray * STRAY_RATIO, - "{name} 列里「叠 {:?}」的禁用按钮容器只有 {hits} px,而「叠画布」的有 {stray} px (要求前者 ≥ 后者 ×{STRAY_RATIO})—— 这一列的控件仍在按 Role::Surface 合成", + button_hits > stray_button * STRAY_RATIO, + "{name} 列里「叠 {:?}」的禁用按钮容器 {button_hits} px,而「叠画布」的**按钮容器** 有 {stray_button} px(要求前者 ≥ 后者 ×{STRAY_RATIO})—— 这一列的按钮仍在按 Role::Surface 合成", base.role() ); - let stray = count_exact(shot, canvas_field, x0, x1, y0, y1); + // 反向 ②:**字段**档(同理,各用各的计数) + let stray_field = count_exact(shot, canvas_field, x0, x1, y0, y1); assert!( - hits > stray * STRAY_RATIO, - "{name} 列里「叠 {:?}」的禁用字段容器只有 {hits} px,而「叠画布」的有 {stray} px (要求前者 ≥ 后者 ×{STRAY_RATIO})", + field_hits > stray_field * STRAY_RATIO, + "{name} 列里「叠 {:?}」的禁用字段容器 {field_hits} px,而「叠画布」的**字段容器** 有 {stray_field} px(要求前者 ≥ 后者 ×{STRAY_RATIO})—— 这一列的字段仍在按 Role::Surface 合成", base.role() ); assert!( @@ -205,12 +209,14 @@ pub(super) fn check(shot: &Shot, ui: &Ui, flavor: unlucky_design::color::SchemeF let canvas_card_hits = count_exact(shot, card_button, cx0, cx1, cy0, cy1); assert!( canvas_hits > canvas_card_hits * STRAY_RATIO, - "画布列里「叠画布」的禁用按钮容器 {canvas_hits} px,而「叠卡片底」的有 {canvas_card_hits} px —— 画布列的控件被传了卡片底" + "画布列里「叠画布」的禁用按钮容器 {canvas_hits} px,而「叠卡片底」的有 {canvas_card_hits} px —— 画布列的控件被传了卡片底" ); eprintln!( "[probe] OK section=disabledbase flavor={flavor:?}:\ - canvas 按钮 {:?} / 卡片 {} / 面板 {} ⇒ 三列各按自己的底合成", - canvas_button, card_rect.0, panel_rect.0 + 画布列 x<{} · 卡片列左缘 x={}..{} · 面板列左缘 x={}..{};\ + 按钮容器色 canvas {:?}({canvas_hits} px)⇒ 三列各按自己的底合成\ + (两个数字都是列左缘横坐标,不是像素计数)", + card_rect.0, card_rect.0, card_rect.1, panel_rect.0, panel_rect.1, canvas_button ); } diff --git a/crates/app-iced/src/components/interaction/disabled/tests.rs b/crates/app-iced/src/components/interaction/disabled/tests.rs index fed31d7..7295fc5 100644 --- a/crates/app-iced/src/components/interaction/disabled/tests.rs +++ b/crates/app-iced/src/components/interaction/disabled/tests.rs @@ -279,8 +279,10 @@ fn disabled_colors_follow_the_brand_board_not_the_tone_baseline() { /// probe 分别守)。 /// /// 抓不住:① 容器画的是**合成色**(半透明面板 α、叠过状态层的底)——`Base` 只装一个 `Role`; -/// ② 卡/面板组件的 `BASE` 常量与它自己画的色不一致(那条由 `surfaces::elevated::tests` 与 -/// 像素 probe 守);③ GPU(wgpu) 档(本测试是纯值,两档同源同值)。 +/// ② 卡/面板组件**画底时**用的角色与它导出的 `BASE` 不一致(`BASE` 的值本身由 +/// `surfaces::elevated::tests::the_card_base_constant_is_the_role_the_card_paints` 钉住, +/// 「card() 真的用了它」与「页面里的调用点传了它对不对」由 `gallery::probe` 的 disabledbase +/// 段守);③ GPU(wgpu) 档(本测试是纯值,两档同源同值)。 #[test] fn disabled_paint_composites_over_the_base_it_is_given() { use unlucky_design::color::BrandBoard; diff --git a/crates/app-iced/src/components/navigation/drawer/item.rs b/crates/app-iced/src/components/navigation/drawer/item.rs index 511eba6..376c0da 100644 --- a/crates/app-iced/src/components/navigation/drawer/item.rs +++ b/crates/app-iced/src/components/navigation/drawer/item.rs @@ -148,7 +148,7 @@ pub(super) fn palette( _ => ui.color(Role::OnSurface), }; ItemPalette { - pill: interaction::overlay_color(ui, state, Role::OnSurface, Role::Surface), + pill: interaction::overlay_color(ui, state, Role::OnSurface, base.role()), icon: ink, label: ink, } diff --git a/crates/app-iced/src/components/navigation/list_item.rs b/crates/app-iced/src/components/navigation/list_item.rs index 57531b2..0fd2cfc 100644 --- a/crates/app-iced/src/components/navigation/list_item.rs +++ b/crates/app-iced/src/components/navigation/list_item.rs @@ -27,8 +27,9 @@ //! ListItemDisabledTrailingIconColor = OnSurface (+ 0.38f) //! ``` //! -//! 所以这里必须走 `interaction::disabled_content_over_role(ui, OnSurface, false)`; -//! 用默认的 `disabled_colors()`(写死 `OnSurfaceVariant`)会偏浅一档。 +//! 所以这里必须走 `interaction::disabled_content_over_role(ui, base, OnSurface, false)` +//! (`base` = 行脚下的底,默认 [`Base::CANVAS`]);用默认的 `disabled_colors()` +//! (写死 `OnSurfaceVariant`)会偏浅一档。 //! //! ## 选中态:本项目口径(**偏离登记 D-M79-1**) //! @@ -182,8 +183,9 @@ fn palette(ui: &Ui, base: Base, state: Interaction, selected: bool) -> ItemPalet } ItemPalette { - // 状态层叠在**行自己的底**(Surface)上,得到不透明结果(D27) - fill: interaction::overlay_color(ui, state, Role::OnSurface, Role::Surface), + // 状态层叠在**行脚下的底**(`base`)上,得到不透明结果(D27)。 + // M234 复审 P2-3:原来写死 `Role::Surface`,卡片里的列表项抬起时会叠在画布色上。 + fill: interaction::overlay_color(ui, state, Role::OnSurface, base.role()), headline: ui.color(Role::OnSurface), supporting: ui.color(Role::OnSurfaceVariant), leading: ui.color(Role::OnSurfaceVariant), diff --git a/crates/app-iced/src/components/selection/checkbox.rs b/crates/app-iced/src/components/selection/checkbox.rs index 4c6b3ba..d886112 100644 --- a/crates/app-iced/src/components/selection/checkbox.rs +++ b/crates/app-iced/src/components/selection/checkbox.rs @@ -161,7 +161,12 @@ pub fn checkbox<'a, M: Clone + 'a>(ui: &'a Ui, spec: Checkbox<'a, M>) -> Element .align_y(Alignment::Center) .style(source.container_style(move |state| { Frame { - fill: paint::state_layer(ui, state, paint::checkbox_layer_role(filled, error)), + fill: paint::state_layer( + ui, + base, + state, + paint::checkbox_layer_role(filled, error), + ), outline: None, stroke: 0.0, radius: layer_side / 2.0, diff --git a/crates/app-iced/src/components/selection/paint/chip.rs b/crates/app-iced/src/components/selection/paint/chip.rs index ecab50a..369cc1d 100644 --- a/crates/app-iced/src/components/selection/paint/chip.rs +++ b/crates/app-iced/src/components/selection/paint/chip.rs @@ -57,7 +57,7 @@ pub fn chip( let fill = if selected { Some(ui.color(Role::SecondaryContainer)) } else { - state_layer(ui, state, label_role) + state_layer(ui, base, state, label_role) }; // Assist / 选中态的前置图标是 `Primary`(`AssistChipTokens.IconColor` / // `InputChipTokens.SelectedLeadingIconColor`);其余跟文字同色。 diff --git a/crates/app-iced/src/components/selection/paint/mod.rs b/crates/app-iced/src/components/selection/paint/mod.rs index f9bb5c6..ec4331a 100644 --- a/crates/app-iced/src/components/selection/paint/mod.rs +++ b/crates/app-iced/src/components/selection/paint/mod.rs @@ -109,10 +109,14 @@ pub struct ChipPaint { pub icon: Color, } -/// 状态层颜色:`content` 角色 @ 该态的 α 叠在 `surface` 上(不透明;α 的唯一实现点是 -/// [`interaction::overlay_color`],本目录零 α 字面量)。 -pub fn state_layer(ui: &Ui, state: Interaction, content: Role) -> Option { - interaction::overlay_color(ui, state, content, Role::Surface) +/// 状态层颜色:`content` 角色 @ 该态的 α 叠在 **`base`(控件脚下的底)** 上 +/// (不透明;α 的唯一实现点是 [`interaction::overlay_color`],本目录零 α 字面量)。 +/// +/// M234 复审 P2-3:禁用支跟了 `base`,hover/focus/pressed 这一支却仍写死 `Role::Surface` +/// —— 卡片里的 checkbox/radio/chip 抬起来时会把状态层叠在画布色上(口径不一致比那 1/255 的 +/// 数值更麻烦)。现在两支同源。 +pub fn state_layer(ui: &Ui, base: Base, state: Interaction, content: Role) -> Option { + interaction::overlay_color(ui, state, content, base.role()) } /// `OnSurface`@12% 叠 `base`(**容器**禁用色:Switch 轨道、Chip 未选中描边)。 diff --git a/crates/app-iced/src/components/selection/radio.rs b/crates/app-iced/src/components/selection/radio.rs index 86f079f..cc93b8e 100644 --- a/crates/app-iced/src/components/selection/radio.rs +++ b/crates/app-iced/src/components/selection/radio.rs @@ -127,7 +127,7 @@ pub fn radio<'a, M: Clone + 'a>(ui: &'a Ui, spec: Radio<'a, M>) -> Element<'a, M .align_y(Alignment::Center) .style(source.container_style(move |state| { Frame { - fill: paint::state_layer(ui, state, paint::radio_layer_role(selected)), + fill: paint::state_layer(ui, base, state, paint::radio_layer_role(selected)), outline: None, stroke: 0.0, radius: layer_side / 2.0, diff --git a/crates/app-iced/src/components/selection/tests/chip.rs b/crates/app-iced/src/components/selection/tests/chip.rs index b5b758b..b57a60a 100644 --- a/crates/app-iced/src/components/selection/tests/chip.rs +++ b/crates/app-iced/src/components/selection/tests/chip.rs @@ -77,14 +77,16 @@ fn state_layer_only_exists_on_lifted_states() { (Interaction::Pressed, true), (Interaction::Disabled, false), ] { - let layer = paint::state_layer(&ui, state, Role::OnSurfaceVariant); + let layer = paint::state_layer(&ui, Base::CANVAS, state, Role::OnSurfaceVariant); assert_eq!(layer.is_some(), expected, "{state:?} 的状态层有无"); if let Some(color) = layer { assert_eq!(color.a, 1.0, "状态层必须是实色(D27:CPU/GPU 一致)"); } } // 同态下 hover 比 pressed 浅(8% < 10%) - let hover = paint::state_layer(&ui, Interaction::Hovered, Role::OnSurface).unwrap(); - let pressed = paint::state_layer(&ui, Interaction::Pressed, Role::OnSurface).unwrap(); + let hover = + paint::state_layer(&ui, Base::CANVAS, Interaction::Hovered, Role::OnSurface).unwrap(); + let pressed = + paint::state_layer(&ui, Base::CANVAS, Interaction::Pressed, Role::OnSurface).unwrap(); assert_ne!(hover, pressed); } diff --git a/crates/app-iced/src/components/surfaces/elevated/tests.rs b/crates/app-iced/src/components/surfaces/elevated/tests.rs index b51be86..aebe9b9 100644 --- a/crates/app-iced/src/components/surfaces/elevated/tests.rs +++ b/crates/app-iced/src/components/surfaces/elevated/tests.rs @@ -75,3 +75,18 @@ fn the_card_surface_sits_one_step_above_the_canvas() { ); } } + +/// **M234**:卡片的底在代码里只出现两次——[`card`] 的 `background` 与导出的 [`BASE`], +/// 这两处必须同角色,否则卡里的禁用控件会合到另一层底上。 +/// +/// 抓得住:有人只改 `BASE`(或只改 `card`)导致两边漂开;抓不住:「`card()` 真的用了 +/// `BASE` 画底」这件事(`container::Style` 闭包不可内省,只能靠代码审阅与 +/// `gallery::probe` 的 disabledbase 段——那边的像素判据要求卡列里出现「叠卡片底」的禁用容器)。 +#[test] +fn the_card_base_constant_is_the_role_the_card_paints() { + assert_eq!( + super::BASE.role(), + Role::SurfaceContainer, + "`elevated::BASE` 必须就是卡底的角色(`card()` 的 background 用的是同一个值)" + ); +}