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

@@ -2,6 +2,9 @@
//!
//! 单独成文件的原因:`mod.rs` 已经是「对外契约 + 变体角色表」,把绘制细节放一起会顶到 400 行红线;
//! 而且这条产线是**唯一**会把「令牌 → 像素」翻一次的地方,独立出来才好审。
//!
//! 禁用态的合成底([`Base`],M234)也从这里往下传:`Button::base(..)` → [`StyleSpec::base`] →
//! `disabled_button_colors` / `disabled_button_content`。装配侧没有第二个猜底色的地方。
use iced::widget::text::LineHeight;
use iced::widget::{button, text, Row};
@@ -14,9 +17,9 @@ use unlucky_design::shape::ShapeScale;
use crate::icons;
use crate::theme::Ui;
use super::super::interaction::{self, Interaction};
use super::super::interaction::{self, Base, Interaction};
use super::super::layout;
use super::{Geometry, Variant};
use super::{Button, Geometry, Variant};
/// 一个按钮样式要的全部输入(避免 `style_closure` 长出七个位置参数)。
#[derive(Debug, Clone, Copy)]
@@ -33,6 +36,12 @@ pub(crate) struct StyleSpec {
pub stroke: f32,
/// 圆角折算用的边长(药丸 = 可见高)。
pub radius_side: f32,
/// **脚下那一层的底**(M234):禁用态的 10% 容器与 38% 内容都叠在它上面。
///
/// 由构造点给(卡片 = `surfaces::elevated::BASE`、菜单项 = `inputs::menu::PANEL_BASE`、
/// 直接摆在画布上 = [`Base::CANVAS`])。M229 之前这里恒为 `Role::Surface`,
/// 卡片里的禁用按钮因此把 10% 叠在画布色上(M223 的「形丢失」)。
pub base: Base,
/// 强制态(gallery 取证);`None` = 跟随运行期的 `button::Status`。
///
/// **`Some(Enabled)` 与 `None` 不是一回事**:截图取证必须能强制「静息」,否则光标恰好停在
@@ -45,15 +54,11 @@ pub(crate) struct StyleSpec {
pub(super) fn assemble<'a, M: Clone + 'a>(
ui: &'a Ui,
variant: Variant,
spec: super::Button<'a, M>,
spec: Button<'a, M>,
) -> Element<'a, M> {
let geometry = Geometry::sized(ui, variant, spec.leading.is_some(), spec.size);
let (fill, content, border) = variant.roles();
let forced = spec.state.or(if spec.on_press.is_none() {
Some(Interaction::Disabled)
} else {
None
});
let (fill, content, _border) = variant.roles();
let forced = spec.forced();
// 按钮家族没有浮层级高程:L1(Elevated)只用容器色调差表达(rubric §7 不画阴影)
debug_assert!(
@@ -67,7 +72,14 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
// 早先这里恒取 `ui.color(content)`,再被 `text(...).color(..)` 钉死,`Style.text_color`
// 里的 38% 永远下发不出去 —— 表现为「text / standard icon 的 enabled 格与 disabled 格
// 逐像素相同」(视觉 reviewer 实测 changed_px = 0;filled 禁用标签 1.23:1)。
let ink = content_color(ui, forced, content, fill, variant.disabled_content());
let ink = content_color(
ui,
spec.base,
forced,
content,
fill,
variant.disabled_content(),
);
// 标签排版跟尺寸档走:`ControlSize::label()` 已把「字号跟字级、字重固定 500」定死
// (XS/S = LabelLarge 14、M = BodyLarge 16、L = HeadlineSmall 24、XL = HeadlineLarge 32)。
@@ -106,18 +118,7 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
.height(Length::Fill)
.align_y(iced::Alignment::Center);
let style = style_closure(
ui,
StyleSpec {
fill,
content,
border,
shape: variant.shape(),
stroke: geometry.stroke,
radius_side: geometry.height,
forced,
},
);
let style = style_closure(ui, text_style_spec(ui, variant, &spec));
let element = button(content_box)
.height(Length::Fixed(geometry.height))
@@ -130,6 +131,26 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
}
}
/// `Button` → 样式描述:**装配的唯一接线点**(`assemble` 与单测都走这里)。
///
/// 抽出来的理由与 [`super::icon_style_spec`] 同源:`button::Style` 是闭包、`Element` 是黑盒,
/// 两者都不可内省,所以「禁用态的底到底传了哪一个」只能在**结构体**上断言。
/// 写死一个 `StyleSpec` 再喂给 `style_closure` 的测试证明不了接线(M229 复审 P1-1 踩过)。
pub(super) fn text_style_spec<M>(ui: &Ui, variant: Variant, spec: &Button<'_, M>) -> StyleSpec {
let (fill, content, border) = variant.roles();
let geometry = Geometry::sized(ui, variant, spec.leading.is_some(), spec.size);
StyleSpec {
fill,
content,
border,
shape: variant.shape(),
stroke: geometry.stroke,
radius_side: geometry.height,
base: spec.base,
forced: spec.forced(),
}
}
/// 内容色(文字与图标共用):禁用态走令牌的 38%,其余态就是角色色。
///
/// 单独成函数是为了让「内容色跟态走」这件事只有一处实现——`assemble`(文字按钮)与 `icon`
@@ -139,19 +160,20 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
/// 其余是 `OnSurfaceVariant`,不能在这里统一写死。
pub(super) fn content_color(
ui: &Ui,
base: Base,
forced: Option<Interaction>,
content: Role,
fill: Option<Role>,
disabled_content: Role,
) -> iced::Color {
match forced {
// 有容器:38% 叠在**按钮家族 10% 容器**上(Filled / Tonal / Elevated)
// 有容器:38% 叠在**按钮家族 10% 容器**上(Filled / Tonal / Elevated),容器再叠在 `base` 上
Some(Interaction::Disabled) if fill.is_some() => {
interaction::disabled_button_content(ui, disabled_content, true)
interaction::disabled_button_content(ui, base, disabled_content, true)
}
// 无容器:38% 直接叠在 **surface** 上(Text / Outlined / standard IconButton)
// 无容器:38% 直接叠在 **base** 上(Text / Outlined / standard IconButton)
Some(Interaction::Disabled) => {
interaction::disabled_button_content(ui, disabled_content, false)
interaction::disabled_button_content(ui, base, disabled_content, false)
}
_ => ui.color(content),
}
@@ -168,28 +190,35 @@ pub(super) fn style_closure(
.forced
.unwrap_or_else(|| Interaction::from_status(status));
let (background, text_color) = if state.is_disabled() {
let (container, content) = interaction::disabled_button_colors(ui, Role::Surface);
// 透明容器在禁用态依然透明(官方只改内容透明度)
(spec.fill.map(|_| container), content)
} else {
// 有填充:状态层叠在**容器角色**上;透明容器:叠在表面色上(否则 hover 看不见)
let background = match spec.fill {
Some(role) => match state.layer() {
Some(_) => interaction::overlay_color(ui, state, spec.content, role)
.or(Some(ui.color(role))),
None => Some(ui.color(role)),
},
None => transparent_state_color(ui, state, spec.content),
};
(background, ui.color(spec.content))
// 禁用那一对颜色**只求值一次**:背景与描边用的是同一对值(早先调了两次)
let disabled = state
.is_disabled()
.then(|| interaction::disabled_button_colors(ui, spec.base));
let (background, text_color) = match disabled {
Some((container, content)) => {
// 透明容器在禁用态依然透明(官方只改内容透明度)
(spec.fill.map(|_| container), content)
}
None => {
// 有填充:状态层叠在**容器角色**上;透明容器:叠在表面色上(否则 hover 看不见)
let background = match spec.fill {
Some(role) => match state.layer() {
Some(_) => interaction::overlay_color(ui, state, spec.content, role)
.or(Some(ui.color(role))),
None => Some(ui.color(role)),
},
None => transparent_state_color(ui, state, spec.content),
};
(background, ui.color(spec.content))
}
};
let border_color = if state.is_disabled() {
interaction::disabled_button_colors(ui, Role::Surface).0
} else {
spec.border
.map_or_else(|| ui.color(Role::OutlineVariant), |role| ui.color(role))
let border_color = match disabled {
Some((container, _content)) => container,
None => spec
.border
.map_or_else(|| ui.color(Role::OutlineVariant), |role| ui.color(role)),
};
button::Style {
@@ -35,7 +35,7 @@ use unlucky_design::typography::{TypeRole, TypeStyle, Weight};
use crate::icons;
use crate::theme::Ui;
use super::super::interaction::{self, Interaction};
use super::super::interaction::{self, Base, Interaction};
use super::super::layout;
/// 扩展态右侧内距(px)。规格卡只量到起始 28(`FabSize::pad_start_px`),右侧没有量;
@@ -97,7 +97,7 @@ pub fn fab<'a, M: Clone + 'a>(ui: &'a Ui, spec: Fab<'a, M>) -> Element<'a, M> {
let ink = match forced {
// 禁用:38% 的内容色叠在容器上(与按钮族的容器档同一口径)
Some(Interaction::Disabled) => {
interaction::disabled_button_content(ui, Role::OnPrimaryContainer, true)
interaction::disabled_button_content(ui, Base::CANVAS, Role::OnPrimaryContainer, true)
}
_ => ui.color(Role::OnPrimaryContainer),
};
@@ -160,7 +160,7 @@ pub fn fab<'a, M: Clone + 'a>(ui: &'a Ui, spec: Fab<'a, M>) -> Element<'a, M> {
None => Some(ui.color(Role::PrimaryContainer)),
};
let text_color = if state.is_disabled() {
interaction::disabled_button_content(ui, Role::OnPrimaryContainer, true)
interaction::disabled_button_content(ui, Base::CANVAS, Role::OnPrimaryContainer, true)
} else {
ui.color(Role::OnPrimaryContainer)
};
@@ -0,0 +1,68 @@
//! IconButton 的四式(规格卡 `ICON BUTTONS / FILLED - TONAL - OUTLINED - PLAIN`)。
//!
//! M234 纯搬移:本文件原来只是 `buttons/mod.rs` 的第 166~225 行(`mod.rs` 已顶到 399 行,
//! 加 `.base(..)` 与 `icon_on` 之前必须先腾地方)。搬移**零行为改动**,`IconVariant` 的文档、
//! 角色表、A1 出处一字未改;对外路径由 `mod.rs` 的 `pub use` 保持为 `buttons::IconVariant`。
use unlucky_design::color::Role;
/// IconButton 的**四式**(规格卡 `ICON BUTTONS / FILLED - TONAL - OUTLINED - PLAIN`)。
///
/// 规格卡画了四颗:实心 / 色调 / 描边 / 纯图标。12b §2.5 只有前三式(Standard / Filled / Tonal),
/// M229 按规格卡把 Standard 拆成 **Plain**(纯图标)与 **Outlined**(1px 描边圆圈),
/// `Standard` 这个名字保留为 `Plain` 的别名语义,避免既有调用点全改。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IconVariant {
/// 纯图标(规格卡 `PLAIN`):容器透明、图标 `OnSurfaceVariant`(各态图标色不变,12b 2.4)。
Plain,
/// 实心:`Primary` + `OnPrimary`。
Filled,
/// Tonal:`SecondaryContainer` + `OnSecondaryContainer`。
Tonal,
/// 描边:透明容器 + 1px `Outline`(规格卡第三颗,M229 新增)。
Outlined,
}
impl IconVariant {
/// 旧名字的别名:`Standard` = [`IconVariant::Plain`](既有调用点不必改语义)。
#[allow(non_upper_case_globals)]
pub const Standard: IconVariant = IconVariant::Plain;
/// 四式全序(gallery 顺序 = 规格卡从左到右)。
pub const ALL: [IconVariant; 4] = [
IconVariant::Filled,
IconVariant::Tonal,
IconVariant::Outlined,
IconVariant::Plain,
];
/// 稳定名字。
pub const fn name(self) -> &'static str {
match self {
IconVariant::Plain => "plain",
IconVariant::Filled => "filled",
IconVariant::Tonal => "tonal",
IconVariant::Outlined => "outlined",
}
}
/// **禁用态图标色**:四式都是 `OnSurface` @0.38 —— A1
/// (`StandardIconButtonTokens.DisabledColor` / `FilledIconButtonTokens.DisabledColor`)。
pub(crate) const fn disabled_content(self) -> Role {
Role::OnSurface
}
/// 色彩角色三元组(同 [`Variant::roles`] 的口径)。
pub(crate) const fn roles(self) -> (Option<Role>, Role, Option<Role>) {
match self {
IconVariant::Plain => (None, Role::OnSurfaceVariant, None),
IconVariant::Filled => (Some(Role::Primary), Role::OnPrimary, None),
IconVariant::Tonal => (
Some(Role::SecondaryContainer),
Role::OnSecondaryContainer,
None,
),
IconVariant::Outlined => (None, Role::OnSurfaceVariant, Some(Role::Outline)),
}
}
}
+50 -64
View File
@@ -53,15 +53,17 @@ use unlucky_design::shape::ShapeScale;
use crate::icons;
use crate::theme::Ui;
use super::interaction::Interaction;
use super::interaction::{Base, Interaction};
use super::layout;
mod assemble;
mod fab;
mod geometry;
mod icon_variant;
pub use fab::{fab, Fab};
pub use geometry::{Geometry, IconGeometry};
pub use icon_variant::IconVariant;
use assemble::{assemble, style_closure, StyleSpec};
@@ -163,67 +165,6 @@ impl Variant {
}
}
/// IconButton 的**四式**(规格卡 `ICON BUTTONS / FILLED - TONAL - OUTLINED - PLAIN`)。
///
/// 规格卡画了四颗:实心 / 色调 / 描边 / 纯图标。12b §2.5 只有前三式(Standard / Filled / Tonal),
/// M229 按规格卡把 Standard 拆成 **Plain**(纯图标)与 **Outlined**(1px 描边圆圈),
/// `Standard` 这个名字保留为 `Plain` 的别名语义,避免既有调用点全改。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IconVariant {
/// 纯图标(规格卡 `PLAIN`):容器透明、图标 `OnSurfaceVariant`(各态图标色不变,12b 2.4)。
Plain,
/// 实心:`Primary` + `OnPrimary`。
Filled,
/// Tonal:`SecondaryContainer` + `OnSecondaryContainer`。
Tonal,
/// 描边:透明容器 + 1px `Outline`(规格卡第三颗,M229 新增)。
Outlined,
}
impl IconVariant {
/// 旧名字的别名:`Standard` = [`IconVariant::Plain`](既有调用点不必改语义)。
#[allow(non_upper_case_globals)]
pub const Standard: IconVariant = IconVariant::Plain;
/// 四式全序(gallery 顺序 = 规格卡从左到右)。
pub const ALL: [IconVariant; 4] = [
IconVariant::Filled,
IconVariant::Tonal,
IconVariant::Outlined,
IconVariant::Plain,
];
/// 稳定名字。
pub const fn name(self) -> &'static str {
match self {
IconVariant::Plain => "plain",
IconVariant::Filled => "filled",
IconVariant::Tonal => "tonal",
IconVariant::Outlined => "outlined",
}
}
/// **禁用态图标色**:四式都是 `OnSurface` @0.38 —— A1
/// (`StandardIconButtonTokens.DisabledColor` / `FilledIconButtonTokens.DisabledColor`)。
pub(crate) const fn disabled_content(self) -> Role {
Role::OnSurface
}
/// 色彩角色三元组(同 [`Variant::roles`] 的口径)。
pub(crate) const fn roles(self) -> (Option<Role>, Role, Option<Role>) {
match self {
IconVariant::Plain => (None, Role::OnSurfaceVariant, None),
IconVariant::Filled => (Some(Role::Primary), Role::OnPrimary, None),
IconVariant::Tonal => (
Some(Role::SecondaryContainer),
Role::OnSecondaryContainer,
None,
),
IconVariant::Outlined => (None, Role::OnSurfaceVariant, Some(Role::Outline)),
}
}
}
/// 一个按钮的装配描述(五式共用;**不持有任何状态所有权**)。
///
/// `on_press: Option<M>` 就是 M3 的 enabled/disabled 开关——`None` = 禁用,
@@ -238,6 +179,8 @@ pub struct Button<'a, M> {
state: Option<Interaction>,
/// 尺寸档(规格卡 `BUTTONS / XS - XL`)。产品默认 **S(40 高)**,与 M3 `control_height` 同值。
size: ControlSize,
/// **脚下那一层的底**(M234):禁用态的 10% 容器与 38% 内容叠在它上面。
base: Base,
}
impl<'a, M> Button<'a, M> {
@@ -250,6 +193,7 @@ impl<'a, M> Button<'a, M> {
trailing: None,
state: None,
size: ControlSize::S,
base: Base::CANVAS,
}
}
@@ -262,6 +206,7 @@ impl<'a, M> Button<'a, M> {
trailing: None,
state: None,
size: ControlSize::S,
base: Base::CANVAS,
}
}
@@ -291,6 +236,28 @@ impl<'a, M> Button<'a, M> {
self.state = Some(state);
self
}
/// **告诉按钮它现在踩在什么底上**(M234):禁用态的 10% 容器与 38% 内容都叠在这个底上。
///
/// 放进卡片/面板/浮层里**必须**给——不给就按 [`Base::CANVAS`] 合成,禁用态会与容器底
/// 脱节(M223 的「CR 1.05、形丢失」)。容器自己的 `BASE` 常量就是标准答案:
/// 卡片 = [`super::surfaces::elevated::BASE`]、菜单项 = `super::inputs::menu::PANEL_BASE`。
///
/// iced 没有环境上下文(样式闭包只拿得到 `&Theme`),所以这条信息只能显式传:
/// 这是「组件不猜宿主」的代价,也是 M229 那条「参数在、全传死值」的修法。
pub fn base(mut self, base: impl Into<Base>) -> Self {
self.base = base.into();
self
}
/// 视觉态:强制的先,否则「没有消息就是禁用」。
pub(super) fn forced(&self) -> Option<Interaction> {
self.state.or(if self.on_press.is_none() {
Some(Interaction::Disabled)
} else {
None
})
}
}
/// Filled 按钮(实心 `Primary`)。
@@ -337,6 +304,7 @@ pub(crate) fn icon_style_spec(
variant: IconVariant,
side: f32,
forced: Option<Interaction>,
base: Base,
) -> StyleSpec {
let (fill, content, border) = variant.roles();
StyleSpec {
@@ -348,6 +316,7 @@ pub(crate) fn icon_style_spec(
// M229 复审 P1-1:这里曾硬编码 `0.0`,于是 `Outlined` 与 `Plain` 逐像素相同。
stroke: icon_stroke(variant),
radius_side: side,
base,
forced,
}
}
@@ -360,6 +329,8 @@ pub(crate) fn icon_style_spec(
/// gallery 的样例因此统一用描边图标(`Download` + `ChevronRight`),并有单测钉住。
///
/// `on_press: Option<M>`(`None` = 禁用)与 `state` 分开给:前者是语义,后者是取证开关。
///
/// 这是 **画布版**(禁用底 = [`Base::CANVAS`]):放进卡片/面板里请用 [`icon_on`] 给真实父底。
pub fn icon<'a, M: Clone + 'a>(
ui: &'a Ui,
variant: IconVariant,
@@ -367,6 +338,20 @@ pub fn icon<'a, M: Clone + 'a>(
on_press: Option<M>,
state: Option<Interaction>,
) -> Element<'a, M> {
icon_on(ui, variant, kind, on_press, state, Base::CANVAS)
}
/// 带**父底**的 IconButton(M234):禁用态的 10% 容器与 38% 内容叠在 `base` 上。
#[allow(clippy::too_many_arguments)]
pub fn icon_on<'a, M: Clone + 'a>(
ui: &'a Ui,
variant: IconVariant,
kind: IconKind,
on_press: Option<M>,
state: Option<Interaction>,
base: impl Into<Base>,
) -> Element<'a, M> {
let base = base.into();
let geometry = IconGeometry::of(ui);
let (fill, content, _) = variant.roles();
let forced = state.or(if on_press.is_some() {
@@ -380,11 +365,12 @@ pub fn icon<'a, M: Clone + 'a>(
// 这里把盒子做成整个 40×40(而不是 24×24):`button::layout` 把内容放左上角、不居中,
// 所以「居中」这件事必须由容器自己写死,别指望 button。
// 内容色跟态走(禁用 = on-surface@38%),见 `assemble::content_color` 的说明
let glyph = assemble::content_color(ui, forced, content, fill, variant.disabled_content());
let glyph =
assemble::content_color(ui, base, forced, content, fill, variant.disabled_content());
let icon_box =
layout::centered_fixed(geometry.side, geometry.side, icons::view::<M>(kind, glyph));
let style = style_closure(ui, icon_style_spec(variant, geometry.side, forced));
let style = style_closure(ui, icon_style_spec(variant, geometry.side, forced, base));
let element = button(icon_box)
.width(Length::Fixed(geometry.side))
@@ -11,7 +11,7 @@ use unlucky_design::elevation::Elevation;
use unlucky_design::icons::IconKind;
use unlucky_design::spacing::Space;
use super::super::interaction::{self, Interaction};
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};
@@ -122,6 +122,7 @@ fn icon_button_geometry_matches_the_reference() {
);
}
mod base;
mod icon;
/// 视觉 reviewer P2-3:同一控件里不许混用填充字形与描边字形(线宽语汇会打架)。
@@ -255,6 +256,7 @@ fn spec_of(variant: Variant, forced: Option<Interaction>) -> StyleSpec {
content,
border,
shape: variant.shape(),
base: Base::CANVAS,
stroke: Geometry::of(&light(), variant, false).stroke,
radius_side: Geometry::of(&light(), variant, false).height,
forced,
@@ -283,8 +285,7 @@ fn outlined_style_draws_hairline_border_and_full_radius() {
assert_ne!(hovered.background, idle.background);
let disabled = style(&ui.theme(), button::Status::Disabled);
let (container, content) =
interaction::disabled_button_colors(&ui, unlucky_design::color::Role::Surface);
let (container, content) = interaction::disabled_button_colors(&ui, Base::CANVAS);
assert_eq!(disabled.text_color, content, "禁用内容 = 令牌 38%");
assert_eq!(
disabled.border.color, container,
@@ -0,0 +1,75 @@
//! 禁用态**合成底**的接线判据(M234;从 `buttons/tests.rs` 拆出,守 400 行硬线)。
//!
//! 判据走真装配路径(`Button::base` → `text_style_spec` → `style_closure`),
//! 不是「自己拼一个 `StyleSpec` 再喂进去」——后者证明不了接线(M229 复审 P1-1 的教训)。
use iced::widget::button;
use iced::Background;
use unlucky_design::color::Role;
use unlucky_design::color::SchemeFlavor;
use super::super::Button;
use super::{light, style_closure, Variant};
use crate::app::components::interaction::Base;
use crate::app::components::surfaces;
use crate::theme::to_iced;
/// **M234 的接线判据**:禁用按钮的容器色必须在**它被告诉的那层底**上合成。
///
/// 走的是**真装配路径**:`Button::base(..)` → `text_style_spec`(接线)→ `style_closure`
/// (求值)。所以这条红了只有两种原因:① `assemble` 没把 `spec.base` 传下去;
/// ② `disabled_button_colors` 内部又写死了画布色。
///
/// 抓得住:卡片/面板里的禁用按钮叠错底(M223 的「CR 1.05、形丢失」就是这么来的)。
/// 抓不住:调用点**忘了**调 `.base(..)`(编译期不报错,默认 `Base::CANVAS`)——那一半由
/// `gallery` 的像素 probe(`ULL_GALLERY_SECTION=disabledbase`)与卡片场景守。
#[test]
fn a_disabled_button_composites_over_the_base_it_is_given() {
use unlucky_design::color::BrandBoard;
let ui = light();
let card = surfaces::elevated::BASE;
let spec = super::super::assemble::text_style_spec(
&ui,
Variant::Filled,
&Button::<u8>::disabled("卡片里"),
);
assert_eq!(
spec.base,
Base::CANVAS,
"不调 `.base(..)` 时是画布底(默认值,语义写在 `Base::CANVAS` 的文档里)"
);
let card_spec = super::super::assemble::text_style_spec(
&ui,
Variant::Filled,
&Button::<u8>::disabled("卡片里").base(card),
);
assert_eq!(
card_spec.base, card,
"接线:`Button::base` 必须原样进 `StyleSpec`"
);
let card_style = style_closure(&ui, card_spec)(&ui.theme(), button::Status::Disabled);
let canvas_style = style_closure(&ui, spec)(&ui.theme(), button::Status::Disabled);
// 期望值独立算(写死 0.10 + 直接取色板角色),不调被测入口。
let scheme = BrandBoard::for_flavor(SchemeFlavor::Light).scheme();
let expected_card = to_iced(
scheme
.get(Role::OnSurface)
.with_alpha(0.10)
.over(scheme.get(Role::SurfaceContainer)),
);
assert_eq!(
card_style.background,
Some(Background::Color(expected_card)),
"卡片里的禁用 Filled 按钮容器 = on-surface@10% 叠 **SurfaceContainer**"
);
assert_ne!(
card_style.background, canvas_style.background,
"卡片底与画布底的合成结果必须不同(叠在画布上就是 M234 要修的那条)"
);
}
@@ -38,7 +38,7 @@ fn only_the_outlined_icon_variant_draws_a_ring() {
);
// **接线断言**:`icon()` 真正塞进 `style_closure` 的那个结构体(`icon_style_spec`)。
// 只测 `icon_stroke()` 抓不住「调用点写死 0」——第一版就是这样漏的(变异测试证明过)。
let spec = icon_style_spec(variant, 40.0, None);
let spec = icon_style_spec(variant, 40.0, None, Base::CANVAS);
assert_eq!(
spec.stroke,
icon_stroke(variant),