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

+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))