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:
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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
use iced::widget::{button, text};
|
||||
use iced::{Background, Border, Element, Length, Padding, Shadow};
|
||||
|
||||
use unlucky_design::color::state::DISABLED_CONTENT_ALPHA;
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::component_map::Component;
|
||||
use unlucky_design::icons::IconKind;
|
||||
@@ -28,7 +27,7 @@ use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
|
||||
use crate::icons;
|
||||
use crate::theme::{to_iced, Ui};
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::interaction::{self, Interaction};
|
||||
use super::super::layout;
|
||||
@@ -43,13 +42,14 @@ fn content_color(ui: &Ui, role: Role, container_role: Role, disabled: bool) -> i
|
||||
if !disabled {
|
||||
return ui.color(role);
|
||||
}
|
||||
// 取 `Ui` 的当前方案(品牌板 / 取色方案),不重算 baseline(复审 F1)。
|
||||
let scheme = ui.scheme();
|
||||
to_iced(
|
||||
scheme
|
||||
.get(Role::OnSurface)
|
||||
.with_alpha(DISABLED_CONTENT_ALPHA)
|
||||
.over(scheme.get(container_role)),
|
||||
// M234:`disabled_content_over_role` 现在收**任意**底(`Base`),所以这里的本地合成可以删掉
|
||||
// ——底就是这两个动作件自己的容器角色(Snackbar = InverseSurface / Dialog = SurfaceContainerHigh),
|
||||
// α 与叠法仍由 `interaction::disabled` 那一处给(本文件零 α 字面量)。
|
||||
interaction::disabled_content_over_role(
|
||||
ui,
|
||||
interaction::Base::of(container_role),
|
||||
Role::OnSurface,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use unlucky_design::typography::Platform;
|
||||
|
||||
use crate::theme::{to_iced, Families, Ui};
|
||||
|
||||
use super::super::interaction::{self, Interaction};
|
||||
use super::super::interaction::{self, Base, Interaction};
|
||||
use super::dialog::scrim;
|
||||
use super::panel::{DialogGeometry, EmptyStateGeometry, SnackbarGeometry};
|
||||
use super::progress::Progress;
|
||||
@@ -235,7 +235,8 @@ fn snackbar_state_layers_come_from_the_interaction_module() {
|
||||
);
|
||||
|
||||
// 禁用内容走 token 口径(38%),不是「变灰一点」
|
||||
let disabled = interaction::disabled_content_over_role(&ui, Role::OnSurface, false);
|
||||
let disabled =
|
||||
interaction::disabled_content_over_role(&ui, Base::CANVAS, Role::OnSurface, false);
|
||||
assert_ne!(disabled, ui.color(Role::OnSurface));
|
||||
assert_eq!(disabled.a, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
//! 禁用态的**合成底**(M234):同一批禁用控件摆在三种底上,一眼看出「叠的是哪一层」。
|
||||
//!
|
||||
//! ## 这张图回答什么
|
||||
//!
|
||||
//! M223 记的「CR 1.05、形丢失」是这么来的:`disabled.rs` 的入口收一个 `Role` 当合成底,
|
||||
//! 而**所有调用点都传死 `Role::Surface`**——卡片(`SurfaceContainer`)与面板
|
||||
//! (`SurfaceContainerLow`)里的禁用控件于是把 10%(按钮族)/ 12%(面状控件族)那一层叠在
|
||||
//! **画布色**上,与它真正的底差一层。
|
||||
//!
|
||||
//! 这里把三列**并排**摆出来,每列一块板、板底就是控件真踩的那层底:
|
||||
//!
|
||||
//! ```text
|
||||
//! canvas Surface ← 页面底色
|
||||
//! card SurfaceContainer ← `surfaces::elevated::card` 的底(`elevated::BASE`)
|
||||
//! panel SurfaceContainerLow ← Card 层(取景板 / 面板)
|
||||
//! ```
|
||||
//!
|
||||
//! 每列都是**同一族控件**:Filled/Outlined 禁用按钮、Filled 禁用字段、禁用开关与 chip、
|
||||
//! 禁用搜索条、禁用列表项、禁用滑块、禁用 tab。控件拿的都是这一列的 [`Base`],
|
||||
//! 所以列与列之间的差别只有「底」。
|
||||
//!
|
||||
//! ## 判据(数值在 `probe/base.rs`)
|
||||
//!
|
||||
//! 像素判据判的是**精确值**:卡列里的禁用按钮容器必须等于 `on-surface@10% 叠卡片底`,
|
||||
//! 且**不等于**画布底那一档。注意量级:Ink/Silver 板上 `Surface` 与 `SurfaceContainer`
|
||||
//! 只差个位数 RGB,10% 之后差 **≈1/255**——肉眼看不出来,所以这张图的用处是**取证**,
|
||||
//! 不是「看出区别」。真判据在 `interaction::disabled::tests`(逐值)与 `probe/base.rs`(像素)。
|
||||
//!
|
||||
//! ```text
|
||||
//! ULL_GALLERY_SECTION=disabledbase $EXE --software-renderer --screen=gallery \
|
||||
//! --screenshot out.png --screenshot-theme=light
|
||||
//! ULL_GALLERY_SECTION=disabledbase ULL_GALLERY_SHOT=out.png \
|
||||
//! cargo test -p unlucky-app-iced gallery::probe
|
||||
//! ```
|
||||
|
||||
use iced::widget::{column, container, row, text};
|
||||
use iced::{Background, Border, Element, Length, Padding};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::buttons::{self, Button};
|
||||
use super::super::inputs::{self, Field, SearchBar, Slider};
|
||||
use super::super::interaction::{self, Base, Interaction};
|
||||
use super::super::navigation::{self, ListItem, Tab, Tabs};
|
||||
use super::super::selection::{self, CheckValue, Checkbox, Chip, Radio, Switch};
|
||||
use super::{heading, DemoMessage};
|
||||
|
||||
/// 一列的底(**板画什么,控件就叠什么**)。
|
||||
const COLUMNS: [(&str, Base); 3] = [
|
||||
("canvas · Surface", Base::CANVAS),
|
||||
(
|
||||
"card · SurfaceContainer",
|
||||
super::super::surfaces::elevated::BASE,
|
||||
),
|
||||
(
|
||||
"panel · SurfaceContainerLow",
|
||||
Base::of(Role::SurfaceContainerLow),
|
||||
),
|
||||
];
|
||||
|
||||
/// 板宽:3 × 360 + 2 × 8 间距 = 1096 < 1180 − 2×24 页边距。
|
||||
const PLATE_WIDTH: f32 = 360.0;
|
||||
|
||||
/// `#RRGGBB`(图上的色值要能直接和 probe 的期望值对上)。
|
||||
fn hex(color: iced::Color) -> String {
|
||||
let channel = |v: f32| (v * 255.0).round() as u8;
|
||||
format!(
|
||||
"#{:02X}{:02X}{:02X}",
|
||||
channel(color.r),
|
||||
channel(color.g),
|
||||
channel(color.b)
|
||||
)
|
||||
}
|
||||
|
||||
/// 整段(标题 + 三块并排的板)。
|
||||
pub(super) fn section<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
|
||||
let note = ui.style(TypeRole::BodySmall);
|
||||
let columns = COLUMNS
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|(title, base)| plate(ui, title, base))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
column![
|
||||
heading(
|
||||
ui,
|
||||
"disabled · 合成底 = 所在容器的底(M234)",
|
||||
"10%(按钮族)/ 12%(面状控件族)/ 38%(内容)全部预合成成实色;\
|
||||
三列控件相同,只有**脚下的底**不同",
|
||||
),
|
||||
row(columns).spacing(ui.space(Space::X2)),
|
||||
text(
|
||||
"三列的控件完全相同,只有脚下的底不同。禁用容器保留 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))
|
||||
.color(ui.color(Role::OnSurfaceVariant)),
|
||||
]
|
||||
.spacing(ui.space(Space::X2))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 一块板:**底色就是这一列的 `base`**,控件全部把同一个 `base` 传进 `.base(..)`。
|
||||
///
|
||||
/// 板上写着这一列**算出来的**色值(同一入口:`disabled_button_colors` / `disabled_colors`):
|
||||
/// 三列只差 ≈1/255,肉眼判不了,图上的数字是给 reviewer 与 probe 对账用的。
|
||||
fn plate<'a>(ui: &'a Ui, title: &'a str, base: Base) -> Element<'a, DemoMessage> {
|
||||
let caption = ui.style(TypeRole::BodySmall);
|
||||
let label = text(title)
|
||||
.size(caption.size_px)
|
||||
.font(ui.font_of(caption))
|
||||
.color(ui.color(Role::OnSurface));
|
||||
let numbers = text(format!(
|
||||
"底 {} · 禁用按钮(10%) {} · 禁用字段(12%) {}",
|
||||
hex(ui.color(base.role())),
|
||||
hex(interaction::disabled_button_colors(ui, base).0),
|
||||
hex(interaction::disabled_colors(ui, base).0),
|
||||
))
|
||||
.size(caption.size_px)
|
||||
.font(ui.font_of(caption))
|
||||
.color(ui.color(Role::OnSurfaceVariant));
|
||||
|
||||
container(
|
||||
column![
|
||||
column![label, numbers].spacing(ui.space(Space::X1)),
|
||||
column_body(ui, base)
|
||||
]
|
||||
.spacing(ui.space(Space::X2)),
|
||||
)
|
||||
.width(Length::Fixed(PLATE_WIDTH))
|
||||
.padding(Padding::from(ui.space(Space::X2)))
|
||||
.style(move |_theme| container::Style {
|
||||
// 板底 = 这一列的 base;描边只为让 `canvas` 列也有边界(画布色与页面同色)。
|
||||
background: Some(Background::Color(ui.color(base.role()))),
|
||||
border: Border {
|
||||
color: ui.color(Role::OutlineVariant),
|
||||
width: crate::theme::STROKE_PX,
|
||||
radius: ui.radius_dp(unlucky_design::shape::ShapeScale::Medium),
|
||||
},
|
||||
..container::Style::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 一列控件:**同族不同底**,每条都显式传 `base`。
|
||||
fn column_body<'a>(ui: &'a Ui, base: Base) -> Element<'a, DemoMessage> {
|
||||
let gap = ui.space(Space::X2);
|
||||
column![
|
||||
row![
|
||||
buttons::filled(ui, Button::disabled("保存").base(base)),
|
||||
buttons::outlined(ui, Button::disabled("取消").base(base)),
|
||||
]
|
||||
.spacing(gap),
|
||||
inputs::text_field(
|
||||
ui,
|
||||
Field::new("")
|
||||
.label("名称")
|
||||
.placeholder("禁用字段")
|
||||
.state(Interaction::Disabled)
|
||||
.base(base),
|
||||
|_| DemoMessage::Pressed("field"),
|
||||
),
|
||||
row![
|
||||
selection::checkbox(
|
||||
ui,
|
||||
Checkbox::disabled("勾选", CheckValue::Unchecked).base(base),
|
||||
),
|
||||
selection::radio(ui, Radio::disabled("单选", false).base(base)),
|
||||
selection::switch(ui, Switch::disabled("开关", false).base(base)),
|
||||
selection::chip(
|
||||
ui,
|
||||
Chip::filter("标签", false, DemoMessage::Pressed("chip"))
|
||||
.disabled()
|
||||
.base(base),
|
||||
),
|
||||
]
|
||||
.spacing(gap),
|
||||
inputs::search_bar(
|
||||
ui,
|
||||
SearchBar::new("")
|
||||
.placeholder("搜索")
|
||||
.state(Interaction::Disabled)
|
||||
.base(base),
|
||||
|_| DemoMessage::Pressed("search"),
|
||||
),
|
||||
navigation::list_item(
|
||||
ui,
|
||||
ListItem::new("列表项", DemoMessage::Pressed("item"))
|
||||
.disabled()
|
||||
.base(base),
|
||||
),
|
||||
inputs::slider(
|
||||
Slider::new(ui, 0.45)
|
||||
.state(Interaction::Disabled)
|
||||
.base(base),
|
||||
),
|
||||
navigation::tabs(
|
||||
ui,
|
||||
Tabs::primary()
|
||||
.tab(Tab::new("标签页", DemoMessage::Pressed("tab")).state(Interaction::Disabled),)
|
||||
.base(base),
|
||||
),
|
||||
]
|
||||
.spacing(gap)
|
||||
.into()
|
||||
}
|
||||
@@ -80,6 +80,7 @@ use super::interaction::Interaction;
|
||||
use super::layout;
|
||||
|
||||
mod buttons;
|
||||
mod disabledbase;
|
||||
mod feedback;
|
||||
mod inputs;
|
||||
mod navigation;
|
||||
@@ -97,6 +98,7 @@ pub const SECTION_ENV: &str = "ULL_GALLERY_SECTION";
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct Sections {
|
||||
buttons: bool,
|
||||
disabledbase: bool,
|
||||
sizes: bool,
|
||||
divider: bool,
|
||||
selection: bool,
|
||||
@@ -109,6 +111,7 @@ impl Sections {
|
||||
/// 七段全渲染(默认)。
|
||||
const ALL: Self = Self {
|
||||
buttons: true,
|
||||
disabledbase: true,
|
||||
sizes: true,
|
||||
divider: true,
|
||||
selection: true,
|
||||
@@ -121,6 +124,7 @@ impl Sections {
|
||||
fn only(name: &str) -> Option<Self> {
|
||||
let mut sections = Self {
|
||||
buttons: false,
|
||||
disabledbase: false,
|
||||
sizes: false,
|
||||
divider: false,
|
||||
selection: false,
|
||||
@@ -130,6 +134,7 @@ impl Sections {
|
||||
};
|
||||
match name {
|
||||
"buttons" => sections.buttons = true,
|
||||
"disabledbase" => sections.disabledbase = true,
|
||||
"sizes" => sections.sizes = true,
|
||||
"divider" => sections.divider = true,
|
||||
"selection" => sections.selection = true,
|
||||
@@ -156,7 +161,7 @@ impl Sections {
|
||||
return sections;
|
||||
}
|
||||
eprintln!(
|
||||
"[gallery] {SECTION_ENV}={raw:?} 不是已知段(all|buttons|sizes|divider|selection|inputs|feedback|navigation),已回落 all"
|
||||
"[gallery] {SECTION_ENV}={raw:?} 不是已知段(all|buttons|disabledbase|sizes|divider|selection|inputs|feedback|navigation),已回落 all"
|
||||
);
|
||||
Self::ALL
|
||||
}
|
||||
@@ -166,6 +171,7 @@ impl Sections {
|
||||
let mut names = Vec::new();
|
||||
for (on, name) in [
|
||||
(self.buttons, "buttons"),
|
||||
(self.disabledbase, "disabledbase"),
|
||||
(self.sizes, "sizes"),
|
||||
(self.divider, "divider"),
|
||||
(self.selection, "selection"),
|
||||
@@ -244,6 +250,9 @@ pub fn view<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
|
||||
if picked.buttons {
|
||||
sections = sections.push(buttons::section(ui));
|
||||
}
|
||||
if picked.disabledbase {
|
||||
sections = sections.push(disabledbase::section(ui));
|
||||
}
|
||||
if picked.sizes {
|
||||
sections = sections.push(sizes::section(ui));
|
||||
}
|
||||
|
||||
@@ -36,10 +36,11 @@ use unlucky_design::typography::Platform;
|
||||
|
||||
use crate::theme::{Families, Ui};
|
||||
|
||||
use crate::app::components::interaction;
|
||||
use crate::app::components::interaction::{self, Base};
|
||||
|
||||
use super::scan::{matches, nearer, rgb, Shot};
|
||||
|
||||
mod bases;
|
||||
mod rows;
|
||||
|
||||
const FAMILIES: Families = Families {
|
||||
@@ -146,14 +147,19 @@ fn gallery_geometry_matches_the_tokens() {
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
assert!(
|
||||
section == "buttons" || section == "divider",
|
||||
"这份 probe 只支持 `ULL_GALLERY_SECTION=buttons`(按钮矩阵)或 `divider`(分隔线);当前 {section:?}。`all` 不行:整页比视口高,分隔线段与按钮矩阵不可能同屏。"
|
||||
section == "buttons" || section == "divider" || section == "disabledbase",
|
||||
"这份 probe 只支持 `ULL_GALLERY_SECTION=buttons`(按钮矩阵)/ `divider`(分隔线)/ `disabledbase`(禁用态合成底);当前 {section:?}。`all` 不行:整页比视口高,分隔线段与按钮矩阵不可能同屏。"
|
||||
);
|
||||
// 分隔线那段是**独立出口**:`divider` 段的 sheet 里没有按钮矩阵,判据也不同。
|
||||
if section == "divider" {
|
||||
check_divider(&shot, &ui);
|
||||
return;
|
||||
}
|
||||
// 禁用态合成底(M234):三块板各按自己的底合成,判据在 `probe/base.rs`。
|
||||
if section == "disabledbase" {
|
||||
bases::check(&shot, &ui, flavor);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- 12b 1.1 / 1.2:实心按钮 40 高、药丸圆角 20 ------------------------------
|
||||
let primary = shot.bbox_of(rgb(ui.color(Role::Primary)));
|
||||
@@ -309,7 +315,7 @@ fn gallery_geometry_matches_the_tokens() {
|
||||
//
|
||||
// **按钮族**:容器 10%(官方 `*ButtonTokens.DisabledContainerOpacity = 0.1f`)、内容 38%。
|
||||
// M86 之前这里和 chip 共用同一个 12% 常量,所以断言与实现「一起错」、永远同源——拆族后才对得上。
|
||||
let (disabled_container, _) = interaction::disabled_button_colors(&ui, Role::Surface);
|
||||
let (disabled_container, _) = interaction::disabled_button_colors(&ui, Base::CANVAS);
|
||||
assert!(
|
||||
shot.bbox_of(rgb(disabled_container))
|
||||
.iter()
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
//! `ULL_GALLERY_SECTION=disabledbase` 的像素判据:**禁用态叠的是哪一层底**(M234)。
|
||||
//!
|
||||
//! ## 为什么必须用「精确匹配」而不是扫描工具的 `matches`(±10)
|
||||
//!
|
||||
//! Ink / Silver 板上 `Surface` 与 `SurfaceContainer` 只差个位数 RGB,10% 之后差 **≈1/255**:
|
||||
//! `scan::matches` 的 ±10 容差会把「叠在画布上」与「叠在卡片上」判成同一个颜色——
|
||||
//! 那样这条判据就永远绿。所以本文件自己数**逐位相等**的像素([`count_exact`]),
|
||||
//! 并且只数**板内**区域,避开页面底色。
|
||||
//!
|
||||
//! ## 量什么
|
||||
//!
|
||||
//! 1. 卡列 / 面板列里:禁用按钮容器(按钮族 10%)与禁用字段容器(面状族 12%)的像素值
|
||||
//! = 各自「@10%/12% 叠**该列板底**」;**不许**出现「叠画布底」那一档;
|
||||
//! 2. 画布列(卡列左侧、同样的纵向范围)里反过来:出现画布档、且不是卡/面板档;
|
||||
//! 3. 三档的期望值两两不同(哪一对相等,说明 `Base` 被丢了或两族 α 合流了)。
|
||||
//!
|
||||
//! ## 抓得住 / 抓不住
|
||||
//!
|
||||
//! - 抓得住:调用点把底传错(叠画布)或没传(默认 `Base::CANVAS`)——卡列里会出现画布档的像素
|
||||
//! 而缺卡片档;两族 α 合流、入口内部写死 `Role::Surface`,同样在这里红。
|
||||
//! - 抓不住:① 容器**自己**的画法(板底 vs 卡组件的 `BASE` 是否一致——那条由
|
||||
//! `surfaces::elevated::tests` 的「卡底必须是 SurfaceContainer」守);
|
||||
//! ② 页面里的真实卡片(pages/ 的调用点尚未接 `.base(..)`,见报告「没验的部分」);
|
||||
//! ③ GPU(wgpu) 档:本判据读的是 tiny-skia(`--software-renderer`) 出的图;
|
||||
//! ④ 差 <1/255 的第三层底(例如 `SurfaceContainer` 与 `SurfaceContainerHigh` 的 10%)——
|
||||
//! 那种情况本文件会退化成「两档相等」,第 3 条断言会红,**不会静默放过**。
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
|
||||
use super::super::scan::{rgb, Shot};
|
||||
use crate::app::components::interaction::{self, Base};
|
||||
use crate::app::components::surfaces;
|
||||
use crate::theme::Ui;
|
||||
|
||||
/// 板子的最小尺寸(判据只认「一整列」,不许把某个控件误当成板)。
|
||||
const MIN_PLATE_W: usize = 300;
|
||||
const MIN_PLATE_H: usize = 260;
|
||||
/// 禁用按钮容器在板内的最少像素(Filled 药丸 40 高 × ≥58 宽,扣掉字形与圆角)。
|
||||
const MIN_BUTTON_PX: usize = 300;
|
||||
/// 禁用字段容器的最少像素(板宽 ~336 × 56,扣掉标签与图标)。
|
||||
const MIN_FIELD_PX: usize = 3000;
|
||||
/// 「正确档位 : 错误档位」的最小倍数。
|
||||
///
|
||||
/// 为什么不干脆要求「错误档位 = 0 px」:**抗锯齿会造出中间色**——板底与控件容器之间那一圈
|
||||
/// 1px 过渡像素会顺路撞上「另一档」的精确值(实测卡片列里 226/226/227 有 58 px,全是边缘)。
|
||||
/// 真实回归的量级完全不同(整颗按钮 ≈3000 px、整块字段 ≈18000 px),所以判据写成**倍数**:
|
||||
/// 正确档位必须比错误档位多一个数量级。
|
||||
const STRAY_RATIO: usize = 10;
|
||||
|
||||
/// 逐位相等的像素计数器(`scan::matches` 的 ±10 容差在这里不够用,见模块文档)。
|
||||
fn count_exact(shot: &Shot, color: [u8; 3], x0: usize, x1: usize, y0: usize, y1: usize) -> usize {
|
||||
(y0..y1.min(shot.height))
|
||||
.map(|y| {
|
||||
(x0..x1.min(shot.width))
|
||||
.filter(|x| shot.at(*x, y) == color)
|
||||
.count()
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// 一块板(`left..right × top..bottom`,闭区间;`x1`/`y1` 是开区间上界)。
|
||||
type Rect = (usize, usize, usize, usize);
|
||||
|
||||
/// 按**逐位相等**的颜色切出竖列:每列 = 一段连续且够宽的 x,y 取该段内的最小/最大行。
|
||||
///
|
||||
/// 为什么不用 `scan::bbox_of`(±10 容差):本板的三档底(`Surface` / `SurfaceContainer` /
|
||||
/// `SurfaceContainerLow`)两两只差 5~11 个 RGB 单位,落在容差里会被并成一块
|
||||
/// (实测把卡片列与面板列判成同一块)。
|
||||
fn exact_columns(shot: &Shot, color: [u8; 3]) -> Vec<Rect> {
|
||||
let mut columns = Vec::new();
|
||||
let mut current: Option<(usize, usize, usize, usize)> = None;
|
||||
for x in 0..shot.width {
|
||||
let rows: Vec<usize> = (0..shot.height)
|
||||
.filter(|y| shot.at(x, *y) == color)
|
||||
.collect();
|
||||
match (&mut current, rows.first()) {
|
||||
(None, Some(top)) => {
|
||||
current = Some((x, x + 1, *top, rows[rows.len() - 1] + 1));
|
||||
}
|
||||
(Some(col), Some(top)) => {
|
||||
col.1 = x + 1;
|
||||
col.2 = col.2.min(*top);
|
||||
col.3 = col.3.max(rows[rows.len() - 1] + 1);
|
||||
}
|
||||
(Some(col), None) => {
|
||||
if col.1 - col.0 >= MIN_PLATE_W && col.3 - col.2 >= MIN_PLATE_H {
|
||||
columns.push(*col);
|
||||
}
|
||||
current = None;
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
}
|
||||
if let Some(col) = current {
|
||||
if col.1 - col.0 >= MIN_PLATE_W && col.3 - col.2 >= MIN_PLATE_H {
|
||||
columns.push(col);
|
||||
}
|
||||
}
|
||||
columns
|
||||
}
|
||||
|
||||
fn find_plate(shot: &Shot, role: Role, ui: &Ui, name: &str) -> Rect {
|
||||
let color = rgb(ui.color(role));
|
||||
let mut columns = exact_columns(shot, color);
|
||||
assert_eq!(
|
||||
columns.len(),
|
||||
1,
|
||||
"{name} 列({role:?} 底,{color:?})应当恰好是 1 列,实测 {}: 板子尺寸判据 MIN {MIN_PLATE_W}×{MIN_PLATE_H}",
|
||||
columns.len()
|
||||
);
|
||||
columns.remove(0)
|
||||
}
|
||||
|
||||
/// 判据入口(`probe.rs` 的 `disabledbase` 分支调它)。
|
||||
pub(super) fn check(shot: &Shot, ui: &Ui, flavor: unlucky_design::color::SchemeFlavor) {
|
||||
let card = Base::of(Role::SurfaceContainer);
|
||||
let panel = Base::of(Role::SurfaceContainerLow);
|
||||
|
||||
let card_rect = find_plate(shot, Role::SurfaceContainer, ui, "card");
|
||||
let panel_rect = find_plate(shot, Role::SurfaceContainerLow, ui, "panel");
|
||||
// 画布列:三列等宽等距(`disabledbase::PLATE_WIDTH` 固定 360、间距 8),所以它 =
|
||||
// 卡列左侧同样宽的一列。画布板的底与页面同色(`Surface`),**没法**靠颜色切出来。
|
||||
let step = panel_rect.0 - card_rect.0;
|
||||
assert!(
|
||||
step > MIN_PLATE_W,
|
||||
"两列间距 {step} 不合理(列宽 {MIN_PLATE_W} 起)—— 布局变了,画布列的镜像推算不再成立"
|
||||
);
|
||||
let canvas_rect = (0, card_rect.0, card_rect.2, card_rect.3);
|
||||
let canvas_left = card_rect.0.saturating_sub(step);
|
||||
|
||||
let button = |base: Base| rgb(interaction::disabled_button_colors(ui, base).0);
|
||||
let field = |base: Base| rgb(interaction::disabled_colors(ui, base).0);
|
||||
|
||||
// 三档必须两两不同(哪一对相等就说明底被丢了 / 两族 α 合流了)
|
||||
let (canvas_button, canvas_field) = (button(Base::CANVAS), field(Base::CANVAS));
|
||||
let (card_button, card_field) = (button(card), field(card));
|
||||
let (panel_button, panel_field) = (button(panel), field(panel));
|
||||
assert_ne!(
|
||||
canvas_button, card_button,
|
||||
"{flavor:?}:画布底与卡片底算出来的按钮容器相同 —— 底被丢了"
|
||||
);
|
||||
assert_ne!(
|
||||
canvas_field, card_field,
|
||||
"{flavor:?}:画布底与卡片底算出来的字段容器相同"
|
||||
);
|
||||
assert_ne!(
|
||||
card_button, panel_button,
|
||||
"{flavor:?}:卡片底与面板底必须可分"
|
||||
);
|
||||
assert_ne!(
|
||||
card_field, panel_field,
|
||||
"{flavor:?}:卡片底与面板底必须可分"
|
||||
);
|
||||
assert_eq!(
|
||||
surfaces::elevated::BASE,
|
||||
card,
|
||||
"卡片列用的底就该是 `surfaces::elevated::BASE`(容器导出的那个常量)"
|
||||
);
|
||||
|
||||
for (rect, base, wanted_button, wanted_field, name) in [
|
||||
(card_rect, card, card_button, card_field, "card"),
|
||||
(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);
|
||||
assert!(
|
||||
hits >= MIN_BUTTON_PX,
|
||||
"{name} 列里「叠 {:?}」的禁用按钮容器只有 {hits} px(应 ≥{MIN_BUTTON_PX}):\
|
||||
这一列的禁用控件没把真实父底传下去",
|
||||
base.role()
|
||||
);
|
||||
let hits = count_exact(shot, wanted_field, x0, x1, y0, y1);
|
||||
assert!(
|
||||
hits >= MIN_FIELD_PX,
|
||||
"{name} 列里「叠 {:?}」的禁用字段容器只有 {hits} px(应 ≥{MIN_FIELD_PX})",
|
||||
base.role()
|
||||
);
|
||||
// 反向:这一列里不许出现「叠画布底」的那一档
|
||||
let stray = count_exact(shot, canvas_button, x0, x1, y0, y1);
|
||||
assert!(
|
||||
hits > stray * STRAY_RATIO,
|
||||
"{name} 列里「叠 {:?}」的禁用按钮容器只有 {hits} px,而「叠画布」的有 {stray} px (要求前者 ≥ 后者 ×{STRAY_RATIO})—— 这一列的控件仍在按 Role::Surface 合成",
|
||||
base.role()
|
||||
);
|
||||
let stray = count_exact(shot, canvas_field, x0, x1, y0, y1);
|
||||
assert!(
|
||||
hits > stray * STRAY_RATIO,
|
||||
"{name} 列里「叠 {:?}」的禁用字段容器只有 {hits} px,而「叠画布」的有 {stray} px (要求前者 ≥ 后者 ×{STRAY_RATIO})",
|
||||
base.role()
|
||||
);
|
||||
assert!(
|
||||
base.role() != Role::Surface,
|
||||
"{name} 列不该是画布底(判据自己写错了)"
|
||||
);
|
||||
}
|
||||
|
||||
// 画布列:反过来必须出现画布档
|
||||
let (_, cx1, cy0, cy1) = canvas_rect;
|
||||
let cx0 = canvas_left;
|
||||
let canvas_hits = count_exact(shot, canvas_button, cx0, cx1, cy0, cy1);
|
||||
assert!(
|
||||
canvas_hits >= MIN_BUTTON_PX,
|
||||
"画布列里没有「叠画布」的禁用按钮容器 —— 对照组不成立"
|
||||
);
|
||||
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 —— 画布列的控件被传了卡片底"
|
||||
);
|
||||
|
||||
eprintln!(
|
||||
"[probe] OK section=disabledbase flavor={flavor:?}:\
|
||||
canvas 按钮 {:?} / 卡片 {} / 面板 {} ⇒ 三列各按自己的底合成",
|
||||
canvas_button, card_rect.0, panel_rect.0
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use unlucky_design::color::Role;
|
||||
|
||||
use super::super::scan::{rgb, Shot};
|
||||
use crate::app::components::interaction;
|
||||
use crate::app::components::interaction::Base;
|
||||
use crate::theme::Ui;
|
||||
|
||||
/// 矩阵行距(40 高按钮 + 8 间距)。
|
||||
@@ -38,7 +39,7 @@ pub(super) fn assert_matrix_rows(shot: &Shot, ui: &Ui, label_top: usize, label_l
|
||||
let y1 = y0 + ROW_HEIGHT;
|
||||
// **与实现同源**:按钮族的内容色一律走 `disabled_button_content`(38% 叠在 10% 底上)。
|
||||
// 用面状控件族的 12% 底会让断言比实现宽一档,差的 1/255 会放跑「容器对内容错」。
|
||||
let ink = interaction::disabled_button_content(ui, role, on_container);
|
||||
let ink = interaction::disabled_button_content(ui, Base::CANVAS, role, on_container);
|
||||
let count = shot.count_in(rgb(ink), column_from, shot.width, y0, y1);
|
||||
// 阈值 **30**:最窄的一行是只有 4 个字符的 `text` 变体(实测 38 px 墨迹,M229 复审后
|
||||
// 按钮内距 24→20 让标签左移,余量只剩 2 px)。角色接错时会掉到个位数甚至 0。
|
||||
|
||||
@@ -22,7 +22,6 @@ use iced::widget::text::LineHeight;
|
||||
use iced::widget::{column, container, row, text, text_input};
|
||||
use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Pixels};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::icons::IconKind;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
@@ -30,10 +29,16 @@ use unlucky_design::typography::TypeRole;
|
||||
use crate::icons;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::interaction::{self, Interaction};
|
||||
use super::super::interaction::{Base, Interaction};
|
||||
|
||||
#[cfg(test)]
|
||||
use super::super::interaction;
|
||||
use super::super::layout;
|
||||
use super::geometry::FieldGeometry;
|
||||
|
||||
mod paint;
|
||||
use paint::palette;
|
||||
|
||||
/// 两式容器(12b 9.3 / 9.4 的差别:Filled 有容器色 + 底部指示线;Outlined 只有描边)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum FieldVariant {
|
||||
@@ -70,6 +75,8 @@ pub struct Field<'a> {
|
||||
state: Option<Interaction>,
|
||||
width: Length,
|
||||
secure: bool,
|
||||
/// 脚下那一层的底(M234,禁用态合成用)。
|
||||
base: Base,
|
||||
}
|
||||
|
||||
impl<'a> Field<'a> {
|
||||
@@ -87,9 +94,19 @@ impl<'a> Field<'a> {
|
||||
state: None,
|
||||
width: Length::Fill,
|
||||
secure: false,
|
||||
base: Base::CANVAS,
|
||||
}
|
||||
}
|
||||
|
||||
/// **告诉字段它踩在什么底上**(M234):禁用态的 12% 容器 / 描边与 38% 内容都叠在这个底上。
|
||||
///
|
||||
/// 放进卡片/面板里必须给(卡片 = `surfaces::elevated::BASE`);不给就按画布合成,
|
||||
/// 禁用字段会与容器底脱节(M223 的「形丢失」)。
|
||||
pub fn base(mut self, base: impl Into<Base>) -> Self {
|
||||
self.base = base.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// 选式(Filled / Outlined)。
|
||||
pub fn variant(mut self, variant: FieldVariant) -> Self {
|
||||
self.variant = variant;
|
||||
@@ -151,106 +168,6 @@ impl<'a> Field<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 一个字段在某一态下的全套颜色(9.4 / 9.7 / 9.8 的落点)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
struct Palette {
|
||||
/// 容器底色;`None` = 透明(Outlined)。
|
||||
fill: Option<Color>,
|
||||
/// 描边/指示线色。
|
||||
line: Color,
|
||||
/// 描边/指示线宽(1 或 2)。
|
||||
stroke: f32,
|
||||
/// 标签(`BodySmall`)。
|
||||
label: Color,
|
||||
/// 输入文本。
|
||||
input: Color,
|
||||
/// 前后图标。
|
||||
icon: Color,
|
||||
/// 辅助文本。
|
||||
supporting: Color,
|
||||
/// 占位文本。
|
||||
placeholder: Color,
|
||||
}
|
||||
|
||||
/// 解析某一态的颜色(**唯一**做角色映射的地方)。
|
||||
fn palette(ui: &Ui, variant: FieldVariant, state: Interaction, error: bool) -> Palette {
|
||||
let geometry = FieldGeometry::of(ui, true, None);
|
||||
|
||||
if state.is_disabled() {
|
||||
// 9.8:内容一律 OnSurface@38%;Filled 容器叠 12%(官方 4%,令牌无 4% 档 → D10);
|
||||
// Outlined 描边正好是官方 12%。
|
||||
let container = interaction::disabled_colors(ui, Role::Surface).0;
|
||||
let ink = interaction::disabled_content_over_role(
|
||||
ui,
|
||||
Role::OnSurface,
|
||||
matches!(variant, FieldVariant::Filled),
|
||||
);
|
||||
return Palette {
|
||||
fill: match variant {
|
||||
FieldVariant::Filled => Some(container),
|
||||
FieldVariant::Outlined => None,
|
||||
},
|
||||
line: match variant {
|
||||
FieldVariant::Filled => ink,
|
||||
FieldVariant::Outlined => container,
|
||||
},
|
||||
stroke: geometry.stroke,
|
||||
label: ink,
|
||||
input: ink,
|
||||
icon: ink,
|
||||
supporting: ink,
|
||||
placeholder: ink,
|
||||
};
|
||||
}
|
||||
|
||||
let focused = matches!(state, Interaction::Focused);
|
||||
let hovered = matches!(state, Interaction::Hovered | Interaction::Pressed);
|
||||
|
||||
let (line, stroke) = if error {
|
||||
(ui.color(Role::Error), geometry.focus_stroke)
|
||||
} else if focused {
|
||||
(ui.color(Role::Primary), geometry.focus_stroke)
|
||||
} else if hovered {
|
||||
(ui.color(Role::OnSurface), geometry.stroke)
|
||||
} else {
|
||||
(
|
||||
match variant {
|
||||
FieldVariant::Filled => ui.color(Role::OnSurfaceVariant),
|
||||
FieldVariant::Outlined => ui.color(Role::Outline),
|
||||
},
|
||||
geometry.stroke,
|
||||
)
|
||||
};
|
||||
|
||||
Palette {
|
||||
fill: match variant {
|
||||
FieldVariant::Filled => Some(ui.color(Role::SurfaceContainerHighest)),
|
||||
FieldVariant::Outlined => None,
|
||||
},
|
||||
line,
|
||||
stroke,
|
||||
label: if error {
|
||||
ui.color(Role::Error)
|
||||
} else if focused {
|
||||
ui.color(Role::Primary)
|
||||
} else {
|
||||
ui.color(Role::OnSurfaceVariant)
|
||||
},
|
||||
input: ui.color(Role::OnSurface),
|
||||
icon: if error {
|
||||
ui.color(Role::Error)
|
||||
} else {
|
||||
ui.color(Role::OnSurfaceVariant)
|
||||
},
|
||||
supporting: if error {
|
||||
ui.color(Role::Error)
|
||||
} else {
|
||||
ui.color(Role::OnSurfaceVariant)
|
||||
},
|
||||
placeholder: ui.color(Role::OnSurfaceVariant),
|
||||
}
|
||||
}
|
||||
|
||||
/// 一个文本框(Filled / Outlined 由 [`Field::variant`] 选)。
|
||||
///
|
||||
/// 受控:值由外部持有,输入经 `on_input` 回传;`state` 强制成 `Disabled` 时输入被摘掉
|
||||
@@ -262,7 +179,7 @@ pub fn text_field<'a, M: Clone + 'a>(
|
||||
) -> Element<'a, M> {
|
||||
let geometry = FieldGeometry::of(ui, field.label.is_some(), field.leading);
|
||||
let state = field.state.unwrap_or(Interaction::Enabled);
|
||||
let colors = palette(ui, field.variant, state, field.error);
|
||||
let colors = palette(ui, field.base, field.variant, state, field.error);
|
||||
|
||||
let input_style = ui.style(TypeRole::BodyLarge);
|
||||
let label_style = ui.style(TypeRole::BodySmall);
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
//! 文本框的颜色求值(`research/12b` §9.4 / §9.7 / §9.8 的落点)。
|
||||
//!
|
||||
//! M234 纯搬移:本文件原来是 `inputs/field.rs` 的 `Palette` + `palette()`(`field.rs` 已顶到
|
||||
//! 398 行,加 `.base(..)` 之前必须腾地方)。搬移只做了一件事以外的零改动——`palette()` 多收
|
||||
//! 一个 [`Base`](禁用态的合成底,M234),其余角色映射一字未改。
|
||||
|
||||
use iced::Color;
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::super::interaction::{self, Base, Interaction};
|
||||
use super::super::geometry::FieldGeometry;
|
||||
use super::FieldVariant;
|
||||
|
||||
/// 一个字段在某一态下的全套颜色(9.4 / 9.7 / 9.8 的落点)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(super) struct Palette {
|
||||
/// 容器底色;`None` = 透明(Outlined)。
|
||||
pub(super) fill: Option<Color>,
|
||||
/// 描边/指示线色。
|
||||
pub(super) line: Color,
|
||||
/// 描边/指示线宽(1 或 2)。
|
||||
pub(super) stroke: f32,
|
||||
/// 标签(`BodySmall`)。
|
||||
pub(super) label: Color,
|
||||
/// 输入文本。
|
||||
pub(super) input: Color,
|
||||
/// 前后图标。
|
||||
pub(super) icon: Color,
|
||||
/// 辅助文本。
|
||||
pub(super) supporting: Color,
|
||||
/// 占位文本。
|
||||
pub(super) placeholder: Color,
|
||||
}
|
||||
|
||||
/// 解析某一态的颜色(**唯一**做角色映射的地方)。
|
||||
///
|
||||
/// `base` 是字段**脚下那一层的底**(M234):禁用态的 12% 容器与 38% 内容都叠在它上面
|
||||
/// (Filled 容器 / Outlined 描边 / 全部标签与文本)。调用点给真实父底,不写死 `Role::Surface`。
|
||||
pub(super) fn palette(
|
||||
ui: &Ui,
|
||||
base: Base,
|
||||
variant: FieldVariant,
|
||||
state: Interaction,
|
||||
error: bool,
|
||||
) -> Palette {
|
||||
let geometry = FieldGeometry::of(ui, true, None);
|
||||
|
||||
if state.is_disabled() {
|
||||
// 9.8:内容一律 OnSurface@38%;Filled 容器叠 12%(官方 4%,令牌无 4% 档 → D10);
|
||||
// Outlined 描边正好是官方 12%。
|
||||
let container = interaction::disabled_colors(ui, base).0;
|
||||
let ink = interaction::disabled_content_over_role(
|
||||
ui,
|
||||
base,
|
||||
Role::OnSurface,
|
||||
matches!(variant, FieldVariant::Filled),
|
||||
);
|
||||
return Palette {
|
||||
fill: match variant {
|
||||
FieldVariant::Filled => Some(container),
|
||||
FieldVariant::Outlined => None,
|
||||
},
|
||||
line: match variant {
|
||||
FieldVariant::Filled => ink,
|
||||
FieldVariant::Outlined => container,
|
||||
},
|
||||
stroke: geometry.stroke,
|
||||
label: ink,
|
||||
input: ink,
|
||||
icon: ink,
|
||||
supporting: ink,
|
||||
placeholder: ink,
|
||||
};
|
||||
}
|
||||
|
||||
let focused = matches!(state, Interaction::Focused);
|
||||
let hovered = matches!(state, Interaction::Hovered | Interaction::Pressed);
|
||||
|
||||
let (line, stroke) = if error {
|
||||
(ui.color(Role::Error), geometry.focus_stroke)
|
||||
} else if focused {
|
||||
(ui.color(Role::Primary), geometry.focus_stroke)
|
||||
} else if hovered {
|
||||
(ui.color(Role::OnSurface), geometry.stroke)
|
||||
} else {
|
||||
(
|
||||
match variant {
|
||||
FieldVariant::Filled => ui.color(Role::OnSurfaceVariant),
|
||||
FieldVariant::Outlined => ui.color(Role::Outline),
|
||||
},
|
||||
geometry.stroke,
|
||||
)
|
||||
};
|
||||
|
||||
Palette {
|
||||
fill: match variant {
|
||||
FieldVariant::Filled => Some(ui.color(Role::SurfaceContainerHighest)),
|
||||
FieldVariant::Outlined => None,
|
||||
},
|
||||
line,
|
||||
stroke,
|
||||
label: if error {
|
||||
ui.color(Role::Error)
|
||||
} else if focused {
|
||||
ui.color(Role::Primary)
|
||||
} else {
|
||||
ui.color(Role::OnSurfaceVariant)
|
||||
},
|
||||
input: ui.color(Role::OnSurface),
|
||||
icon: if error {
|
||||
ui.color(Role::Error)
|
||||
} else {
|
||||
ui.color(Role::OnSurfaceVariant)
|
||||
},
|
||||
supporting: if error {
|
||||
ui.color(Role::Error)
|
||||
} else {
|
||||
ui.color(Role::OnSurfaceVariant)
|
||||
},
|
||||
placeholder: ui.color(Role::OnSurfaceVariant),
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//! field 的单测(从 `field.rs` 拆出:单文件 ≤400 行,AGENTS.md §3-1)。
|
||||
|
||||
use super::*;
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_design::typography::Platform;
|
||||
|
||||
@@ -44,23 +45,47 @@ fn every_variant_and_state_builds() {
|
||||
#[test]
|
||||
fn outline_colors_and_widths_follow_the_state() {
|
||||
let ui = ui(SchemeFlavor::Light);
|
||||
let rest = palette(&ui, FieldVariant::Outlined, Interaction::Enabled, false);
|
||||
let rest = palette(
|
||||
&ui,
|
||||
Base::CANVAS,
|
||||
FieldVariant::Outlined,
|
||||
Interaction::Enabled,
|
||||
false,
|
||||
);
|
||||
assert_eq!(rest.line, ui.color(Role::Outline));
|
||||
assert_eq!(rest.stroke, 1.0);
|
||||
assert_eq!(rest.fill, None, "Outlined 无容器色");
|
||||
|
||||
let hover = palette(&ui, FieldVariant::Outlined, Interaction::Hovered, false);
|
||||
let hover = palette(
|
||||
&ui,
|
||||
Base::CANVAS,
|
||||
FieldVariant::Outlined,
|
||||
Interaction::Hovered,
|
||||
false,
|
||||
);
|
||||
assert_eq!(hover.line, ui.color(Role::OnSurface));
|
||||
assert_eq!(hover.stroke, 1.0);
|
||||
|
||||
let focus = palette(&ui, FieldVariant::Outlined, Interaction::Focused, false);
|
||||
let focus = palette(
|
||||
&ui,
|
||||
Base::CANVAS,
|
||||
FieldVariant::Outlined,
|
||||
Interaction::Focused,
|
||||
false,
|
||||
);
|
||||
assert_eq!(focus.line, ui.color(Role::Primary));
|
||||
assert_eq!(focus.stroke, 2.0, "FocusOutlineWidth = 2dp");
|
||||
assert_eq!(focus.label, ui.color(Role::Primary), "FocusLabelColor");
|
||||
|
||||
// A1 没有 Pressed token:按 hover 处理(D11),不是漏做
|
||||
assert_eq!(
|
||||
palette(&ui, FieldVariant::Outlined, Interaction::Pressed, false),
|
||||
palette(
|
||||
&ui,
|
||||
Base::CANVAS,
|
||||
FieldVariant::Outlined,
|
||||
Interaction::Pressed,
|
||||
false
|
||||
),
|
||||
hover
|
||||
);
|
||||
}
|
||||
@@ -69,7 +94,13 @@ fn outline_colors_and_widths_follow_the_state() {
|
||||
#[test]
|
||||
fn error_beats_focus_and_paints_everything_error() {
|
||||
let ui = ui(SchemeFlavor::Light);
|
||||
let error = palette(&ui, FieldVariant::Outlined, Interaction::Focused, true);
|
||||
let error = palette(
|
||||
&ui,
|
||||
Base::CANVAS,
|
||||
FieldVariant::Outlined,
|
||||
Interaction::Focused,
|
||||
true,
|
||||
);
|
||||
assert_eq!(error.line, ui.color(Role::Error));
|
||||
assert_eq!(error.label, ui.color(Role::Error));
|
||||
assert_eq!(error.supporting, ui.color(Role::Error));
|
||||
@@ -82,21 +113,33 @@ fn error_beats_focus_and_paints_everything_error() {
|
||||
fn disabled_uses_the_token_alphas() {
|
||||
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
|
||||
let ui = ui(flavor);
|
||||
let container = interaction::disabled_colors(&ui, Role::Surface).0;
|
||||
let container = interaction::disabled_colors(&ui, Base::CANVAS).0;
|
||||
|
||||
let filled = palette(&ui, FieldVariant::Filled, Interaction::Disabled, false);
|
||||
let filled = palette(
|
||||
&ui,
|
||||
Base::CANVAS,
|
||||
FieldVariant::Filled,
|
||||
Interaction::Disabled,
|
||||
false,
|
||||
);
|
||||
assert_eq!(filled.fill, Some(container));
|
||||
assert_eq!(
|
||||
filled.input,
|
||||
interaction::disabled_content_over_role(&ui, Role::OnSurface, true)
|
||||
interaction::disabled_content_over_role(&ui, Base::CANVAS, Role::OnSurface, true)
|
||||
);
|
||||
|
||||
let outlined = palette(&ui, FieldVariant::Outlined, Interaction::Disabled, false);
|
||||
let outlined = palette(
|
||||
&ui,
|
||||
Base::CANVAS,
|
||||
FieldVariant::Outlined,
|
||||
Interaction::Disabled,
|
||||
false,
|
||||
);
|
||||
assert_eq!(outlined.fill, None);
|
||||
assert_eq!(outlined.line, container, "DisabledOutlineOpacity = 0.12");
|
||||
assert_eq!(
|
||||
outlined.input,
|
||||
interaction::disabled_content_over_role(&ui, Role::OnSurface, false)
|
||||
interaction::disabled_content_over_role(&ui, Base::CANVAS, Role::OnSurface, false)
|
||||
);
|
||||
assert_ne!(outlined.input, ui.color(Role::OnSurface), "禁用不许满档");
|
||||
}
|
||||
@@ -107,7 +150,14 @@ fn disabled_uses_the_token_alphas() {
|
||||
fn filled_carries_the_container_role() {
|
||||
let ui = ui(SchemeFlavor::Light);
|
||||
assert_eq!(
|
||||
palette(&ui, FieldVariant::Filled, Interaction::Enabled, false).fill,
|
||||
palette(
|
||||
&ui,
|
||||
Base::CANVAS,
|
||||
FieldVariant::Filled,
|
||||
Interaction::Enabled,
|
||||
false
|
||||
)
|
||||
.fill,
|
||||
Some(ui.color(Role::SurfaceContainerHighest)),
|
||||
"FilledTextFieldTokens.ContainerColor"
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@ use crate::icons;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::divider;
|
||||
use super::super::interaction::{self, Interaction};
|
||||
use super::super::interaction::{self, Base, Interaction};
|
||||
use super::super::layout;
|
||||
use super::geometry::MenuGeometry;
|
||||
|
||||
@@ -171,10 +171,10 @@ struct ItemPalette {
|
||||
}
|
||||
|
||||
/// 解析一项的颜色:选中优先,其余按五态叠状态层(`interaction::overlay_color`)。
|
||||
fn item_palette(ui: &Ui, state: Interaction, selected: bool) -> ItemPalette {
|
||||
fn item_palette(ui: &Ui, base: Base, state: Interaction, selected: bool) -> ItemPalette {
|
||||
if state.is_disabled() {
|
||||
// 禁用:内容 38%(无容器时叠在表面色上;菜单项本身透明)
|
||||
let ink = interaction::disabled_content_over_role(ui, Role::OnSurface, false);
|
||||
// 禁用:内容 38%,叠在**菜单面板的底**上(项本身透明,透出来的就是面板色)
|
||||
let ink = interaction::disabled_content_over_role(ui, base, Role::OnSurface, false);
|
||||
return ItemPalette {
|
||||
fill: None,
|
||||
ink,
|
||||
@@ -201,14 +201,20 @@ fn item_palette(ui: &Ui, state: Interaction, selected: bool) -> ItemPalette {
|
||||
}
|
||||
|
||||
ItemPalette {
|
||||
// 状态层叠在**菜单容器色**上(SurfaceContainer),得到不透明结果(D27)
|
||||
fill: interaction::overlay_color(ui, state, Role::OnSurface, Role::SurfaceContainer),
|
||||
// 状态层叠在**菜单容器色**上(`PANEL_BASE` = SurfaceContainer),得到不透明结果(D27)
|
||||
fill: interaction::overlay_color(ui, state, Role::OnSurface, base.role()),
|
||||
ink: ui.color(Role::OnSurface),
|
||||
icon: ui.color(Role::OnSecondaryContainer),
|
||||
shape: ShapeScale::Small,
|
||||
}
|
||||
}
|
||||
|
||||
/// 菜单弹出层的底(**面板画这个色,项就叠在它上面**)。
|
||||
///
|
||||
/// 8.2 的 `MenuTokens.ContainerColor = SurfaceContainer`:面板用 `ui.color(PANEL_BASE.role())` 画底,
|
||||
/// 项的禁用态(38% 内容)与状态层拿同一个 [`Base`] 合成——两边同源,改色不会只改一半(M234)。
|
||||
pub const PANEL_BASE: Base = Base::of(Role::SurfaceContainer);
|
||||
|
||||
/// 菜单弹出层(`SurfaceContainer` + L2 阴影)。
|
||||
pub fn menu<'a, M: Clone + 'a>(ui: &'a Ui, menu: Menu<'a, M>) -> Element<'a, M> {
|
||||
let geometry = MenuGeometry::of(ui);
|
||||
@@ -220,13 +226,13 @@ pub fn menu<'a, M: Clone + 'a>(ui: &'a Ui, menu: Menu<'a, M>) -> Element<'a, M>
|
||||
"MenuTokens.ContainerElevation"
|
||||
);
|
||||
let shadow_spec = elevation.shadow();
|
||||
let panel = ui.color(Role::SurfaceContainer);
|
||||
let panel = ui.color(PANEL_BASE.role());
|
||||
let shape = geometry.shape;
|
||||
|
||||
let mut rows = column![].width(Length::Fill);
|
||||
for entry in menu.entries {
|
||||
rows = rows.push(match entry {
|
||||
Entry::Item(item) => item_row(ui, item, geometry),
|
||||
Entry::Item(item) => item_row(ui, PANEL_BASE, item, geometry),
|
||||
// 分组分隔线:复用 `divider::full`(一像素线 + OutlineVariant),不自己画
|
||||
Entry::Divider => divider::full::<M>(ui),
|
||||
});
|
||||
@@ -256,6 +262,7 @@ pub fn menu<'a, M: Clone + 'a>(ui: &'a Ui, menu: Menu<'a, M>) -> Element<'a, M>
|
||||
/// 一行菜单项(用 `button` 承载命中与四态,几何与颜色自绘)。
|
||||
fn item_row<'a, M: Clone + 'a>(
|
||||
ui: &'a Ui,
|
||||
base: Base,
|
||||
item: MenuItem<'a, M>,
|
||||
geometry: MenuGeometry,
|
||||
) -> Element<'a, M> {
|
||||
@@ -265,7 +272,12 @@ fn item_row<'a, M: Clone + 'a>(
|
||||
None
|
||||
});
|
||||
// 选中项的背景也要跟态走:禁用时不许留满档 SecondaryContainer
|
||||
let colors = item_palette(ui, forced.unwrap_or(Interaction::Enabled), item.selected);
|
||||
let colors = item_palette(
|
||||
ui,
|
||||
base,
|
||||
forced.unwrap_or(Interaction::Enabled),
|
||||
item.selected,
|
||||
);
|
||||
let body = ui.style(TypeRole::BodyMedium);
|
||||
|
||||
let label = text(item.label)
|
||||
@@ -308,7 +320,7 @@ fn item_row<'a, M: Clone + 'a>(
|
||||
let selected = item.selected;
|
||||
let style = move |_theme: &iced::Theme, status: button::Status| {
|
||||
let state = forced.unwrap_or_else(|| Interaction::from_status(status));
|
||||
let colors = item_palette(ui, state, selected);
|
||||
let colors = item_palette(ui, base, state, selected);
|
||||
button::Style {
|
||||
background: colors.fill.map(Background::Color),
|
||||
text_color: colors.ink,
|
||||
|
||||
@@ -55,13 +55,13 @@ fn container_uses_surface_container_and_level2_shadow() {
|
||||
#[test]
|
||||
fn selected_and_disabled_items_use_the_right_roles() {
|
||||
let ui = ui(SchemeFlavor::Light);
|
||||
let selected = item_palette(&ui, Interaction::Enabled, true);
|
||||
let selected = item_palette(&ui, PANEL_BASE, Interaction::Enabled, true);
|
||||
assert_eq!(selected.fill, Some(ui.color(Role::SecondaryContainer)));
|
||||
assert_eq!(selected.ink, ui.color(Role::OnSecondaryContainer));
|
||||
assert_eq!(selected.icon, ui.color(Role::OnSecondaryContainer));
|
||||
|
||||
// 选中 + hover:状态层叠在 SecondaryContainer 上(不是满档底色)
|
||||
let hovered = item_palette(&ui, Interaction::Hovered, true);
|
||||
let hovered = item_palette(&ui, PANEL_BASE, Interaction::Hovered, true);
|
||||
assert_ne!(hovered.fill, selected.fill);
|
||||
assert_eq!(
|
||||
hovered.fill,
|
||||
@@ -73,11 +73,11 @@ fn selected_and_disabled_items_use_the_right_roles() {
|
||||
)
|
||||
);
|
||||
|
||||
let disabled = item_palette(&ui, Interaction::Disabled, true);
|
||||
let disabled = item_palette(&ui, PANEL_BASE, Interaction::Disabled, true);
|
||||
assert_eq!(disabled.fill, None, "禁用项不留满档选中底色");
|
||||
assert_eq!(
|
||||
disabled.ink,
|
||||
interaction::disabled_content_over_role(&ui, Role::OnSurface, false)
|
||||
interaction::disabled_content_over_role(&ui, PANEL_BASE, Role::OnSurface, false)
|
||||
);
|
||||
assert_ne!(disabled.ink, ui.color(Role::OnSurface));
|
||||
}
|
||||
@@ -86,14 +86,14 @@ fn selected_and_disabled_items_use_the_right_roles() {
|
||||
#[test]
|
||||
fn item_states_blend_on_the_menu_container() {
|
||||
let ui = ui(SchemeFlavor::Light);
|
||||
let rest = item_palette(&ui, Interaction::Enabled, false);
|
||||
let rest = item_palette(&ui, PANEL_BASE, Interaction::Enabled, false);
|
||||
assert_eq!(rest.fill, None, "静息不叠色(菜单底色透出来)");
|
||||
for state in [
|
||||
Interaction::Hovered,
|
||||
Interaction::Pressed,
|
||||
Interaction::Focused,
|
||||
] {
|
||||
let fill = item_palette(&ui, state, false)
|
||||
let fill = item_palette(&ui, PANEL_BASE, state, false)
|
||||
.fill
|
||||
.expect("可交互态必须给底色");
|
||||
assert_eq!(fill.a, 1.0, "结果必须不透明(D27)");
|
||||
|
||||
@@ -38,7 +38,7 @@ use unlucky_design::typography::TypeRole;
|
||||
use crate::icons;
|
||||
use crate::theme::{Ui, STROKE_PX};
|
||||
|
||||
use super::super::interaction::{self, Interaction};
|
||||
use super::super::interaction::{self, Base, Interaction};
|
||||
use super::super::layout;
|
||||
use super::geometry::{SEARCH_MAX_WIDTH, SEARCH_MIN_WIDTH, SEARCH_VIEW_MIN_HEIGHT};
|
||||
|
||||
@@ -52,6 +52,8 @@ pub struct SearchBar<'a> {
|
||||
trailing: Option<IconKind>,
|
||||
state: Option<Interaction>,
|
||||
width: Length,
|
||||
/// 脚下那一层的底(M234,禁用态合成用)。
|
||||
base: Base,
|
||||
}
|
||||
|
||||
impl<'a> SearchBar<'a> {
|
||||
@@ -63,6 +65,7 @@ impl<'a> SearchBar<'a> {
|
||||
trailing: Some(IconKind::Filter),
|
||||
state: None,
|
||||
width: Length::Fill,
|
||||
base: Base::CANVAS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,12 +86,22 @@ impl<'a> SearchBar<'a> {
|
||||
self.width = width;
|
||||
self
|
||||
}
|
||||
|
||||
/// **告诉药丸它踩在什么底上**(M234):禁用态那 12% 的容器叠在这个底上。
|
||||
///
|
||||
/// 药丸自己的容器色是 `SurfaceContainerHighest`,但**禁用它不画那个色**——禁用画的是
|
||||
/// 「12% 叠父底」,所以这里要的是父底(卡片 = `surfaces::elevated::BASE`),不是它自己。
|
||||
pub fn base(mut self, base: impl Into<Base>) -> Self {
|
||||
self.base = base.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// 搜索条内的一行内容(bar 与 view 头部共用,保证「展开时头部不跳」——20.6)。
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn bar_content<'a, M: Clone + 'a>(
|
||||
ui: &'a Ui,
|
||||
base: Base,
|
||||
value: &'a str,
|
||||
placeholder: &'a str,
|
||||
trailing: Option<IconKind>,
|
||||
@@ -102,7 +115,7 @@ fn bar_content<'a, M: Clone + 'a>(
|
||||
let icon_offset = icon_edge - bar.pad_start;
|
||||
|
||||
let ink = if state.is_disabled() {
|
||||
interaction::disabled_content_over_role(ui, Role::OnSurface, true)
|
||||
interaction::disabled_content_over_role(ui, base, Role::OnSurface, true)
|
||||
} else {
|
||||
ui.color(Role::OnSurface)
|
||||
};
|
||||
@@ -190,7 +203,8 @@ pub fn search_bar<'a, M: Clone + 'a>(
|
||||
let height = ui.metrics().search_bar_height;
|
||||
|
||||
let background = if state.is_disabled() {
|
||||
interaction::disabled_colors(ui, Role::Surface).0
|
||||
// 禁用:12% 叠在**药丸脚下的底**上(不是药丸自己的 SurfaceContainerHighest)
|
||||
interaction::disabled_colors(ui, bar.base).0
|
||||
} else {
|
||||
// 规格卡里搜索药丸与 filled 字段是**同一个底**(`SurfaceContainerHighest`):
|
||||
// M229 r1 用的是 `High`,比 filled 字段低一档,两块并排时能看出色差(复审 F2 裁决抬档)。
|
||||
@@ -212,6 +226,7 @@ pub fn search_bar<'a, M: Clone + 'a>(
|
||||
|
||||
container(bar_content(
|
||||
ui,
|
||||
bar.base,
|
||||
bar.value,
|
||||
bar.placeholder,
|
||||
bar.trailing,
|
||||
@@ -269,7 +284,8 @@ pub fn search_view<'a, M: Clone + 'a>(
|
||||
) -> Element<'a, M> {
|
||||
// 展开面板**不跟着药丸抬档**:它是浮层(菜单语言,`SurfaceContainerHigh`),
|
||||
// 规格卡只画了收起的药丸。两块不同档是层级差,不是漏改(复审 F2 只裁药丸)。
|
||||
let panel = ui.color(Role::SurfaceContainerHigh);
|
||||
let panel_role = Role::SurfaceContainerHigh;
|
||||
let panel = ui.color(panel_role);
|
||||
let header_height = ui.metrics().search_bar_height;
|
||||
let body_style = ui.style(TypeRole::BodyMedium);
|
||||
|
||||
@@ -327,6 +343,7 @@ pub fn search_view<'a, M: Clone + 'a>(
|
||||
let panel_content = column![
|
||||
container(bar_content(
|
||||
ui,
|
||||
Base::of(panel_role),
|
||||
view.value,
|
||||
view.placeholder,
|
||||
Some(IconKind::Close),
|
||||
|
||||
@@ -40,13 +40,12 @@ use iced::advanced::widget::{tree, Tree};
|
||||
use iced::advanced::{mouse, Clipboard, Shell, Widget};
|
||||
use iced::{Background, Border, Color, Element, Event, Length, Rectangle, Size};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::interaction::{self, Interaction, Tracker};
|
||||
use super::geometry::{SliderGeometry, HANDLE_HEIGHT, HANDLE_WIDTH_ACTIVE, STOP_GAP, STOP_SIZE};
|
||||
use super::super::interaction::{Base, Interaction, Tracker};
|
||||
use super::geometry::{SliderGeometry, HANDLE_HEIGHT, STOP_GAP, STOP_SIZE};
|
||||
|
||||
mod paint;
|
||||
mod track;
|
||||
|
||||
pub use track::TrackGeometry;
|
||||
@@ -60,6 +59,8 @@ pub struct Slider<'a, M> {
|
||||
steps: Option<u32>,
|
||||
on_change: Option<Box<dyn Fn(f32) -> M + 'a>>,
|
||||
forced: Option<Interaction>,
|
||||
/// 脚下那一层的底(M234,禁用态合成用)。
|
||||
base: Base,
|
||||
}
|
||||
|
||||
impl<'a, M> Slider<'a, M> {
|
||||
@@ -71,6 +72,7 @@ impl<'a, M> Slider<'a, M> {
|
||||
steps: None,
|
||||
on_change: None,
|
||||
forced: None,
|
||||
base: Base::CANVAS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +95,14 @@ impl<'a, M> Slider<'a, M> {
|
||||
self
|
||||
}
|
||||
|
||||
/// **告诉滑块它踩在什么底上**(M234):禁用态的 12% 未激活轨与 38% 内容叠在这个底上。
|
||||
///
|
||||
/// 放进卡片/面板里必须给(卡片 = `surfaces::elevated::BASE`)。
|
||||
pub fn base(mut self, base: impl Into<Base>) -> Self {
|
||||
self.base = base.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// 当前比例(量化后)。
|
||||
fn value(&self) -> f32 {
|
||||
quantize(self.fraction, self.steps)
|
||||
@@ -240,30 +250,13 @@ where
|
||||
.forced
|
||||
.unwrap_or_else(|| tree.state.downcast_ref::<State>().tracker.state());
|
||||
let disabled = state.is_disabled();
|
||||
|
||||
let (active_color, inactive_color, handle_color, handle_width) = if disabled {
|
||||
(
|
||||
interaction::disabled_content_over_role(ui, Role::OnSurface, false),
|
||||
interaction::disabled_colors(ui, Role::Surface).0,
|
||||
interaction::disabled_content_over_role(ui, Role::OnSurface, false),
|
||||
geometry.handle,
|
||||
)
|
||||
} else {
|
||||
// 状态层只叠在 handle 上(D16):α 全部来自 interaction(hover 8 / focus 10 / pressed 10)
|
||||
let handle_color =
|
||||
interaction::overlay_color(ui, state, Role::OnSurface, Role::Primary)
|
||||
.unwrap_or_else(|| ui.color(Role::Primary));
|
||||
let handle_width = match state {
|
||||
Interaction::Focused | Interaction::Pressed => HANDLE_WIDTH_ACTIVE,
|
||||
_ => geometry.handle,
|
||||
};
|
||||
(
|
||||
ui.color(Role::Primary),
|
||||
ui.color(Role::SecondaryContainer),
|
||||
handle_color,
|
||||
handle_width,
|
||||
)
|
||||
};
|
||||
let colors = paint::colors(ui, self.base, state, geometry.handle);
|
||||
let (active_color, inactive_color, handle_color, handle_width) = (
|
||||
colors.active,
|
||||
colors.inactive,
|
||||
colors.handle,
|
||||
colors.handle_width,
|
||||
);
|
||||
|
||||
let centre_y = bounds.y + bounds.height / 2.0;
|
||||
let track_x = bounds.x;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
//! 滑块的颜色求值(M234 从 `slider.rs` 的 `draw` 里搬出来,顺手收一个 [`Base`])。
|
||||
//!
|
||||
//! 搬出来的两个理由:① `slider.rs` 已顶到 399 行,加 `.base(..)` 之前必须腾地方;
|
||||
//! ② 禁用态那三笔颜色原来在 `draw` 里写死 `Role::Surface` 当合成底(M234 的病灶),
|
||||
//! 搬进纯函数后可以被单测直接钉住「叠在哪个底上」。
|
||||
|
||||
use iced::Color;
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::super::interaction::{self, Base, Interaction};
|
||||
use super::super::geometry::HANDLE_WIDTH_ACTIVE;
|
||||
|
||||
/// 滑块一笔要的三个颜色 + handle 宽。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(super) struct Colors {
|
||||
/// 激活段(左侧已走部分)。
|
||||
pub(super) active: Color,
|
||||
/// 未激活段。
|
||||
pub(super) inactive: Color,
|
||||
/// handle(4×44 竖条)。
|
||||
pub(super) handle: Color,
|
||||
/// handle 宽(focus / pressed 收窄)。
|
||||
pub(super) handle_width: f32,
|
||||
}
|
||||
|
||||
/// 五态 → 三笔颜色(`HANDLE_WIDTH_ACTIVE` 只在 focus/pressed 用)。
|
||||
///
|
||||
/// 禁用(11.6):激活段与 handle `OnSurface`@38%、未激活轨 @12%,三者都叠在 `base` 上
|
||||
/// ——`base` 是滑块**脚下那一层**(卡片 = `surfaces::elevated::BASE`),不是恒定的画布色。
|
||||
pub(super) fn colors(ui: &Ui, base: Base, state: Interaction, handle: f32) -> Colors {
|
||||
if state.is_disabled() {
|
||||
return Colors {
|
||||
active: interaction::disabled_content_over_role(ui, base, Role::OnSurface, false),
|
||||
inactive: interaction::disabled_colors(ui, base).0,
|
||||
handle: interaction::disabled_content_over_role(ui, base, Role::OnSurface, false),
|
||||
handle_width: handle,
|
||||
};
|
||||
}
|
||||
// 状态层只叠在 handle 上(D16):α 全部来自 interaction(hover 8 / focus 10 / pressed 10)
|
||||
let handle_color = interaction::overlay_color(ui, state, Role::OnSurface, Role::Primary)
|
||||
.unwrap_or_else(|| ui.color(Role::Primary));
|
||||
let handle_width = match state {
|
||||
Interaction::Focused | Interaction::Pressed => HANDLE_WIDTH_ACTIVE,
|
||||
_ => handle,
|
||||
};
|
||||
Colors {
|
||||
active: ui.color(Role::Primary),
|
||||
inactive: ui.color(Role::SecondaryContainer),
|
||||
handle: handle_color,
|
||||
handle_width,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//! 禁用态的**底**:控件脚下那一层容器实际画出来的底色(M234)。
|
||||
//!
|
||||
//! ## 为什么不是一个 `Role::Surface`
|
||||
//!
|
||||
//! 禁用态的容器(按钮 10% / 面状控件 12%)与内容(38%)都要**预先合成成不透明色**
|
||||
//! (D27:GPU/CPU 两档逐像素一致,所以不许把 α 交给渲染器混合)。合成必须知道「叠在什么上」,
|
||||
//! 而这个「什么」是**宿主容器**的属性,不是控件的属性:
|
||||
//!
|
||||
//! ```text
|
||||
//! 画布(Surface) ← 页面底色
|
||||
//! └ 卡片(SurfaceContainer) ← `surfaces::elevated::card`
|
||||
//! └ 面板(SurfaceContainerLow)← 取景板 / Card 层
|
||||
//! └ 禁用按钮(OnSurface@10% 叠在面板底上)
|
||||
//! ```
|
||||
//!
|
||||
//! M229 之前 `disabled.rs` 的入口签名是 `(ui, surface: Role)`,而**所有调用点都传死
|
||||
//! `Role::Surface`** —— 于是卡片里的禁用按钮把 10% 叠在画布色上,出来是一块与卡片底
|
||||
//! 不协调的灰(M223 记的「CR 1.05、形丢失」)。参数在、语义也在,只是没人给真值。
|
||||
//!
|
||||
//! ## 纪律:底由**画那个底的容器**给
|
||||
//!
|
||||
//! 每个自己画底的容器**导出 `BASE` 常量**,画的时候用同一个 `Role`、给子控件时给这个常量
|
||||
//! (见 [`super::super::surfaces::elevated::BASE`]、`inputs::menu::PANEL_BASE`)。两边同源,
|
||||
//! 改色不会漂一半。
|
||||
//!
|
||||
//! iced 没有「环境上下文」(样式闭包只拿得到 `&Theme`,拿不到父容器的样式),所以这条信息
|
||||
//! **只能显式往下传**:容器 → 控件(`.base(..)`)→ 禁用色求值。控件自己不猜。
|
||||
//!
|
||||
//! ## 抓得住 / 抓不住
|
||||
//!
|
||||
//! - 抓得住:合成用了哪个 `Role`(`interaction::disabled::tests` 的逐值断言、gallery 的像素 probe);
|
||||
//! - 抓不住:① 容器画的是**合成色**(半透明面板 α、状态层叠过的底)——`Base` 只装一个 `Role`;
|
||||
//! ② 调用点给了**错的** `Role`(机制不校验语义,判据只能落在「给的底对不对」的像素上);
|
||||
//! ③ 忘了调 `.base(..)` 时回落到 [`Base::CANVAS`],编译期不会报错(文档 + 像素 probe 兜)。
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
|
||||
/// 控件脚下那一层的底(容器自己画的角色,**不是**「反正是 surface」)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Base(Role);
|
||||
|
||||
impl Base {
|
||||
/// 画布底(`Surface`):控件**直接**摆在页面/画布上时用它。
|
||||
///
|
||||
/// 这也是各控件 `.base(..)` 的默认值——默认值只对「没有容器」这一种情形成立,
|
||||
/// 放进卡片/面板/浮层里**必须**换成那个容器的 `BASE`。
|
||||
pub const CANVAS: Base = Base(Role::Surface);
|
||||
|
||||
/// 从容器**画自己底时用的那个角色**构造(容器应导出一个 `BASE` 常量走这条路)。
|
||||
pub const fn of(role: Role) -> Self {
|
||||
Self(role)
|
||||
}
|
||||
|
||||
/// 该底对应的角色(合成时取色用)。
|
||||
pub const fn role(self) -> Role {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// 容器也可以直接把**自己的角色**传进来(`disabled_*` 的入口收 `impl Into<Base>`):
|
||||
/// 这样历史调用点不必全改签名,但语义只有一条——「你脚下那一层是谁」。
|
||||
impl From<Role> for Base {
|
||||
fn from(role: Role) -> Self {
|
||||
Self(role)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 画布底就是画布角色:`Base` 不许偷偷改语义。
|
||||
#[test]
|
||||
fn canvas_is_surface_and_of_keeps_the_role() {
|
||||
assert_eq!(Base::CANVAS.role(), Role::Surface);
|
||||
assert_eq!(
|
||||
Base::of(Role::SurfaceContainer).role(),
|
||||
Role::SurfaceContainer
|
||||
);
|
||||
// 容器底与画布底**不是同一个值**:这一条红了说明 `Base` 把角色丢了
|
||||
assert_ne!(Base::of(Role::SurfaceContainer), Base::CANVAS);
|
||||
}
|
||||
}
|
||||
@@ -34,46 +34,77 @@ use unlucky_design::color::{Role, Scheme};
|
||||
|
||||
use crate::theme::{to_iced, Ui};
|
||||
|
||||
use super::base::Base;
|
||||
|
||||
// 四个入口都收 `impl Into<Base>`:容器导出的 `BASE` 常量(推荐)与裸 `Role`(历史调用点,
|
||||
// 例如 `pages/settings/java/tests/marker.rs`)都能进来。**没有隐式默认值**——不给就是编译错误,
|
||||
// 这一条正是 M229 缺的:参数在、语义在,只是调用点全传了 `Role::Surface`。
|
||||
|
||||
/// 禁用态内容色(**按角色、按有没有容器**求值)——A1 口径的入口(**面状控件族**的底)。
|
||||
///
|
||||
/// - `base`:控件**脚下那一层的底**([`Base`])。容器给的 `BASE` 常量,不是猜;
|
||||
/// - `role`:由变体决定(Filled/Outlined/Text/Elevated = `OnSurfaceVariant`;
|
||||
/// FilledTonal 与三式 IconButton = `OnSurface`,见 `Variant::disabled_content`);
|
||||
/// - `on_container = true`:叠在**面状控件族的 12% 容器**([`DISABLED_SURFACE_ALPHA`])上
|
||||
/// (Filled TextField / 有容器控件在 12% 底上求内容时);
|
||||
/// `false`:直接叠在 **surface** 上(Text / Outlined / standard IconButton / 描边类控件的内容)。
|
||||
/// (Filled TextField / 有容器控件在 12% 底上求内容时),容器本身叠在 `base` 上;
|
||||
/// `false`:直接叠在 `base` 上(Text / Outlined / standard IconButton / 描边类控件的内容)。
|
||||
///
|
||||
/// **按钮家族不要用这个函数**(按钮容器是 10%):走 [`disabled_button_content`]。
|
||||
///
|
||||
/// 之前这里写死 `OnSurfaceVariant` 且只有一个「底」,导致 FilledTonal 与 IconButton 两族偏浅
|
||||
/// 15/255(视觉 r3 用像素量的)。
|
||||
pub fn disabled_content_over_role(ui: &Ui, role: Role, on_container: bool) -> Color {
|
||||
pub fn disabled_content_over_role(
|
||||
ui: &Ui,
|
||||
base: impl Into<Base>,
|
||||
role: Role,
|
||||
on_container: bool,
|
||||
) -> Color {
|
||||
// **取 `Ui` 里已注入的方案**,不重算 baseline:M229 之后产品方案是品牌色板
|
||||
// (Ink / Silver),照 baseline 重算会画出旧的天蓝中性色(复审 F1)。
|
||||
disabled_content_with_alpha(ui.scheme(), role, on_container, DISABLED_SURFACE_ALPHA)
|
||||
disabled_content_with_alpha(
|
||||
ui.scheme(),
|
||||
base.into(),
|
||||
role,
|
||||
on_container,
|
||||
DISABLED_SURFACE_ALPHA,
|
||||
)
|
||||
}
|
||||
|
||||
/// 禁用态内容色——**按钮家族**(`on_container`:是否叠在本族 10% 容器上)。
|
||||
///
|
||||
/// 与 [`disabled_content_over_role`] 的差别只有「底」的 α:按钮 10%、面状控件 12%。
|
||||
/// 两个入口分开是 M86 的结论:合成一个数会让一族永远差 1/255,肉眼看不见、只有 probe 会红。
|
||||
pub fn disabled_button_content(ui: &Ui, role: Role, on_container: bool) -> Color {
|
||||
disabled_content_with_alpha(ui.scheme(), role, on_container, DISABLED_CONTAINER_ALPHA)
|
||||
pub fn disabled_button_content(
|
||||
ui: &Ui,
|
||||
base: impl Into<Base>,
|
||||
role: Role,
|
||||
on_container: bool,
|
||||
) -> Color {
|
||||
disabled_content_with_alpha(
|
||||
ui.scheme(),
|
||||
base.into(),
|
||||
role,
|
||||
on_container,
|
||||
DISABLED_CONTAINER_ALPHA,
|
||||
)
|
||||
}
|
||||
|
||||
/// 两个族共用的内容色求值(「底」的 α 由调用方给)。
|
||||
fn disabled_content_with_alpha(
|
||||
scheme: &Scheme,
|
||||
base: Base,
|
||||
role: Role,
|
||||
on_container: bool,
|
||||
container_alpha: f32,
|
||||
) -> Color {
|
||||
let underneath = scheme.get(base.role());
|
||||
let base = if on_container {
|
||||
scheme
|
||||
.get(Role::OnSurface)
|
||||
.with_alpha(container_alpha)
|
||||
.over(scheme.get(Role::Surface))
|
||||
.over(underneath)
|
||||
} else {
|
||||
scheme.get(Role::Surface)
|
||||
underneath
|
||||
};
|
||||
|
||||
to_iced(
|
||||
@@ -99,29 +130,32 @@ fn disabled_content_with_alpha(
|
||||
///
|
||||
/// 顺序必须「先容器后内容」:Compose 也是这么画的,先叠容器再叠 label,
|
||||
/// 所以内容叠的角色是容器实色而不是底下的 surface。
|
||||
pub fn disabled_colors(ui: &Ui, surface: Role) -> (Color, Color) {
|
||||
disabled_colors_with_alpha(ui, surface, DISABLED_SURFACE_ALPHA)
|
||||
///
|
||||
/// ⚠️ **`base` 必须是控件脚下那一层的底**([`Base`]):M229 之前这里的参数名叫 `surface`,
|
||||
/// 而 8 个调用点全传 `Role::Surface`,于是卡片/面板里的禁用控件把 12% 叠在画布色上(M234)。
|
||||
pub fn disabled_colors(ui: &Ui, base: impl Into<Base>) -> (Color, Color) {
|
||||
disabled_colors_with_alpha(ui, base.into(), DISABLED_SURFACE_ALPHA)
|
||||
}
|
||||
|
||||
/// 禁用态的一对颜色 `(容器, 内容)`——**按钮家族**(10% 容器)。
|
||||
///
|
||||
/// - 容器 = `OnSurface` @ 10%([`DISABLED_CONTAINER_ALPHA`])叠在 `surface` 上;
|
||||
/// - 容器 = `OnSurface` @ 10%([`DISABLED_CONTAINER_ALPHA`])叠在 `base` 上;
|
||||
/// - 内容 = **默认** `OnSurfaceVariant` @ 38%;按变体的取值走 [`disabled_button_content`]。
|
||||
///
|
||||
/// 除了按钮家族,**没有别的消费者**:面状控件(chip / slider 轨道 / switch 轨道 / 描边)走
|
||||
/// [`disabled_colors`] 的 12%。
|
||||
pub fn disabled_button_colors(ui: &Ui, surface: Role) -> (Color, Color) {
|
||||
disabled_colors_with_alpha(ui, surface, DISABLED_CONTAINER_ALPHA)
|
||||
pub fn disabled_button_colors(ui: &Ui, base: impl Into<Base>) -> (Color, Color) {
|
||||
disabled_colors_with_alpha(ui, base.into(), DISABLED_CONTAINER_ALPHA)
|
||||
}
|
||||
|
||||
/// 两个族共用的求值:容器 = `OnSurface` @ `container_alpha` 叠在 `surface` 上,内容 = 38% 叠在容器上。
|
||||
fn disabled_colors_with_alpha(ui: &Ui, surface: Role, container_alpha: f32) -> (Color, Color) {
|
||||
/// 两个族共用的求值:容器 = `OnSurface` @ `container_alpha` 叠在 `base` 上,内容 = 38% 叠在容器上。
|
||||
fn disabled_colors_with_alpha(ui: &Ui, base: Base, container_alpha: f32) -> (Color, Color) {
|
||||
let scheme = ui.scheme();
|
||||
let base = scheme.get(surface);
|
||||
let underneath = scheme.get(base.role());
|
||||
let container = scheme
|
||||
.get(Role::OnSurface)
|
||||
.with_alpha(container_alpha)
|
||||
.over(base);
|
||||
.over(underneath);
|
||||
// 这里是**默认口径**(OnSurfaceVariant);按变体取值请走 [`disabled_content_over_role`]
|
||||
// ——FilledTonal 与三式 IconButton 的禁用内容是 `OnSurface`(A1)。
|
||||
let content = scheme
|
||||
|
||||
@@ -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) {
|
||||
(
|
||||
|
||||
@@ -39,6 +39,13 @@
|
||||
//! 上表右列是**唯一**允许的取值口;左列只是给人看的,代码里一个数字都不许出现
|
||||
//! (`components::gate` 会扫)。
|
||||
//!
|
||||
//! ## 合成的「底」是参数,不是常量(M234)
|
||||
//!
|
||||
//! 四个入口都收一个 [`Base`]——控件**脚下那一层的底**,由画那个底的容器导出
|
||||
//! (`surfaces::elevated::BASE` / `inputs::menu::PANEL_BASE` / 各控件的 `.base(..)`)。
|
||||
//! M229 之前这个值恒等于 `Role::Surface`,于是卡片/面板里的禁用控件把 10%/12% 叠在画布色上
|
||||
//! (M223 的「CR 1.05、形丢失」)。**不许**在入口内部再写一个默认值回来。
|
||||
//!
|
||||
//! ## 两个消费形态
|
||||
//!
|
||||
//! 1. **iced `button` 系**(本 mission 的按钮家族):拿 [`Interaction`] 直接配 [`overlay_color`];
|
||||
@@ -144,8 +151,10 @@ pub fn overlay_color(ui: &Ui, state: Interaction, content: Role, container: Role
|
||||
.map(|layer| ui.state_over(layer, content, container))
|
||||
}
|
||||
|
||||
mod base;
|
||||
mod disabled;
|
||||
|
||||
pub use base::Base;
|
||||
pub use disabled::{
|
||||
disabled_button_colors, disabled_button_content, disabled_colors, disabled_content_over_role,
|
||||
};
|
||||
|
||||
@@ -34,12 +34,19 @@ use unlucky_design::typography::TypeRole;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::divider;
|
||||
use super::super::interaction::Base;
|
||||
use super::geometry::{NavLayout, NavPaneGeometry};
|
||||
|
||||
mod item;
|
||||
use item::{line, nav_row};
|
||||
pub use item::{NavGroup, NavItem};
|
||||
|
||||
/// 导航容器的底:§21.3 标准(常驻)= `Surface` + L0 —— 所以项的禁用内容叠在画布色上。
|
||||
///
|
||||
/// 与 [`navigation`] 的 `ui.layer(MaterialLayer::Canvas)` 同源(`Canvas` 的 surface_role 就是
|
||||
/// `Surface`),M234 之后「底」由这里给,项自己不猜。
|
||||
pub const BASE: Base = Base::of(Role::Surface);
|
||||
|
||||
/// 导航容器(D33:`layout` 决定 88 还是 240,其余全同)。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Navigation<'a, M> {
|
||||
@@ -110,7 +117,7 @@ pub fn navigation<'a, M: Clone + 'a>(ui: &'a Ui, nav: Navigation<'a, M>) -> Elem
|
||||
);
|
||||
}
|
||||
for item in group.items {
|
||||
body = body.push(nav_row(ui, item, geometry));
|
||||
body = body.push(nav_row(ui, BASE, item, geometry));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ use unlucky_design::typography::TypeRole;
|
||||
use crate::icons;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::super::interaction::{self, Interaction};
|
||||
use super::super::super::interaction::{self, Base, Interaction};
|
||||
use super::super::super::layout;
|
||||
use super::super::geometry::{NavLayout, NavPaneGeometry};
|
||||
|
||||
@@ -109,12 +109,13 @@ pub(super) struct ItemPalette {
|
||||
|
||||
pub(super) fn palette(
|
||||
ui: &Ui,
|
||||
base: Base,
|
||||
layout: NavLayout,
|
||||
state: Interaction,
|
||||
selected: bool,
|
||||
) -> ItemPalette {
|
||||
if state.is_disabled() {
|
||||
let ink = interaction::disabled_content_over_role(ui, Role::OnSurface, false);
|
||||
let ink = interaction::disabled_content_over_role(ui, base, Role::OnSurface, false);
|
||||
return ItemPalette {
|
||||
pill: None,
|
||||
icon: ink,
|
||||
@@ -174,6 +175,7 @@ pub(super) fn line<'a, M: 'a>(
|
||||
/// 一项(rail:图标在上、标签在下;drawer:图标在左、标签在右)。
|
||||
pub(super) fn nav_row<'a, M: Clone + 'a>(
|
||||
ui: &'a Ui,
|
||||
base: Base,
|
||||
item: NavItem<'a, M>,
|
||||
geometry: NavPaneGeometry,
|
||||
) -> Element<'a, M> {
|
||||
@@ -183,7 +185,7 @@ pub(super) fn nav_row<'a, M: Clone + 'a>(
|
||||
None
|
||||
});
|
||||
let state = forced.unwrap_or(Interaction::Enabled);
|
||||
let colors = palette(ui, geometry.layout, state, item.selected);
|
||||
let colors = palette(ui, base, geometry.layout, state, item.selected);
|
||||
let radius = iced::border::Radius::new(geometry.pill_height / 2.0);
|
||||
|
||||
let icon = layout::fixed(
|
||||
|
||||
@@ -26,7 +26,7 @@ fn ui() -> Ui {
|
||||
#[test]
|
||||
fn drawer_selected_uses_secondary_container() {
|
||||
let ui = ui();
|
||||
let palette = palette(&ui, NavLayout::Drawer, Interaction::Enabled, true);
|
||||
let palette = palette(&ui, BASE, NavLayout::Drawer, Interaction::Enabled, true);
|
||||
assert_eq!(palette.pill, Some(ui.color(Role::SecondaryContainer)));
|
||||
assert_eq!(palette.icon, ui.color(Role::OnSecondaryContainer));
|
||||
assert_eq!(palette.label, ui.color(Role::OnSecondaryContainer));
|
||||
@@ -36,8 +36,8 @@ fn drawer_selected_uses_secondary_container() {
|
||||
#[test]
|
||||
fn rail_selected_label_is_secondary() {
|
||||
let ui = ui();
|
||||
let rail = palette(&ui, NavLayout::Rail, Interaction::Enabled, true);
|
||||
let drawer = palette(&ui, NavLayout::Drawer, Interaction::Enabled, true);
|
||||
let rail = palette(&ui, BASE, NavLayout::Rail, Interaction::Enabled, true);
|
||||
let drawer = palette(&ui, BASE, NavLayout::Drawer, Interaction::Enabled, true);
|
||||
assert_eq!(rail.label, ui.color(Role::Secondary));
|
||||
assert_ne!(rail.label, drawer.label, "两条线的选中标签角色不同");
|
||||
// 但图标与药丸相同(同一套语言)
|
||||
@@ -49,7 +49,7 @@ fn rail_selected_label_is_secondary() {
|
||||
#[test]
|
||||
fn inactive_lifts_to_on_surface() {
|
||||
let ui = ui();
|
||||
let resting = palette(&ui, NavLayout::Drawer, Interaction::Enabled, false);
|
||||
let resting = palette(&ui, BASE, NavLayout::Drawer, Interaction::Enabled, false);
|
||||
assert_eq!(resting.label, ui.color(Role::OnSurfaceVariant));
|
||||
assert_eq!(resting.pill, None);
|
||||
for state in [
|
||||
@@ -57,7 +57,7 @@ fn inactive_lifts_to_on_surface() {
|
||||
Interaction::Focused,
|
||||
Interaction::Pressed,
|
||||
] {
|
||||
let active = palette(&ui, NavLayout::Drawer, state, false);
|
||||
let active = palette(&ui, BASE, NavLayout::Drawer, state, false);
|
||||
assert_eq!(active.label, ui.color(Role::OnSurface), "{state:?}");
|
||||
assert_eq!(active.icon, ui.color(Role::OnSurface), "{state:?}");
|
||||
assert!(active.pill.is_some(), "{state:?} 必须叠状态层");
|
||||
@@ -68,10 +68,10 @@ fn inactive_lifts_to_on_surface() {
|
||||
#[test]
|
||||
fn disabled_draws_no_pill() {
|
||||
let ui = ui();
|
||||
let palette = palette(&ui, NavLayout::Drawer, Interaction::Disabled, true);
|
||||
let palette = palette(&ui, BASE, NavLayout::Drawer, Interaction::Disabled, true);
|
||||
assert_eq!(
|
||||
palette.icon,
|
||||
interaction::disabled_content_over_role(&ui, Role::OnSurface, false)
|
||||
interaction::disabled_content_over_role(&ui, BASE, Role::OnSurface, false)
|
||||
);
|
||||
assert_eq!(palette.label, palette.icon);
|
||||
assert_eq!(palette.pill, None);
|
||||
|
||||
@@ -49,7 +49,7 @@ 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::{
|
||||
typography_headline, typography_overline, typography_supporting, ListItemGeometry,
|
||||
@@ -66,6 +66,8 @@ pub struct ListItem<'a, M> {
|
||||
on_press: Option<M>,
|
||||
selected: bool,
|
||||
state: Option<Interaction>,
|
||||
/// 脚下那一层的底(M234,禁用态合成用)。
|
||||
base: Base,
|
||||
}
|
||||
|
||||
impl<'a, M> ListItem<'a, M> {
|
||||
@@ -80,6 +82,7 @@ impl<'a, M> ListItem<'a, M> {
|
||||
on_press: Some(on_press),
|
||||
selected: false,
|
||||
state: None,
|
||||
base: Base::CANVAS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +122,12 @@ impl<'a, M> ListItem<'a, M> {
|
||||
self
|
||||
}
|
||||
|
||||
/// **告诉列表项它踩在什么底上**(M234):禁用态的 38% 内容叠在这个底上。
|
||||
pub fn base(mut self, base: impl Into<Base>) -> Self {
|
||||
self.base = base.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// 转禁用:**只清消息**,槽位与选中态一律保留。
|
||||
pub fn disabled(mut self) -> Self {
|
||||
self.on_press = None;
|
||||
@@ -141,10 +150,10 @@ struct ItemPalette {
|
||||
trailing: Color,
|
||||
}
|
||||
|
||||
fn palette(ui: &Ui, state: Interaction, selected: bool) -> ItemPalette {
|
||||
fn palette(ui: &Ui, base: Base, state: Interaction, selected: bool) -> ItemPalette {
|
||||
if state.is_disabled() {
|
||||
// A1:ListItemDisabled*Color = OnSurface × 0.38(无容器时叠在 surface 上)
|
||||
let ink = interaction::disabled_content_over_role(ui, Role::OnSurface, false);
|
||||
let ink = interaction::disabled_content_over_role(ui, base, Role::OnSurface, false);
|
||||
return ItemPalette {
|
||||
fill: None,
|
||||
headline: ink,
|
||||
@@ -213,7 +222,12 @@ pub fn list_item<'a, M: Clone + 'a>(ui: &'a Ui, item: ListItem<'a, M>) -> Elemen
|
||||
} else {
|
||||
None
|
||||
});
|
||||
let colors = palette(ui, forced.unwrap_or(Interaction::Enabled), item.selected);
|
||||
let colors = palette(
|
||||
ui,
|
||||
item.base,
|
||||
forced.unwrap_or(Interaction::Enabled),
|
||||
item.selected,
|
||||
);
|
||||
|
||||
let mut body = iced::widget::Column::new().spacing(0);
|
||||
if let Some(overline) = item.overline {
|
||||
@@ -268,9 +282,10 @@ pub fn list_item<'a, M: Clone + 'a>(ui: &'a Ui, item: ListItem<'a, M>) -> Elemen
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
let selected = item.selected;
|
||||
let base = item.base;
|
||||
let style = move |_theme: &iced::Theme, status: button::Status| {
|
||||
let state = forced.unwrap_or_else(|| Interaction::from_status(status));
|
||||
let colors = palette(ui, state, selected);
|
||||
let colors = palette(ui, base, state, selected);
|
||||
button::Style {
|
||||
background: colors.fill.map(Background::Color),
|
||||
text_color: colors.headline,
|
||||
|
||||
@@ -56,14 +56,15 @@ fn disabled_only_clears_the_message() {
|
||||
#[test]
|
||||
fn disabled_content_uses_on_surface() {
|
||||
let ui = ui();
|
||||
let palette = palette(&ui, Interaction::Disabled, false);
|
||||
let expected = interaction::disabled_content_over_role(&ui, Role::OnSurface, false);
|
||||
let palette = palette(&ui, Base::CANVAS, Interaction::Disabled, false);
|
||||
let expected =
|
||||
interaction::disabled_content_over_role(&ui, Base::CANVAS, Role::OnSurface, false);
|
||||
assert_eq!(palette.headline, expected);
|
||||
assert_eq!(palette.leading, expected);
|
||||
assert_eq!(palette.trailing, expected);
|
||||
assert_ne!(
|
||||
palette.headline,
|
||||
interaction::disabled_content_over_role(&ui, Role::OnSurfaceVariant, false),
|
||||
interaction::disabled_content_over_role(&ui, Base::CANVAS, Role::OnSurfaceVariant, false),
|
||||
"走默认的 OnSurfaceVariant 就会偏浅一档(这条钉住 A1 的角色)"
|
||||
);
|
||||
assert!(palette.fill.is_none(), "禁用不叠状态层容器");
|
||||
@@ -73,7 +74,7 @@ fn disabled_content_uses_on_surface() {
|
||||
#[test]
|
||||
fn selected_row_is_secondary_container() {
|
||||
let ui = ui();
|
||||
let palette = palette(&ui, Interaction::Enabled, true);
|
||||
let palette = palette(&ui, Base::CANVAS, Interaction::Enabled, true);
|
||||
assert_eq!(palette.fill, Some(ui.color(Role::SecondaryContainer)));
|
||||
assert_eq!(palette.headline, ui.color(Role::OnSecondaryContainer));
|
||||
assert_eq!(palette.trailing, ui.color(Role::Primary), "4.8");
|
||||
@@ -83,13 +84,13 @@ fn selected_row_is_secondary_container() {
|
||||
#[test]
|
||||
fn interaction_states_keep_the_content_colors() {
|
||||
let ui = ui();
|
||||
let resting = palette(&ui, Interaction::Enabled, false);
|
||||
let resting = palette(&ui, Base::CANVAS, Interaction::Enabled, false);
|
||||
for state in [
|
||||
Interaction::Hovered,
|
||||
Interaction::Focused,
|
||||
Interaction::Pressed,
|
||||
] {
|
||||
let active = palette(&ui, state, false);
|
||||
let active = palette(&ui, Base::CANVAS, state, false);
|
||||
assert_eq!(active.headline, resting.headline, "{state:?} 不许改文本色");
|
||||
assert_eq!(active.leading, resting.leading, "{state:?} 不许改图标色");
|
||||
assert!(active.fill.is_some(), "{state:?} 必须叠状态层");
|
||||
|
||||
@@ -127,11 +127,13 @@ fn list_item_rows_are_44_60_76() {
|
||||
let darkest = shot.darkest(&disabled).expect("禁用格里应有墨迹");
|
||||
let right = rgb(interaction::disabled_content_over_role(
|
||||
&ui,
|
||||
interaction::Base::CANVAS,
|
||||
Role::OnSurface,
|
||||
false,
|
||||
));
|
||||
let wrong = rgb(interaction::disabled_content_over_role(
|
||||
&ui,
|
||||
interaction::Base::CANVAS,
|
||||
Role::OnSurfaceVariant,
|
||||
false,
|
||||
));
|
||||
|
||||
@@ -50,7 +50,7 @@ use unlucky_design::typography::TypeRole;
|
||||
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::{TabsGeometry, TABS_ICON_HEIGHT};
|
||||
|
||||
@@ -133,6 +133,8 @@ impl<'a, M> Tab<'a, M> {
|
||||
pub struct Tabs<'a, M> {
|
||||
variant: TabsVariant,
|
||||
tabs: Vec<Tab<'a, M>>,
|
||||
/// 脚下那一层的底(M234,禁用态合成用)。
|
||||
base: Base,
|
||||
}
|
||||
|
||||
impl<'a, M> Tabs<'a, M> {
|
||||
@@ -141,6 +143,7 @@ impl<'a, M> Tabs<'a, M> {
|
||||
Self {
|
||||
variant: TabsVariant::Primary,
|
||||
tabs: Vec::new(),
|
||||
base: Base::CANVAS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +152,7 @@ impl<'a, M> Tabs<'a, M> {
|
||||
Self {
|
||||
variant: TabsVariant::Secondary,
|
||||
tabs: Vec::new(),
|
||||
base: Base::CANVAS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,14 +161,20 @@ impl<'a, M> Tabs<'a, M> {
|
||||
self.tabs.push(tab);
|
||||
self
|
||||
}
|
||||
|
||||
/// **告诉 tab 条它踩在什么底上**(M234):禁用态的 38% 标签/指示条叠在这个底上。
|
||||
pub fn base(mut self, base: impl Into<Base>) -> Self {
|
||||
self.base = base.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// 一个 tab 的内容色(A1 的 `Active*` / `Inactive*` / 各态表,见模块文档)。
|
||||
fn content_color(ui: &Ui, primary: bool, state: Interaction, selected: bool) -> Color {
|
||||
fn content_color(ui: &Ui, base: Base, primary: bool, state: Interaction, selected: bool) -> Color {
|
||||
if state.is_disabled() {
|
||||
// A1 的 Tabs 没有 Disabled* token(实读:Primary/Secondary 两套都只有 Active/Inactive × 四态),
|
||||
// 按 ListItem 的 38% 口径处理 —— 登记 `[估]`。
|
||||
return interaction::disabled_content_over_role(ui, Role::OnSurface, false);
|
||||
return interaction::disabled_content_over_role(ui, base, Role::OnSurface, false);
|
||||
}
|
||||
if selected {
|
||||
// 6.5:Primary 选中 `Primary`;Secondary 选中 `OnSurface`
|
||||
@@ -182,13 +192,14 @@ fn content_color(ui: &Ui, primary: bool, state: Interaction, selected: bool) ->
|
||||
}
|
||||
|
||||
/// Primary 指示条的颜色(只有选中项有;禁用项降档,否则「点不动却亮着」)。
|
||||
fn bar_color(ui: &Ui, state: Interaction, selected: bool) -> Option<Color> {
|
||||
fn bar_color(ui: &Ui, base: Base, state: Interaction, selected: bool) -> Option<Color> {
|
||||
if !selected {
|
||||
return None;
|
||||
}
|
||||
if state.is_disabled() {
|
||||
return Some(interaction::disabled_content_over_role(
|
||||
ui,
|
||||
base,
|
||||
Role::OnSurface,
|
||||
false,
|
||||
));
|
||||
@@ -207,6 +218,7 @@ fn secondary_line_color(ui: &Ui) -> Color {
|
||||
pub fn tabs<'a, M: Clone + 'a>(ui: &'a Ui, tabs: Tabs<'a, M>) -> Element<'a, M> {
|
||||
let geometry = TabsGeometry::of(ui);
|
||||
let primary = tabs.variant.is_primary();
|
||||
let base = tabs.base;
|
||||
// 6.1:带图标时容器高 **64**(`IconAndLabelTextContainerHeight`),纯文字 48
|
||||
let height = if tabs.tabs.iter().any(|tab| tab.icon.is_some()) {
|
||||
TABS_ICON_HEIGHT
|
||||
@@ -218,7 +230,7 @@ pub fn tabs<'a, M: Clone + 'a>(ui: &'a Ui, tabs: Tabs<'a, M>) -> Element<'a, M>
|
||||
.height(Length::Fixed(height))
|
||||
.align_y(Alignment::Center);
|
||||
for tab in tabs.tabs {
|
||||
row = row.push(tab_cell(ui, tab, geometry, primary, height));
|
||||
row = row.push(tab_cell(ui, base, tab, geometry, primary, height));
|
||||
}
|
||||
|
||||
let body: Element<'a, M> = if primary {
|
||||
@@ -254,8 +266,10 @@ pub fn tabs<'a, M: Clone + 'a>(ui: &'a Ui, tabs: Tabs<'a, M>) -> Element<'a, M>
|
||||
}
|
||||
|
||||
/// 一个 tab 单元(内容 + 底部条)。
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn tab_cell<'a, M: Clone + 'a>(
|
||||
ui: &'a Ui,
|
||||
base: Base,
|
||||
tab: Tab<'a, M>,
|
||||
geometry: TabsGeometry,
|
||||
primary: bool,
|
||||
@@ -268,7 +282,7 @@ fn tab_cell<'a, M: Clone + 'a>(
|
||||
});
|
||||
let state = forced.unwrap_or(Interaction::Enabled);
|
||||
let selected = tab.selected;
|
||||
let color = content_color(ui, primary, state, selected);
|
||||
let color = content_color(ui, base, primary, state, selected);
|
||||
let style = ui.style(TypeRole::TitleSmall);
|
||||
|
||||
let label = text(tab.label)
|
||||
@@ -291,7 +305,7 @@ fn tab_cell<'a, M: Clone + 'a>(
|
||||
|
||||
let bar_height = geometry.bar(primary);
|
||||
// 底层:内容自己决定宽度(Shrink),底部留出条的高度 —— 未选中的 Primary tab 也占同样的高
|
||||
let base = container(content)
|
||||
let cell = container(content)
|
||||
.height(Length::Fixed(height))
|
||||
// Q21:要给居中就两轴都显式给(`Container` 默认 Left/Top)
|
||||
.align_x(Alignment::Center)
|
||||
@@ -305,12 +319,12 @@ fn tab_cell<'a, M: Clone + 'a>(
|
||||
|
||||
// Primary:逐 tab 画条(要跟随该 tab 的宽度);Secondary:整行一条,在 `tabs()` 里画
|
||||
let bar = if primary {
|
||||
bar_color(ui, state, selected)
|
||||
bar_color(ui, base, state, selected)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let radius = geometry.indicator_radius();
|
||||
let mut stack = Stack::new().push(base);
|
||||
let mut stack = Stack::new().push(cell);
|
||||
if let Some(color) = bar {
|
||||
// 上层:Fill —— `Stack` 先按底层定尺寸,所以这里的 Fill = 这个 tab 的宽(不是整行)
|
||||
stack = stack.push(
|
||||
@@ -333,7 +347,7 @@ fn tab_cell<'a, M: Clone + 'a>(
|
||||
);
|
||||
}
|
||||
|
||||
let fill = interaction::overlay_color(ui, state, Role::OnSurface, Role::Surface);
|
||||
let fill = interaction::overlay_color(ui, state, Role::OnSurface, base.role());
|
||||
let button = button(stack)
|
||||
.height(Length::Fixed(height))
|
||||
.padding(0)
|
||||
|
||||
@@ -7,7 +7,7 @@ use unlucky_design::typography::Platform;
|
||||
|
||||
use crate::theme::Families;
|
||||
|
||||
use super::super::super::interaction;
|
||||
use super::super::super::interaction::{self, Base};
|
||||
|
||||
fn ui() -> Ui {
|
||||
Ui::with_platform(
|
||||
@@ -26,16 +26,16 @@ fn ui() -> Ui {
|
||||
fn active_colors_follow_the_variant() {
|
||||
let ui = ui();
|
||||
assert_eq!(
|
||||
content_color(&ui, true, Interaction::Enabled, true),
|
||||
content_color(&ui, Base::CANVAS, true, Interaction::Enabled, true),
|
||||
ui.color(Role::Primary)
|
||||
);
|
||||
assert_eq!(
|
||||
content_color(&ui, false, Interaction::Enabled, true),
|
||||
content_color(&ui, Base::CANVAS, false, Interaction::Enabled, true),
|
||||
ui.color(Role::OnSurface),
|
||||
"6.5:Secondary 的选中项是 OnSurface"
|
||||
);
|
||||
assert_ne!(
|
||||
content_color(&ui, false, Interaction::Enabled, true),
|
||||
content_color(&ui, Base::CANVAS, false, Interaction::Enabled, true),
|
||||
ui.color(Role::Primary),
|
||||
"把 Secondary 的选中色写成 Primary 是最常见的串线"
|
||||
);
|
||||
@@ -47,7 +47,7 @@ fn inactive_states_lift_to_on_surface() {
|
||||
let ui = ui();
|
||||
for primary in [true, false] {
|
||||
assert_eq!(
|
||||
content_color(&ui, primary, Interaction::Enabled, false),
|
||||
content_color(&ui, Base::CANVAS, primary, Interaction::Enabled, false),
|
||||
ui.color(Role::OnSurfaceVariant)
|
||||
);
|
||||
for state in [
|
||||
@@ -56,7 +56,7 @@ fn inactive_states_lift_to_on_surface() {
|
||||
Interaction::Pressed,
|
||||
] {
|
||||
assert_eq!(
|
||||
content_color(&ui, primary, state, false),
|
||||
content_color(&ui, Base::CANVAS, primary, state, false),
|
||||
ui.color(Role::OnSurface),
|
||||
"{state:?} 必须提到 OnSurface"
|
||||
);
|
||||
@@ -68,15 +68,19 @@ fn inactive_states_lift_to_on_surface() {
|
||||
#[test]
|
||||
fn primary_bar_only_on_the_selected_tab() {
|
||||
let ui = ui();
|
||||
assert_eq!(bar_color(&ui, Interaction::Enabled, false), None);
|
||||
assert_eq!(
|
||||
bar_color(&ui, Interaction::Enabled, true),
|
||||
bar_color(&ui, Base::CANVAS, Interaction::Enabled, false),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
bar_color(&ui, Base::CANVAS, Interaction::Enabled, true),
|
||||
Some(ui.color(Role::Primary))
|
||||
);
|
||||
assert_eq!(
|
||||
bar_color(&ui, Interaction::Disabled, true),
|
||||
bar_color(&ui, Base::CANVAS, Interaction::Disabled, true),
|
||||
Some(interaction::disabled_content_over_role(
|
||||
&ui,
|
||||
Base::CANVAS,
|
||||
Role::OnSurface,
|
||||
false
|
||||
)),
|
||||
|
||||
@@ -22,7 +22,7 @@ use unlucky_design::typography::TypeRole;
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::interaction::Interaction;
|
||||
use super::super::interaction::{Base, Interaction};
|
||||
use super::geometry::ControlGeometry;
|
||||
use super::paint::{self, Frame};
|
||||
use super::shell::{self, StateSource};
|
||||
@@ -58,6 +58,8 @@ pub struct Checkbox<'a, M> {
|
||||
on_toggle: Option<M>,
|
||||
error: bool,
|
||||
state: Option<Interaction>,
|
||||
/// 脚下那一层的底(M234,禁用态合成用)。
|
||||
base: Base,
|
||||
}
|
||||
|
||||
impl<'a, M> Checkbox<'a, M> {
|
||||
@@ -82,6 +84,7 @@ impl<'a, M> Checkbox<'a, M> {
|
||||
on_toggle: Some(on_toggle),
|
||||
error: false,
|
||||
state: None,
|
||||
base: Base::CANVAS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +96,7 @@ impl<'a, M> Checkbox<'a, M> {
|
||||
on_toggle: None,
|
||||
error: false,
|
||||
state: None,
|
||||
base: Base::CANVAS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +111,12 @@ impl<'a, M> Checkbox<'a, M> {
|
||||
self.state = Some(state);
|
||||
self
|
||||
}
|
||||
|
||||
/// **告诉复选框它踩在什么底上**(M234):禁用态的 38% 内容/描边叠在这个底上。
|
||||
pub fn base(mut self, base: impl Into<Base>) -> Self {
|
||||
self.base = base.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// 画一个复选框(框 + 标签)。
|
||||
@@ -117,11 +127,12 @@ pub fn checkbox<'a, M: Clone + 'a>(ui: &'a Ui, spec: Checkbox<'a, M>) -> Element
|
||||
let error = spec.error;
|
||||
let radius = geometry.radius;
|
||||
let stroke = geometry.stroke;
|
||||
let base = spec.base;
|
||||
let source = StateSource::new(spec.state, spec.on_toggle.is_some());
|
||||
|
||||
// 勾的颜色与 hover/pressed 无关(A1:`SelectedHover/Focus/PressedIconColor` 都是 `OnPrimary`),
|
||||
// 只随「选中 + 禁用」变 —— 所以这里用**静态**态求值(SVG 的颜色在构建期就要定)。
|
||||
let at_rest = paint::checkbox(ui, filled, error, source.get(), radius, stroke);
|
||||
let at_rest = paint::checkbox(ui, base, filled, error, source.get(), radius, stroke);
|
||||
let glyph: Element<'a, M> = if filled {
|
||||
shell::square(
|
||||
geometry.side,
|
||||
@@ -136,7 +147,7 @@ pub fn checkbox<'a, M: Clone + 'a>(ui: &'a Ui, spec: Checkbox<'a, M>) -> Element
|
||||
.width(Length::Fixed(geometry.side))
|
||||
.height(Length::Fixed(geometry.side))
|
||||
.style(source.container_style(move |state| {
|
||||
paint::checkbox(ui, filled, error, state, radius, stroke)
|
||||
paint::checkbox(ui, base, filled, error, state, radius, stroke)
|
||||
.frame
|
||||
.container_style()
|
||||
}));
|
||||
@@ -165,7 +176,7 @@ pub fn checkbox<'a, M: Clone + 'a>(ui: &'a Ui, spec: Checkbox<'a, M>) -> Element
|
||||
text(spec.label)
|
||||
.size(style.size_px)
|
||||
.font(ui.font_of(style))
|
||||
.color(paint::label(ui, source.get())),
|
||||
.color(paint::label(ui, base, source.get())),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ use unlucky_design::typography::TypeRole;
|
||||
use crate::icons;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::interaction::Interaction;
|
||||
use super::super::interaction::{Base, Interaction};
|
||||
use super::geometry::ChipGeometry;
|
||||
use super::paint::{self, ChipVariant};
|
||||
|
||||
@@ -58,6 +58,8 @@ pub struct Chip<'a, M> {
|
||||
state: Option<Interaction>,
|
||||
/// 「当前项」标记([`Chip::current`]):选中配色照画,但**不**降成禁用配色、也不吃五态。
|
||||
marker: bool,
|
||||
/// 脚下那一层的底(M234,禁用态合成用)。
|
||||
base: Base,
|
||||
}
|
||||
|
||||
impl<'a, M> Chip<'a, M> {
|
||||
@@ -94,6 +96,7 @@ impl<'a, M> Chip<'a, M> {
|
||||
on_press: None,
|
||||
state: None,
|
||||
marker: false,
|
||||
base: Base::CANVAS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +119,7 @@ impl<'a, M> Chip<'a, M> {
|
||||
on_press: None,
|
||||
state: None,
|
||||
marker: true,
|
||||
base: Base::CANVAS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +144,7 @@ impl<'a, M> Chip<'a, M> {
|
||||
on_press: Some(on_press),
|
||||
state: None,
|
||||
marker: false,
|
||||
base: Base::CANVAS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +166,12 @@ impl<'a, M> Chip<'a, M> {
|
||||
self
|
||||
}
|
||||
|
||||
/// **告诉 chip 它踩在什么底上**(M234):禁用态那 12% 容器/描边与 38% 内容叠在这个底上。
|
||||
pub fn base(mut self, base: impl Into<Base>) -> Self {
|
||||
self.base = base.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// 静息时的外观——**唯一**把「这颗 chip 属于哪个档位」翻成颜色的地方。
|
||||
///
|
||||
/// `chip()` 用它求文字/图标色,测试也用它量对比度:档位的判据(当前项 / 选中 / 禁用)
|
||||
@@ -177,7 +188,7 @@ impl<'a, M> Chip<'a, M> {
|
||||
} else {
|
||||
self.state.unwrap_or(Interaction::Enabled)
|
||||
};
|
||||
paint::chip(ui, self.variant, self.selected, state, radius)
|
||||
paint::chip(ui, self.base, self.variant, self.selected, state, radius)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +199,7 @@ pub fn chip<'a, M: Clone + 'a>(ui: &'a Ui, spec: Chip<'a, M>) -> Element<'a, M>
|
||||
let selected = spec.selected;
|
||||
let marker = spec.marker;
|
||||
let forced = spec.state;
|
||||
let base = spec.base;
|
||||
|
||||
// 文字/图标色只看「选中 + 禁用」(A1:Filter/Input 的 label 色在 hover/focus/pressed 不变),
|
||||
// 所以这里静态求值;容器(含状态层)在样式闭包里跟态走。档位的判据在 `Chip::paint` 一处
|
||||
@@ -241,7 +253,7 @@ pub fn chip<'a, M: Clone + 'a>(ui: &'a Ui, spec: Chip<'a, M>) -> Element<'a, M>
|
||||
paint::current_item(ui, geometry.radius)
|
||||
} else {
|
||||
let state = forced.unwrap_or_else(|| Interaction::from_status(status));
|
||||
paint::chip(ui, variant, selected, state, geometry.radius)
|
||||
paint::chip(ui, base, variant, selected, state, geometry.radius)
|
||||
};
|
||||
button::Style {
|
||||
background: paint.frame.fill.map(Background::Color),
|
||||
|
||||
@@ -4,7 +4,7 @@ use unlucky_design::color::Role;
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::super::interaction::Interaction;
|
||||
use super::super::super::interaction::{Base, Interaction};
|
||||
use super::{disabled_ink, lifted, CheckboxPaint, Frame};
|
||||
|
||||
/// Checkbox 的外观。
|
||||
@@ -13,6 +13,7 @@ use super::{disabled_ink, lifted, CheckboxPaint, Frame};
|
||||
/// 选中态没有描边(`SelectedOutlineWidth = 0`,走填充分支)。
|
||||
pub fn checkbox(
|
||||
ui: &Ui,
|
||||
base: Base,
|
||||
selected: bool,
|
||||
error: bool,
|
||||
state: Interaction,
|
||||
@@ -25,8 +26,8 @@ pub fn checkbox(
|
||||
// A1:`SelectedDisabledContainerColor = OnSurface` +
|
||||
// `SelectedDisabledContainerOpacity = 0.38` —— **不是** 12%(复审 P2-3)。
|
||||
// 与未选中禁用的描边是同一个 38% 叠色,两格看起来才成对。
|
||||
fill: selected.then(|| disabled_ink(ui, Role::OnSurface)),
|
||||
outline: (!selected).then(|| disabled_ink(ui, Role::OnSurface)),
|
||||
fill: selected.then(|| disabled_ink(ui, base, Role::OnSurface)),
|
||||
outline: (!selected).then(|| disabled_ink(ui, base, Role::OnSurface)),
|
||||
stroke,
|
||||
radius,
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ use unlucky_design::color::Role;
|
||||
|
||||
use crate::theme::{Ui, STROKE_PX};
|
||||
|
||||
use super::super::super::interaction::Interaction;
|
||||
use super::super::super::interaction::{Base, Interaction};
|
||||
use super::{disabled_container, disabled_ink, state_layer, ChipPaint, Frame};
|
||||
|
||||
/// Chip 的三种(§13.6 / §13.7):Assist / Filter / Input。
|
||||
@@ -24,6 +24,7 @@ pub enum ChipVariant {
|
||||
/// 所以未选中 + hover 的 fill 是「状态层叠在 surface 上」的实色。
|
||||
pub fn chip(
|
||||
ui: &Ui,
|
||||
base: Base,
|
||||
variant: ChipVariant,
|
||||
selected: bool,
|
||||
state: Interaction,
|
||||
@@ -39,11 +40,11 @@ pub fn chip(
|
||||
};
|
||||
|
||||
if state.is_disabled() {
|
||||
let ink = disabled_ink(ui, Role::OnSurface);
|
||||
let ink = disabled_ink(ui, base, Role::OnSurface);
|
||||
return ChipPaint {
|
||||
frame: Frame {
|
||||
fill: selected.then(|| disabled_container(ui)),
|
||||
outline: (!selected).then(|| disabled_container(ui)),
|
||||
fill: selected.then(|| disabled_container(ui, base)),
|
||||
outline: (!selected).then(|| disabled_container(ui, base)),
|
||||
stroke: STROKE_PX,
|
||||
radius,
|
||||
},
|
||||
@@ -102,5 +103,12 @@ pub fn chip(
|
||||
/// pressed / disabled 五态(见 `Chip::current` 的文档)。对比度下限是 [`crate::components::selection::paint`]
|
||||
/// 同一条正文口径(WCAG AA 4.5:1,`unlucky_design::palette::wcag::AA_TEXT`)。
|
||||
pub fn current_item(ui: &Ui, radius: f32) -> ChipPaint {
|
||||
chip(ui, ChipVariant::Filter, true, Interaction::Enabled, radius)
|
||||
chip(
|
||||
ui,
|
||||
Base::CANVAS,
|
||||
ChipVariant::Filter,
|
||||
true,
|
||||
Interaction::Enabled,
|
||||
radius,
|
||||
)
|
||||
}
|
||||
@@ -27,7 +27,7 @@ use unlucky_design::color::Role;
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::interaction::{self, Interaction};
|
||||
use super::super::interaction::{self, Base, Interaction};
|
||||
|
||||
mod check;
|
||||
mod chip;
|
||||
@@ -115,17 +115,21 @@ pub fn state_layer(ui: &Ui, state: Interaction, content: Role) -> Option<Color>
|
||||
interaction::overlay_color(ui, state, content, Role::Surface)
|
||||
}
|
||||
|
||||
/// `OnSurface`@12% 叠 surface(**容器**禁用色:Switch 轨道、Chip 未选中描边)。
|
||||
/// `OnSurface`@12% 叠 `base`(**容器**禁用色:Switch 轨道、Chip 未选中描边)。
|
||||
///
|
||||
/// `base` 是控件**脚下那一层的底**(M234):画布 = [`Base::CANVAS`]、卡片 =
|
||||
/// `surfaces::elevated::BASE`。M229 之前这里写死 `Role::Surface`,卡片里的禁用 chip
|
||||
/// 于是把 12% 叠在画布色上。
|
||||
///
|
||||
/// 注意:Checkbox「选中且禁用」的**填充**不是这个 12%,A1 的
|
||||
/// `SelectedDisabledContainerOpacity = 0.38` → 见 [`disabled_ink`](复审 P2-3)。
|
||||
pub(super) fn disabled_container(ui: &Ui) -> Color {
|
||||
interaction::disabled_colors(ui, Role::Surface).0
|
||||
pub(super) fn disabled_container(ui: &Ui, base: Base) -> Color {
|
||||
interaction::disabled_colors(ui, base).0
|
||||
}
|
||||
|
||||
/// 内容禁用色:`role` @38% 叠在 surface 上(`on_container = false`)。
|
||||
pub(super) fn disabled_ink(ui: &Ui, role: Role) -> Color {
|
||||
interaction::disabled_content_over_role(ui, role, false)
|
||||
/// 内容禁用色:`role` @38% 叠在 `base` 上(`on_container = false`)。
|
||||
pub(super) fn disabled_ink(ui: &Ui, base: Base, role: Role) -> Color {
|
||||
interaction::disabled_content_over_role(ui, base, role, false)
|
||||
}
|
||||
|
||||
/// hover / focus / pressed(「抬起来」的三态)。
|
||||
@@ -140,9 +144,9 @@ pub(super) fn lifted(state: Interaction) -> bool {
|
||||
///
|
||||
/// A1 里 checkbox/radio 都没有「标签色随 hover 变」的字段(标签不属于控件本体),
|
||||
/// 所以这里只看禁用与否。
|
||||
pub fn label(ui: &Ui, state: Interaction) -> Color {
|
||||
pub fn label(ui: &Ui, base: Base, state: Interaction) -> Color {
|
||||
if state.is_disabled() {
|
||||
disabled_ink(ui, Role::OnSurface)
|
||||
disabled_ink(ui, base, Role::OnSurface)
|
||||
} else {
|
||||
ui.color(Role::OnSurface)
|
||||
}
|
||||
|
||||
@@ -4,15 +4,22 @@ use unlucky_design::color::Role;
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::super::interaction::Interaction;
|
||||
use super::super::super::interaction::{Base, Interaction};
|
||||
use super::{disabled_ink, lifted, Frame, RadioPaint};
|
||||
|
||||
/// Radio 的外观。
|
||||
///
|
||||
/// **选中态 hover 不换色**:环与点在四态里都是 `Primary`(只有状态层变)。
|
||||
pub fn radio(ui: &Ui, selected: bool, state: Interaction, radius: f32, stroke: f32) -> RadioPaint {
|
||||
pub fn radio(
|
||||
ui: &Ui,
|
||||
base: Base,
|
||||
selected: bool,
|
||||
state: Interaction,
|
||||
radius: f32,
|
||||
stroke: f32,
|
||||
) -> RadioPaint {
|
||||
let ink = if state.is_disabled() {
|
||||
disabled_ink(ui, Role::OnSurface)
|
||||
disabled_ink(ui, base, Role::OnSurface)
|
||||
} else if selected {
|
||||
ui.color(Role::Primary)
|
||||
} else if lifted(state) {
|
||||
|
||||
@@ -7,17 +7,17 @@ use unlucky_design::shape::ShapeScale;
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::super::interaction::{self, Interaction};
|
||||
use super::super::super::interaction::{self, Base, Interaction};
|
||||
use super::super::geometry::SwitchGeometry;
|
||||
use super::{disabled_container, disabled_ink, lifted, Frame, SwitchPaint};
|
||||
|
||||
/// Switch 的外观。
|
||||
pub fn switch(ui: &Ui, selected: bool, state: Interaction) -> SwitchPaint {
|
||||
pub fn switch(ui: &Ui, base: Base, selected: bool, state: Interaction) -> SwitchPaint {
|
||||
let geometry = SwitchGeometry::of(ui);
|
||||
let radius = ShapeScale::Full.radius_px(geometry.track_height);
|
||||
|
||||
if state.is_disabled() {
|
||||
let container = disabled_container(ui);
|
||||
let container = disabled_container(ui, base);
|
||||
return if selected {
|
||||
SwitchPaint {
|
||||
track: Frame {
|
||||
@@ -28,7 +28,7 @@ pub fn switch(ui: &Ui, selected: bool, state: Interaction) -> SwitchPaint {
|
||||
},
|
||||
// `DisabledSelectedHandleColor = Surface` 且不透明度 1(A1 的 fconst_1)
|
||||
thumb: ui.color(Role::Surface),
|
||||
icon: Some(disabled_ink(ui, Role::OnSurface)),
|
||||
icon: Some(disabled_ink(ui, base, Role::OnSurface)),
|
||||
}
|
||||
} else {
|
||||
SwitchPaint {
|
||||
@@ -38,7 +38,7 @@ pub fn switch(ui: &Ui, selected: bool, state: Interaction) -> SwitchPaint {
|
||||
stroke: geometry.track_stroke,
|
||||
radius,
|
||||
},
|
||||
thumb: disabled_ink(ui, Role::OnSurface),
|
||||
thumb: disabled_ink(ui, base, Role::OnSurface),
|
||||
icon: None,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
use unlucky_design::color::{Role, SchemeFlavor};
|
||||
use unlucky_design::typography::Platform;
|
||||
|
||||
use super::super::interaction::{self, Interaction};
|
||||
use super::super::interaction::{self, Base, Interaction};
|
||||
use super::pixel::{center, dims, ink_boxes, inner_ink, matches, near, rgb, shot_path, Shot};
|
||||
use crate::theme::{Families, Ui};
|
||||
|
||||
@@ -273,7 +273,7 @@ fn selection_geometry_matches_the_reference() {
|
||||
// 判据:禁用列里必须**恰好有一块** 12% 填充的 chip(= filter·on)。旧 API 把 selected
|
||||
// 一起删掉,这一块根本不存在(实测 0 块);能查到它、且宽度 ≥58,说明选中态与图标都还在。
|
||||
// (未选中的那三个禁用 chip 只有 1px 描边,落在半像素上时覆盖率不足 50%,量不稳,故不在此断言。)
|
||||
let disabled_container = interaction::disabled_colors(&ui, Role::Surface).0;
|
||||
let disabled_container = interaction::disabled_colors(&ui, Base::CANVAS).0;
|
||||
let disabled_chips: Vec<_> = ink_boxes(&shot, disabled_container, surface, 28, 140)
|
||||
.into_iter()
|
||||
.filter(|bbox| near(bbox.height(), 32) && bbox.width() >= 58)
|
||||
@@ -292,7 +292,8 @@ fn selection_geometry_matches_the_reference() {
|
||||
);
|
||||
|
||||
// ---- 复审 P2-3:**选中且禁用**的 Checkbox 容器是 38%,不是 12% -----------------
|
||||
let disabled_ink = interaction::disabled_content_over_role(&ui, Role::OnSurface, false);
|
||||
let disabled_ink =
|
||||
interaction::disabled_content_over_role(&ui, Base::CANVAS, Role::OnSurface, false);
|
||||
let ink38: Vec<_> = ink_boxes(&shot, disabled_ink, surface, 17, 24)
|
||||
.into_iter()
|
||||
.filter(|bbox| near(bbox.width(), 20) && near(bbox.height(), 20))
|
||||
|
||||
@@ -22,7 +22,7 @@ use unlucky_design::typography::TypeRole;
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::interaction::Interaction;
|
||||
use super::super::interaction::{Base, Interaction};
|
||||
use super::geometry::ControlGeometry;
|
||||
use super::paint::{self, Frame};
|
||||
use super::shell::{self, StateSource};
|
||||
@@ -34,6 +34,8 @@ pub struct Radio<'a, M> {
|
||||
selected: bool,
|
||||
on_select: Option<M>,
|
||||
state: Option<Interaction>,
|
||||
/// 脚下那一层的底(M234,禁用态合成用)。
|
||||
base: Base,
|
||||
}
|
||||
|
||||
impl<'a, M> Radio<'a, M> {
|
||||
@@ -44,6 +46,7 @@ impl<'a, M> Radio<'a, M> {
|
||||
selected,
|
||||
on_select: Some(on_select),
|
||||
state: None,
|
||||
base: Base::CANVAS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +57,7 @@ impl<'a, M> Radio<'a, M> {
|
||||
selected,
|
||||
on_select: None,
|
||||
state: None,
|
||||
base: Base::CANVAS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +66,12 @@ impl<'a, M> Radio<'a, M> {
|
||||
self.state = Some(state);
|
||||
self
|
||||
}
|
||||
|
||||
/// **告诉单选框它踩在什么底上**(M234):禁用态的 38% 环/内点叠在这个底上。
|
||||
pub fn base(mut self, base: impl Into<Base>) -> Self {
|
||||
self.base = base.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// 画一个单选框(环 + 内点 + 标签)。
|
||||
@@ -71,11 +81,12 @@ pub fn radio<'a, M: Clone + 'a>(ui: &'a Ui, spec: Radio<'a, M>) -> Element<'a, M
|
||||
let radius = geometry.radius;
|
||||
let stroke = geometry.stroke;
|
||||
let dot_side = geometry.inner_dot;
|
||||
let base = spec.base;
|
||||
let source = StateSource::new(spec.state, spec.on_select.is_some());
|
||||
|
||||
// 内点与环的颜色与 hover/pressed 无关(A1:选中态四态都是 `Primary`),
|
||||
// 只随「选中 + 禁用」变 —— 所以点的颜色在这里静态求值即可。
|
||||
let at_rest = paint::radio(ui, selected, source.get(), radius, stroke);
|
||||
let at_rest = paint::radio(ui, base, selected, source.get(), radius, stroke);
|
||||
|
||||
// Ø10 内点(半径 = 直径/2)
|
||||
let dot: Element<'a, M> = match at_rest.dot {
|
||||
@@ -102,7 +113,7 @@ pub fn radio<'a, M: Clone + 'a>(ui: &'a Ui, spec: Radio<'a, M>) -> Element<'a, M
|
||||
.align_x(Alignment::Center)
|
||||
.align_y(Alignment::Center)
|
||||
.style(source.container_style(move |state| {
|
||||
paint::radio(ui, selected, state, radius, stroke)
|
||||
paint::radio(ui, base, selected, state, radius, stroke)
|
||||
.ring
|
||||
.container_style()
|
||||
}));
|
||||
@@ -131,7 +142,7 @@ pub fn radio<'a, M: Clone + 'a>(ui: &'a Ui, spec: Radio<'a, M>) -> Element<'a, M
|
||||
text(spec.label)
|
||||
.size(style.size_px)
|
||||
.font(ui.font_of(style))
|
||||
.color(paint::label(ui, source.get())),
|
||||
.color(paint::label(ui, base, source.get())),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ use unlucky_design::typography::TypeRole;
|
||||
use crate::icons;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::interaction::Interaction;
|
||||
use super::super::interaction::{Base, Interaction};
|
||||
use super::geometry::SwitchGeometry;
|
||||
use super::paint::{self, Frame};
|
||||
use super::shell::{self, StateSource};
|
||||
@@ -36,6 +36,8 @@ pub struct Switch<'a, M> {
|
||||
value: bool,
|
||||
on_toggle: Option<M>,
|
||||
state: Option<Interaction>,
|
||||
/// 脚下那一层的底(M234,禁用态合成用)。
|
||||
base: Base,
|
||||
}
|
||||
|
||||
impl<'a, M> Switch<'a, M> {
|
||||
@@ -46,6 +48,7 @@ impl<'a, M> Switch<'a, M> {
|
||||
value,
|
||||
on_toggle: Some(on_toggle),
|
||||
state: None,
|
||||
base: Base::CANVAS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +59,7 @@ impl<'a, M> Switch<'a, M> {
|
||||
value,
|
||||
on_toggle: None,
|
||||
state: None,
|
||||
base: Base::CANVAS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +68,12 @@ impl<'a, M> Switch<'a, M> {
|
||||
self.state = Some(state);
|
||||
self
|
||||
}
|
||||
|
||||
/// **告诉开关它踩在什么底上**(M234):禁用态的 12% 轨道 / 38% 滑块叠在这个底上。
|
||||
pub fn base(mut self, base: impl Into<Base>) -> Self {
|
||||
self.base = base.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// 画一个开关(轨道 + 滑块 + 标签)。
|
||||
@@ -71,11 +81,12 @@ pub fn switch<'a, M: Clone + 'a>(ui: &'a Ui, spec: Switch<'a, M>) -> Element<'a,
|
||||
let geometry = SwitchGeometry::of(ui);
|
||||
let selected = spec.value;
|
||||
let thumb_side = geometry.thumb(selected, false);
|
||||
let base = spec.base;
|
||||
let source = StateSource::new(spec.state, spec.on_toggle.is_some());
|
||||
|
||||
// 滑块内的勾:颜色只随选中/禁用变(A1 的 `Selected*IconColor` 都是 `OnPrimaryContainer`),
|
||||
// 所以静态求值;未选中态官方图标色 = 轨道色(画了也看不见),因此不画。
|
||||
let at_rest = paint::switch(ui, selected, source.get());
|
||||
let at_rest = paint::switch(ui, base, selected, source.get());
|
||||
let glyph: Element<'a, M> = match at_rest.icon {
|
||||
Some(ink) => super::super::layout::centered_fixed(
|
||||
geometry.icon,
|
||||
@@ -94,7 +105,7 @@ pub fn switch<'a, M: Clone + 'a>(ui: &'a Ui, spec: Switch<'a, M>) -> Element<'a,
|
||||
.align_y(Alignment::Center)
|
||||
.style(source.container_style(move |state| {
|
||||
Frame {
|
||||
fill: Some(paint::switch(ui, selected, state).thumb),
|
||||
fill: Some(paint::switch(ui, base, selected, state).thumb),
|
||||
outline: None,
|
||||
stroke: 0.0,
|
||||
radius: thumb_side / 2.0,
|
||||
@@ -124,7 +135,9 @@ pub fn switch<'a, M: Clone + 'a>(ui: &'a Ui, spec: Switch<'a, M>) -> Element<'a,
|
||||
.width(Length::Fixed(geometry.track_width))
|
||||
.height(Length::Fixed(geometry.track_height))
|
||||
.style(source.container_style(move |state| {
|
||||
paint::switch(ui, selected, state).track.container_style()
|
||||
paint::switch(ui, base, selected, state)
|
||||
.track
|
||||
.container_style()
|
||||
}));
|
||||
|
||||
// 绘制区 = 轨道 + 状态层外扩;两层叠放(层序:轨道在下,滑块与状态层在上)
|
||||
@@ -159,7 +172,7 @@ pub fn switch<'a, M: Clone + 'a>(ui: &'a Ui, spec: Switch<'a, M>) -> Element<'a,
|
||||
text(spec.label)
|
||||
.size(style.size_px)
|
||||
.font(ui.font_of(style))
|
||||
.color(paint::label(ui, source.get())),
|
||||
.color(paint::label(ui, base, source.get())),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
use unlucky_design::color::{Role, SchemeFlavor};
|
||||
use unlucky_design::typography::Platform;
|
||||
|
||||
use super::super::interaction::{self, Interaction};
|
||||
use super::super::interaction::{self, Base, Interaction};
|
||||
|
||||
mod chip;
|
||||
use super::geometry::{ChipGeometry, ControlGeometry, SwitchGeometry};
|
||||
use super::paint::{self, ChipVariant};
|
||||
use crate::theme::{Families, Ui};
|
||||
@@ -156,7 +158,15 @@ fn chip_geometry_matches_the_tokens() {
|
||||
fn checkbox_unselected_lifts_the_outline_to_on_surface() {
|
||||
let ui = ui();
|
||||
let g = ControlGeometry::checkbox(&ui);
|
||||
let at_rest = paint::checkbox(&ui, false, false, Interaction::Enabled, g.radius, g.stroke);
|
||||
let at_rest = paint::checkbox(
|
||||
&ui,
|
||||
Base::CANVAS,
|
||||
false,
|
||||
false,
|
||||
Interaction::Enabled,
|
||||
g.radius,
|
||||
g.stroke,
|
||||
);
|
||||
assert_eq!(
|
||||
at_rest.frame.outline,
|
||||
Some(ui.color(Role::OnSurfaceVariant))
|
||||
@@ -166,7 +176,7 @@ fn checkbox_unselected_lifts_the_outline_to_on_surface() {
|
||||
Interaction::Focused,
|
||||
Interaction::Pressed,
|
||||
] {
|
||||
let lifted = paint::checkbox(&ui, false, false, state, g.radius, g.stroke);
|
||||
let lifted = paint::checkbox(&ui, Base::CANVAS, false, false, state, g.radius, g.stroke);
|
||||
assert_eq!(
|
||||
lifted.frame.outline,
|
||||
Some(ui.color(Role::OnSurface)),
|
||||
@@ -175,7 +185,15 @@ fn checkbox_unselected_lifts_the_outline_to_on_surface() {
|
||||
assert_eq!(lifted.frame.stroke, 2.0, "18.8:描边宽度不随态变");
|
||||
}
|
||||
// 错误态优先于 hover
|
||||
let error = paint::checkbox(&ui, false, true, Interaction::Hovered, g.radius, g.stroke);
|
||||
let error = paint::checkbox(
|
||||
&ui,
|
||||
Base::CANVAS,
|
||||
false,
|
||||
true,
|
||||
Interaction::Hovered,
|
||||
g.radius,
|
||||
g.stroke,
|
||||
);
|
||||
assert_eq!(error.frame.outline, Some(ui.color(Role::Error)));
|
||||
}
|
||||
|
||||
@@ -184,7 +202,15 @@ fn checkbox_unselected_lifts_the_outline_to_on_surface() {
|
||||
fn checkbox_disabled_keeps_the_stroke_and_uses_the_a1_roles() {
|
||||
let ui = ui();
|
||||
let g = ControlGeometry::checkbox(&ui);
|
||||
let unchecked = paint::checkbox(&ui, false, false, Interaction::Disabled, g.radius, g.stroke);
|
||||
let unchecked = paint::checkbox(
|
||||
&ui,
|
||||
Base::CANVAS,
|
||||
false,
|
||||
false,
|
||||
Interaction::Disabled,
|
||||
g.radius,
|
||||
g.stroke,
|
||||
);
|
||||
assert_eq!(unchecked.frame.stroke, 2.0, "18.8:禁用描边仍 2px");
|
||||
assert_eq!(unchecked.frame.fill, None, "18.8:未选中禁用内部仍透明");
|
||||
assert!(
|
||||
@@ -195,7 +221,15 @@ fn checkbox_disabled_keeps_the_stroke_and_uses_the_a1_roles() {
|
||||
"18.8:禁用描边是 OnSurface@38%,不是满档角色色"
|
||||
);
|
||||
|
||||
let checked = paint::checkbox(&ui, true, false, Interaction::Disabled, g.radius, g.stroke);
|
||||
let checked = paint::checkbox(
|
||||
&ui,
|
||||
Base::CANVAS,
|
||||
true,
|
||||
false,
|
||||
Interaction::Disabled,
|
||||
g.radius,
|
||||
g.stroke,
|
||||
);
|
||||
assert_eq!(
|
||||
checked.glyph,
|
||||
ui.color(Role::Surface),
|
||||
@@ -203,8 +237,8 @@ fn checkbox_disabled_keeps_the_stroke_and_uses_the_a1_roles() {
|
||||
);
|
||||
// 复审 P2-3:**选中且禁用**的填充是 OnSurface@38%(`SelectedDisabledContainerOpacity`),
|
||||
// 不是普通禁用容器的 12%。两格差一眼就能看出来,所以逐字钉住。
|
||||
let ink38 = interaction::disabled_content_over_role(&ui, Role::OnSurface, false);
|
||||
let container12 = interaction::disabled_colors(&ui, Role::Surface).0;
|
||||
let ink38 = interaction::disabled_content_over_role(&ui, Base::CANVAS, Role::OnSurface, false);
|
||||
let container12 = interaction::disabled_colors(&ui, Base::CANVAS).0;
|
||||
assert_eq!(
|
||||
checked.frame.fill,
|
||||
Some(ink38),
|
||||
@@ -232,7 +266,7 @@ fn radio_keeps_primary_on_hover_when_selected() {
|
||||
if state.is_disabled() {
|
||||
continue;
|
||||
}
|
||||
let paint = paint::radio(&ui, true, state, g.radius, g.stroke);
|
||||
let paint = paint::radio(&ui, Base::CANVAS, true, state, g.radius, g.stroke);
|
||||
assert_eq!(
|
||||
paint.ring.outline, primary,
|
||||
"19.4:{state:?} 环仍是 Primary"
|
||||
@@ -240,10 +274,24 @@ fn radio_keeps_primary_on_hover_when_selected() {
|
||||
assert_eq!(paint.dot, primary, "19.4:{state:?} 点仍是 Primary");
|
||||
}
|
||||
// 未选中:静息 OnSurfaceVariant → hover/focus/pressed 提到 OnSurface
|
||||
let at_rest = paint::radio(&ui, false, Interaction::Enabled, g.radius, g.stroke);
|
||||
let at_rest = paint::radio(
|
||||
&ui,
|
||||
Base::CANVAS,
|
||||
false,
|
||||
Interaction::Enabled,
|
||||
g.radius,
|
||||
g.stroke,
|
||||
);
|
||||
assert_eq!(at_rest.dot, None, "未选中不画内点");
|
||||
assert_eq!(at_rest.ring.outline, Some(ui.color(Role::OnSurfaceVariant)));
|
||||
let lifted = paint::radio(&ui, false, Interaction::Hovered, g.radius, g.stroke);
|
||||
let lifted = paint::radio(
|
||||
&ui,
|
||||
Base::CANVAS,
|
||||
false,
|
||||
Interaction::Hovered,
|
||||
g.radius,
|
||||
g.stroke,
|
||||
);
|
||||
assert_eq!(lifted.ring.outline, Some(ui.color(Role::OnSurface)));
|
||||
}
|
||||
|
||||
@@ -251,12 +299,12 @@ fn radio_keeps_primary_on_hover_when_selected() {
|
||||
#[test]
|
||||
fn switch_handle_roles_follow_the_tokens() {
|
||||
let ui = ui();
|
||||
let at_rest = paint::switch(&ui, true, Interaction::Enabled);
|
||||
let at_rest = paint::switch(&ui, Base::CANVAS, true, Interaction::Enabled);
|
||||
assert_eq!(at_rest.track.fill, Some(ui.color(Role::Primary)));
|
||||
assert_eq!(at_rest.thumb, ui.color(Role::OnPrimary));
|
||||
assert_eq!(at_rest.icon, Some(ui.color(Role::OnPrimaryContainer)));
|
||||
|
||||
let hovered = paint::switch(&ui, true, Interaction::Hovered);
|
||||
let hovered = paint::switch(&ui, Base::CANVAS, true, Interaction::Hovered);
|
||||
assert_eq!(
|
||||
hovered.thumb,
|
||||
ui.color(Role::PrimaryContainer),
|
||||
@@ -268,7 +316,7 @@ fn switch_handle_roles_follow_the_tokens() {
|
||||
"轨道不随 hover 变"
|
||||
);
|
||||
|
||||
let off = paint::switch(&ui, false, Interaction::Enabled);
|
||||
let off = paint::switch(&ui, Base::CANVAS, false, Interaction::Enabled);
|
||||
assert_eq!(
|
||||
off.track.outline,
|
||||
Some(ui.color(Role::Outline)),
|
||||
@@ -282,79 +330,10 @@ fn switch_handle_roles_follow_the_tokens() {
|
||||
);
|
||||
assert_eq!(off.icon, None, "未选中态图标与轨道同色(不画)");
|
||||
|
||||
let disabled = paint::switch(&ui, true, Interaction::Disabled);
|
||||
let disabled = paint::switch(&ui, Base::CANVAS, true, Interaction::Disabled);
|
||||
assert_eq!(
|
||||
disabled.thumb,
|
||||
ui.color(Role::Surface),
|
||||
"A1:DisabledSelectedHandleColor=Surface"
|
||||
);
|
||||
}
|
||||
|
||||
/// §13.5 / §13.6 / §13.7:选中态**无描边**、Assist 不可选中、禁用文字 @38%。
|
||||
#[test]
|
||||
fn chip_roles_follow_the_tokens() {
|
||||
let ui = ui();
|
||||
let radius = ChipGeometry::of(&ui).radius;
|
||||
|
||||
let selected = paint::chip(&ui, ChipVariant::Filter, true, Interaction::Enabled, radius);
|
||||
assert_eq!(
|
||||
selected.frame.fill,
|
||||
Some(ui.color(Role::SecondaryContainer))
|
||||
);
|
||||
assert_eq!(selected.frame.outline, None, "13.5:选中描边 0dp");
|
||||
assert_eq!(selected.label, ui.color(Role::OnSecondaryContainer));
|
||||
|
||||
let unselected = paint::chip(
|
||||
&ui,
|
||||
ChipVariant::Filter,
|
||||
false,
|
||||
Interaction::Enabled,
|
||||
radius,
|
||||
);
|
||||
assert_eq!(
|
||||
unselected.frame.outline,
|
||||
Some(ui.color(Role::OutlineVariant))
|
||||
);
|
||||
assert_eq!(unselected.label, ui.color(Role::OnSurfaceVariant));
|
||||
|
||||
// Assist 没有选中态:即使传 selected = true 也不会变 SecondaryContainer
|
||||
let assist = paint::chip(&ui, ChipVariant::Assist, true, Interaction::Enabled, radius);
|
||||
assert_ne!(assist.frame.fill, Some(ui.color(Role::SecondaryContainer)));
|
||||
assert_eq!(
|
||||
assist.icon,
|
||||
ui.color(Role::Primary),
|
||||
"13.4:Assist 前置图标 Primary"
|
||||
);
|
||||
|
||||
let disabled = paint::chip(
|
||||
&ui,
|
||||
ChipVariant::Filter,
|
||||
false,
|
||||
Interaction::Disabled,
|
||||
radius,
|
||||
);
|
||||
assert_ne!(disabled.label, unselected.label, "13.7:禁用文字降到 38%");
|
||||
}
|
||||
|
||||
/// 状态层只在三态存在,且**不是**满档角色色(α 由 `interaction` 给)。
|
||||
#[test]
|
||||
fn state_layer_only_exists_on_lifted_states() {
|
||||
let ui = ui();
|
||||
for (state, expected) in [
|
||||
(Interaction::Enabled, false),
|
||||
(Interaction::Hovered, true),
|
||||
(Interaction::Focused, true),
|
||||
(Interaction::Pressed, true),
|
||||
(Interaction::Disabled, false),
|
||||
] {
|
||||
let layer = paint::state_layer(&ui, 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();
|
||||
assert_ne!(hover, pressed);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
//! Chip 角色与状态层的单测(M234 从 `selection/tests.rs` 纯搬移,守 400 行硬线)。
|
||||
//!
|
||||
//! 搬移零改动:判据与注释一字未动,只是**换文件**(`tests.rs` 加了 `.base(..)` 之后到 422 行)。
|
||||
|
||||
use super::*;
|
||||
|
||||
/// §13.5 / §13.6 / §13.7:选中态**无描边**、Assist 不可选中、禁用文字 @38%。
|
||||
#[test]
|
||||
fn chip_roles_follow_the_tokens() {
|
||||
let ui = ui();
|
||||
let radius = ChipGeometry::of(&ui).radius;
|
||||
|
||||
let selected = paint::chip(
|
||||
&ui,
|
||||
Base::CANVAS,
|
||||
ChipVariant::Filter,
|
||||
true,
|
||||
Interaction::Enabled,
|
||||
radius,
|
||||
);
|
||||
assert_eq!(
|
||||
selected.frame.fill,
|
||||
Some(ui.color(Role::SecondaryContainer))
|
||||
);
|
||||
assert_eq!(selected.frame.outline, None, "13.5:选中描边 0dp");
|
||||
assert_eq!(selected.label, ui.color(Role::OnSecondaryContainer));
|
||||
|
||||
let unselected = paint::chip(
|
||||
&ui,
|
||||
Base::CANVAS,
|
||||
ChipVariant::Filter,
|
||||
false,
|
||||
Interaction::Enabled,
|
||||
radius,
|
||||
);
|
||||
assert_eq!(
|
||||
unselected.frame.outline,
|
||||
Some(ui.color(Role::OutlineVariant))
|
||||
);
|
||||
assert_eq!(unselected.label, ui.color(Role::OnSurfaceVariant));
|
||||
|
||||
// Assist 没有选中态:即使传 selected = true 也不会变 SecondaryContainer
|
||||
let assist = paint::chip(
|
||||
&ui,
|
||||
Base::CANVAS,
|
||||
ChipVariant::Assist,
|
||||
true,
|
||||
Interaction::Enabled,
|
||||
radius,
|
||||
);
|
||||
assert_ne!(assist.frame.fill, Some(ui.color(Role::SecondaryContainer)));
|
||||
assert_eq!(
|
||||
assist.icon,
|
||||
ui.color(Role::Primary),
|
||||
"13.4:Assist 前置图标 Primary"
|
||||
);
|
||||
|
||||
let disabled = paint::chip(
|
||||
&ui,
|
||||
Base::CANVAS,
|
||||
ChipVariant::Filter,
|
||||
false,
|
||||
Interaction::Disabled,
|
||||
radius,
|
||||
);
|
||||
assert_ne!(disabled.label, unselected.label, "13.7:禁用文字降到 38%");
|
||||
}
|
||||
|
||||
/// 状态层只在三态存在,且**不是**满档角色色(α 由 `interaction` 给)。
|
||||
#[test]
|
||||
fn state_layer_only_exists_on_lifted_states() {
|
||||
let ui = ui();
|
||||
for (state, expected) in [
|
||||
(Interaction::Enabled, false),
|
||||
(Interaction::Hovered, true),
|
||||
(Interaction::Focused, true),
|
||||
(Interaction::Pressed, true),
|
||||
(Interaction::Disabled, false),
|
||||
] {
|
||||
let layer = paint::state_layer(&ui, 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();
|
||||
assert_ne!(hover, pressed);
|
||||
}
|
||||
@@ -31,6 +31,15 @@ use unlucky_design::typography::{TypeRole, TypeStyle, Weight};
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::interaction::Base;
|
||||
|
||||
/// 卡片的底(**这个容器画什么,子控件的禁用态就叠什么**)。
|
||||
///
|
||||
/// 卡内控件的禁用色必须拿这个常量,不许自己写 `Role::Surface`:M234 之前 8 个调用点全传
|
||||
/// 画布色,卡片里的禁用按钮于是把 10% 叠在画布上(M223 的「CR 1.05、形丢失」)。
|
||||
/// 与 [`card`] 的 `background` 用同一个 `Role`,改色不会漂。
|
||||
pub const BASE: Base = Base::of(Role::SurfaceContainer);
|
||||
|
||||
/// ELEVATED CARD 的内边距:**20**(= `LayoutMetrics::card_padding`)。
|
||||
///
|
||||
/// 为什么不是规格卡量到的 **24**(卡片文字 `x=88` − 框 `x=64`):本组件唯一的消费者是实例页的
|
||||
@@ -63,7 +72,8 @@ pub fn card<'a, M: 'a>(ui: &'a Ui, content: impl Into<Element<'a, M>>) -> Elemen
|
||||
.style(move |_theme| container::Style {
|
||||
// 规格卡的卡底是 **surface-container**(不是 `surface-container-low`):
|
||||
// 层级靠与画布(`surface`)的 tone 差表达,不靠描边。
|
||||
background: Some(Background::Color(ui.color(Role::SurfaceContainer))),
|
||||
// 子控件的禁用态拿同一个角色([`BASE`])合成,两边不会漂。
|
||||
background: Some(Background::Color(ui.color(BASE.role()))),
|
||||
border: Border {
|
||||
radius: ui.radius_dp(shape),
|
||||
..Border::default()
|
||||
|
||||
@@ -178,7 +178,8 @@ fn the_disabled_chip_still_looks_disabled() {
|
||||
let disabled = Chip::<()>::filter("安装", true, ()).disabled().paint(ui);
|
||||
let marker = Chip::<()>::current("默认").paint(ui);
|
||||
|
||||
let ink38 = interaction::disabled_content_over_role(ui, Role::OnSurface, false);
|
||||
let ink38 =
|
||||
interaction::disabled_content_over_role(ui, Role::Surface, Role::OnSurface, false);
|
||||
let container12 = interaction::disabled_colors(ui, Role::Surface).0;
|
||||
|
||||
assert_eq!(
|
||||
|
||||
Reference in new issue
Block a user