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 {