fix(app-iced): pressed 状态层改走 Interaction 表 + 外观 choice 叠卡底(M284)

四处手写按钮(语言 chip / 主题 chip / 日志行两个动作 / 外观互斥按钮)把
`matches!(status, Hovered | Pressed)` 合成分支换成 `Interaction::from_status(status)`
→ `interaction::layer_color`:pressed 从被压成 hover 的 8% 回到令牌表的 10%。

`appearance/section/mod.rs` 的 `choice` 合成底从写死的 `Role::Surface` 换成
`frame::BASE.role()`(= `MaterialLayer::Card` = `SurfaceContainerLow`),与同屏
`general.rs` / `general/theme.rs` 同源——M234/M237 修过两轮的那个错,这是漏网的第三处。

四处样式闭包抽成自由函数(`choice_style` / `chip_style` / `action_style`),单测直接喂
`Hovered` 与 `Pressed`:断言两色不等 + 各自逐值等于 `state_over` 的令牌求值(改 α 也会红)。
语言行那枚 chip 的断言住在 `general/theme.rs`(`general.rs` 已 390 行,贴 400 硬线)。

未验:hover/pressed 的图像判据。`--force-state` 到不了设置页/外观区(那里不消费
`Model.forced`,`pages/harness.rs` 自己写着),接线另立任务;本轮只出 rest 图并比 md5。
This commit is contained in:
AskaEth committed 2026-11-12 21:48:03 +08:00
1 parent bc2ad85c63
commit 747521fe54
4 files changed
+373 -61

No files matched your search

@@ -54,6 +54,7 @@
//! CPU 档下面板 α 被强制 1.0(D36),而用户请求值可能是 0.5。滑块位置按**生效值**画、旁边写 //! CPU 档下面板 α 被强制 1.0(D36),而用户请求值可能是 0.5。滑块位置按**生效值**画、旁边写
//! 「requested 0.50 → forced 1.00」,这样「为什么拖了没反应」在界面上就有答案,而不是只躺在日志里。 //! 「requested 0.50 → forced 1.00」,这样「为什么拖了没反应」在界面上就有答案,而不是只躺在日志里。
use iced::border::Radius;
use iced::widget::{button, column, container, row}; use iced::widget::{button, column, container, row};
use iced::{ use iced::{
Alignment, Background, Border, Color, Element, Length, Padding, Shadow, Theme as IcedTheme, Alignment, Background, Border, Color, Element, Length, Padding, Shadow, Theme as IcedTheme,
@@ -67,6 +68,7 @@ use unlucky_design::typography::TypeRole;
use unlucky_i18n::catalog::Key; use unlucky_i18n::catalog::Key;
use unlucky_i18n::Lang; use unlucky_i18n::Lang;
use crate::app::components::interaction::{self, Interaction};
use crate::pages::{frame, Message}; use crate::pages::{frame, Message};
use crate::theme::Ui; use crate::theme::Ui;
@@ -242,6 +244,7 @@ pub(super) fn choice<'a>(
change: Change, change: Change,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let height = ui.metrics().control_height; let height = ui.metrics().control_height;
let radius = ui.radius(ShapeScale::Full, height);
let content_color = if selected { let content_color = if selected {
ui.color(Role::OnSecondaryContainer) ui.color(Role::OnSecondaryContainer)
} else { } else {
@@ -260,30 +263,114 @@ pub(super) fn choice<'a>(
) )
.height(height) .height(height)
.padding(Padding::from([0.0, ui.space(Space::X3)])) .padding(Padding::from([0.0, ui.space(Space::X3)]))
.style(move |_theme: &IcedTheme, status| { .style(choice_style(ui, selected, content_color, radius))
.on_press(Message::Appearance(change))
.into()
}
/// [`choice`] 的样式闭包(抽成自由函数:单测要**直接**喂 `Hovered` / `Pressed` 两态)。
///
/// 两处口径都在这里,别在控件里再各写一遍:
///
/// - **α 只从 [`Interaction`] 来**:hover 8% / pressed 10%(M3)。M284 之前两种态合并成一条
/// `matches!(status, Hovered | Pressed)` 分支、都叠 `StateLayer::Hover`,于是按下被压成 8%
/// —— 两个态在图上逐像素相同;
/// - **合成底是 [`frame::BASE`]**(`MaterialLayer::Card` = `SurfaceContainerLow`),因为 `choice`
/// 住在 [`card`] 里。写死 `Role::Surface` 就是 M234/M237 修过两轮的那个错(M284 是第三处)。
fn choice_style<'a>(
ui: &'a Ui,
selected: bool,
content_color: Color,
radius: Radius,
) -> impl Fn(&IcedTheme, button::Status) -> button::Style + 'a {
move |_theme, status| {
let background = if selected { let background = if selected {
Some(Background::Color(ui.color(Role::SecondaryContainer))) Some(Background::Color(ui.color(Role::SecondaryContainer)))
} else if matches!(status, button::Status::Hovered | button::Status::Pressed) {
Some(Background::Color(ui.state_over(
unlucky_design::color::StateLayer::Hover,
Role::OnSurface,
Role::Surface,
)))
} else { } else {
None interaction::layer_color(
ui,
Interaction::from_status(status),
Role::OnSurface,
frame::BASE.role(),
)
.map(Background::Color)
}; };
button::Style { button::Style {
background, background,
text_color: content_color, text_color: content_color,
border: Border { border: Border {
radius: ui.radius(ShapeScale::Full, height), radius,
..Border::default() ..Border::default()
}, },
shadow: Shadow::default(), shadow: Shadow::default(),
snap: true, snap: true,
} }
}) }
.on_press(Message::Appearance(change)) }
.into()
#[cfg(test)]
mod tests {
use super::*;
use unlucky_design::color::{SchemeFlavor, StateLayer};
use unlucky_design::typography::Platform;
use crate::theme::Families;
fn ui() -> Ui {
Ui::with_platform(
SchemeFlavor::Light,
Platform::Windows,
Families {
text: "Segoe UI",
display: "Segoe UI",
mono: "Consolas",
},
)
}
fn solid(style: button::Style) -> Color {
match style.background {
Some(Background::Color(color)) => color,
None => panic!("未选中态的 hover/pressed 必须叠状态层"),
Some(other) => panic!("背景必须是实色,得到 {other:?}"),
}
}
/// hover(8%) 与 pressed(10%) 是**两档**,且各自逐值等于令牌表(把 10% 改成 11% 也会红)。
///
/// 抓得住:①回到 `matches!(status, Hovered | Pressed)` 合并分支(两色相等 ⇒ 第一条红);
/// ②pressed 用 hover 的 α、或合成底被换回画布(第二、三条红)。
/// 抓不住:观感;**过渡中间帧**(forced 路径不套 `tracked`,`layer_color` 直落端点值)。
///
/// 底的角色取 `MaterialLayer::Card.surface_role()`(`frame::BASE` 就是它):既盯住「叠的是卡底」
/// 这条语义,也不往 `settings/*.rs` 的接线点计数里多塞一次字面量(`pages/tests/base_wiring.rs`)。
#[test]
fn the_choice_button_draws_pressed_darker_than_hover() {
let ui = ui();
let style = choice_style(
&ui,
false,
ui.color(Role::OnSurfaceVariant),
ui.radius(ShapeScale::Full, ui.metrics().control_height),
);
let theme = ui.theme();
let hover = solid(style(&theme, button::Status::Hovered));
let pressed = solid(style(&theme, button::Status::Pressed));
assert_ne!(hover, pressed, "hover 与 pressed 必须是两档(8% / 10%)");
let base = MaterialLayer::Card.surface_role();
assert_eq!(
hover,
ui.state_over(StateLayer::Hover, Role::OnSurface, base)
);
assert_eq!(
pressed,
ui.state_over(StateLayer::Pressed, Role::OnSurface, base)
);
assert!(
style(&theme, button::Status::Active).background.is_none(),
"静息态不叠状态层"
);
}
} }
+43 -19
View File
@@ -38,16 +38,20 @@
//! //!
//! 分组元数据只有一份(这里的 [`GROUP`]):`settings/mod.rs` 的 `GROUPS` 直接引用它。 //! 分组元数据只有一份(这里的 [`GROUP`]):`settings/mod.rs` 的 `GROUPS` 直接引用它。
use iced::border::Radius;
use iced::widget::{button, column, container, row}; use iced::widget::{button, column, container, row};
use iced::{Alignment, Background, Border, Element, Length, Padding, Shadow, Theme as IcedTheme}; use iced::{
Alignment, Background, Border, Color, Element, Length, Padding, Shadow, Theme as IcedTheme,
};
use unlucky_design::color::{Role, StateLayer}; use unlucky_design::color::Role;
use unlucky_design::icons::IconKind; use unlucky_design::icons::IconKind;
use unlucky_design::spacing::Space; use unlucky_design::spacing::Space;
use unlucky_design::typography::TypeRole; use unlucky_design::typography::TypeRole;
use unlucky_i18n::catalog::Key; use unlucky_i18n::catalog::Key;
use unlucky_i18n::LangChoice; use unlucky_i18n::LangChoice;
use crate::app::components::interaction::{self, Interaction};
use crate::fonts::install::{CjkInstall, FetchClass}; use crate::fonts::install::{CjkInstall, FetchClass};
use crate::pages::{frame, Language, Message}; use crate::pages::{frame, Language, Message};
use crate::theme::Ui; use crate::theme::Ui;
@@ -315,6 +319,8 @@ fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, choice: LangChoice) -> El
ui.color(Role::OnSurfaceVariant) ui.color(Role::OnSurfaceVariant)
}; };
let border_color = ui.color(Role::OutlineVariant); let border_color = ui.color(Role::OutlineVariant);
let stroke = crate::theme::STROKE_PX;
let radius = ui.radius(box_.shape, height);
button( button(
container(frame::text_role( container(frame::text_role(
@@ -331,18 +337,42 @@ fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, choice: LangChoice) -> El
0.0, 0.0,
unlucky_design::inkcat::metrics::CHIP_PAD_X, unlucky_design::inkcat::metrics::CHIP_PAD_X,
])) ]))
.style(move |_theme: &IcedTheme, status| { .style(chip_style(
ui,
selected,
content_color,
border_color,
stroke,
radius,
))
.on_press(Message::Language(choice))
.into()
}
/// [`chip`] 的样式闭包(抽成自由函数:单测要**直接**喂 `Hovered` / `Pressed` 两态)。
///
/// - **α 只从 [`Interaction`] 来**:hover 8% / pressed 10%(M3)。M284 之前两种态合并成一条
/// `matches!(status, Hovered | Pressed)` 分支、都叠 `StateLayer::Hover`,按下于是被压成 8%;
/// - **合成底是 [`frame::BASE`]**(M237:这一组住在 `frame::card` 里,状态层叠卡底而不是画布)。
fn chip_style<'a>(
ui: &'a Ui,
selected: bool,
content_color: Color,
border_color: Color,
stroke: f32,
radius: Radius,
) -> impl Fn(&IcedTheme, button::Status) -> button::Style + 'a {
move |_theme, status| {
let background = if selected { let background = if selected {
Some(Background::Color(ui.color(Role::SecondaryContainer))) Some(Background::Color(ui.color(Role::SecondaryContainer)))
} else if matches!(status, button::Status::Hovered | button::Status::Pressed) { } else {
// M237:这一组整个住在 `frame::card` 里,状态层叠**卡底**(不是画布)。 interaction::layer_color(
Some(Background::Color(ui.state_over( ui,
StateLayer::Hover, Interaction::from_status(status),
Role::OnSurface, Role::OnSurface,
frame::BASE.role(), frame::BASE.role(),
))) )
} else { .map(Background::Color)
None
}; };
button::Style { button::Style {
@@ -350,17 +380,11 @@ fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, choice: LangChoice) -> El
text_color: content_color, text_color: content_color,
border: Border { border: Border {
color: border_color, color: border_color,
width: if selected { width: if selected { 0.0 } else { stroke },
0.0 radius,
} else {
crate::theme::STROKE_PX
},
radius: ui.radius(box_.shape, height),
}, },
shadow: Shadow::default(), shadow: Shadow::default(),
snap: true, snap: true,
} }
}) }
.on_press(Message::Language(choice))
.into()
} }
@@ -9,16 +9,20 @@
//! 的 `dark` / `light`;第三档「跟随系统」= `auto`,由系统深浅落到对应色板 //! 的 `dark` / `light`;第三档「跟随系统」= `auto`,由系统深浅落到对应色板
//! (`BrandBoard::for_flavor`)。所以这里**不新增配置键**,旧配置读进来语义也一致。 //! (`BrandBoard::for_flavor`)。所以这里**不新增配置键**,旧配置读进来语义也一致。
use iced::border::Radius;
use iced::widget::{button, column, container, row}; use iced::widget::{button, column, container, row};
use iced::{Alignment, Background, Border, Element, Length, Padding, Shadow, Theme as IcedTheme}; use iced::{
Alignment, Background, Border, Color, Element, Length, Padding, Shadow, Theme as IcedTheme,
};
use unlucky_design::color::{Role, StateLayer}; use unlucky_design::color::Role;
use unlucky_design::inkcat::metrics as chipbox; use unlucky_design::inkcat::metrics as chipbox;
use unlucky_design::spacing::Space; use unlucky_design::spacing::Space;
use unlucky_design::typography::TypeRole; use unlucky_design::typography::TypeRole;
use unlucky_i18n::catalog::{Catalog, Key}; use unlucky_i18n::catalog::{Catalog, Key};
use unlucky_presenter::prefs::ThemeMode; use unlucky_presenter::prefs::ThemeMode;
use crate::app::components::interaction::{self, Interaction};
use crate::pages::{frame, Message}; use crate::pages::{frame, Message};
use crate::theme::Ui; use crate::theme::Ui;
@@ -103,6 +107,7 @@ fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, mode: ThemeMode) -> Eleme
}; };
let border_color = ui.color(Role::OutlineVariant); let border_color = ui.color(Role::OutlineVariant);
let stroke = crate::theme::STROKE_PX; let stroke = crate::theme::STROKE_PX;
let radius = ui.radius(box_.shape, height);
button( button(
container(frame::text_role( container(frame::text_role(
@@ -116,18 +121,42 @@ fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, mode: ThemeMode) -> Eleme
) )
.height(height) .height(height)
.padding(Padding::from([0.0, chipbox::CHIP_PAD_X])) .padding(Padding::from([0.0, chipbox::CHIP_PAD_X]))
.style(move |_theme: &IcedTheme, status| { .style(chip_style(
ui,
selected,
content_color,
border_color,
stroke,
radius,
))
.on_press(Message::Theme(mode))
.into()
}
/// [`chip`] 的样式闭包(抽成自由函数:单测要**直接**喂 `Hovered` / `Pressed` 两态)。
///
/// - **α 只从 [`Interaction`] 来**:hover 8% / pressed 10%(M3)。M284 之前两种态合并成一条
/// `matches!(status, Hovered | Pressed)` 分支、都叠 `StateLayer::Hover`,按下于是被压成 8%;
/// - **合成底是 [`frame::BASE`]**(M237:主题三档住在设置卡里,状态层叠卡底而不是画布)。
fn chip_style<'a>(
ui: &'a Ui,
selected: bool,
content_color: Color,
border_color: Color,
stroke: f32,
radius: Radius,
) -> impl Fn(&IcedTheme, button::Status) -> button::Style + 'a {
move |_theme, status| {
let background = if selected { let background = if selected {
Some(Background::Color(ui.color(Role::SecondaryContainer))) Some(Background::Color(ui.color(Role::SecondaryContainer)))
} else if matches!(status, button::Status::Hovered | button::Status::Pressed) { } else {
// M237:主题三档同样住在设置卡里,状态层叠卡底(`frame::BASE`)。 interaction::layer_color(
Some(Background::Color(ui.state_over( ui,
StateLayer::Hover, Interaction::from_status(status),
Role::OnSurface, Role::OnSurface,
frame::BASE.role(), frame::BASE.role(),
))) )
} else { .map(Background::Color)
None
}; };
button::Style { button::Style {
@@ -136,19 +165,41 @@ fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, mode: ThemeMode) -> Eleme
border: Border { border: Border {
color: border_color, color: border_color,
width: if selected { 0.0 } else { stroke }, width: if selected { 0.0 } else { stroke },
radius: ui.radius(box_.shape, height), radius,
}, },
shadow: Shadow::default(), shadow: Shadow::default(),
snap: true, snap: true,
} }
}) }
.on_press(Message::Theme(mode))
.into()
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use unlucky_design::color::{SchemeFlavor, StateLayer};
use unlucky_design::typography::Platform;
use crate::theme::Families;
fn ui() -> Ui {
Ui::with_platform(
SchemeFlavor::Light,
Platform::Windows,
Families {
text: "Segoe UI",
display: "Segoe UI",
mono: "Consolas",
},
)
}
fn solid(style: button::Style) -> Color {
match style.background {
Some(Background::Color(color)) => color,
None => panic!("未选中态的 hover/pressed 必须叠状态层"),
Some(other) => panic!("背景必须是实色,得到 {other:?}"),
}
}
/// 三档 = 两块板 + 一种跟随,顺序是深 → 浅 → 跟随(换序会让「Ink 在最左」这条观感失守)。 /// 三档 = 两块板 + 一种跟随,顺序是深 → 浅 → 跟随(换序会让「Ink 在最左」这条观感失守)。
#[test] #[test]
@@ -161,4 +212,71 @@ mod tests {
assert_eq!(mode_label(ThemeMode::Light), Key::SettingsThemeSilver); assert_eq!(mode_label(ThemeMode::Light), Key::SettingsThemeSilver);
assert_eq!(mode_label(ThemeMode::Auto), Key::SettingsThemeFollowSystem); assert_eq!(mode_label(ThemeMode::Auto), Key::SettingsThemeFollowSystem);
} }
/// hover(8%) 与 pressed(10%) 是**两档**,且各自逐值等于令牌表(把 10% 改成 11% 也会红)。
///
/// 抓得住:①回到 `matches!(status, Hovered | Pressed)` 合并分支(两色相等 ⇒ 第一条红);
/// ②pressed 用 hover 的 α、或合成底被换回画布(第二、三条红)。
/// 抓不住:观感;**过渡中间帧**(forced 路径不套 `tracked`,`layer_color` 直落端点值)。
///
/// 底的角色取 `MaterialLayer::Card.surface_role()`(`frame::BASE` 就是它):既盯住「叠的是卡底」
/// 这条语义,也不往 `settings/*.rs` 的接线点计数里多塞一次字面量(`pages/tests/base_wiring.rs`)。
fn assert_two_layers(ui: &Ui, style: impl Fn(&IcedTheme, button::Status) -> button::Style) {
let theme = ui.theme();
let hover = solid(style(&theme, button::Status::Hovered));
let pressed = solid(style(&theme, button::Status::Pressed));
assert_ne!(hover, pressed, "hover 与 pressed 必须是两档(8% / 10%)");
let base = unlucky_design::elevation::MaterialLayer::Card.surface_role();
assert_eq!(
hover,
ui.state_over(StateLayer::Hover, Role::OnSurface, base)
);
assert_eq!(
pressed,
ui.state_over(StateLayer::Pressed, Role::OnSurface, base)
);
assert!(
style(&theme, button::Status::Active).background.is_none(),
"静息态不叠状态层"
);
}
#[test]
fn the_theme_chip_draws_pressed_darker_than_hover() {
let ui = ui();
assert_two_layers(
&ui,
chip_style(
&ui,
false,
ui.color(Role::OnSurfaceVariant),
ui.color(Role::OutlineVariant),
crate::theme::STROKE_PX,
ui.radius(chipbox::CHIP.shape, chipbox::CHIP.height),
),
);
}
/// **语言行那枚 chip**(`general::chip` 的 `chip_style`)同理:hover 8% / pressed 10%。
///
/// 为什么这条断言住在 `theme.rs` 而不是 `general.rs`:后者已经 389 行(硬线 400),而
/// `general` 的私有项对子模块可见(Rust 的可见性包含后代模块),搬过来不用加 `pub`。
#[test]
fn the_language_chip_draws_pressed_darker_than_hover() {
use super::super::chip_style as language_chip_style;
let ui = ui();
assert_two_layers(
&ui,
language_chip_style(
&ui,
false,
ui.color(Role::OnSurfaceVariant),
ui.color(Role::OutlineVariant),
crate::theme::STROKE_PX,
ui.radius(chipbox::CHIP.shape, chipbox::CHIP.height),
),
);
}
} }
+99 -16
View File
@@ -12,15 +12,19 @@
//! 为什么单独一个文件:`pages/settings/general.rs` 贴着 400 行硬线(语言行 + 字体行 + 三行只读), //! 为什么单独一个文件:`pages/settings/general.rs` 贴着 400 行硬线(语言行 + 字体行 + 三行只读),
//! 这一行自带按钮与样式,塞回去必破线。 //! 这一行自带按钮与样式,塞回去必破线。
use iced::border::Radius;
use iced::widget::{button, column, container, row as row_widget}; use iced::widget::{button, column, container, row as row_widget};
use iced::{Alignment, Background, Border, Element, Length, Padding, Shadow, Theme as IcedTheme}; use iced::{
Alignment, Background, Border, Color, Element, Length, Padding, Shadow, Theme as IcedTheme,
};
use unlucky_design::color::{Role, StateLayer}; use unlucky_design::color::Role;
use unlucky_design::shape::ShapeScale; use unlucky_design::shape::ShapeScale;
use unlucky_design::spacing::Space; use unlucky_design::spacing::Space;
use unlucky_design::typography::TypeRole; use unlucky_design::typography::TypeRole;
use unlucky_i18n::catalog::{Catalog, Key}; use unlucky_i18n::catalog::{Catalog, Key};
use crate::app::components::interaction::{self, Interaction};
use crate::pages::{frame, Action, Message}; use crate::pages::{frame, Action, Message};
use crate::theme::Ui; use crate::theme::Ui;
@@ -97,33 +101,112 @@ fn action_button<'a>(ui: &'a Ui, label: &'a str, message: Message) -> Element<'a
let height = ui.metrics().control_height; let height = ui.metrics().control_height;
let content = ui.color(Role::OnSurface); let content = ui.color(Role::OnSurface);
let outline = ui.color(Role::OutlineVariant); let outline = ui.color(Role::OutlineVariant);
let radius = ui.radius(ShapeScale::Full, height);
button( button(
container(frame_text(ui, TypeRole::LabelLarge, label, content)).align_y(Alignment::Center), container(frame_text(ui, TypeRole::LabelLarge, label, content)).align_y(Alignment::Center),
) )
.height(height) .height(height)
.padding(Padding::from([0.0, ui.space(Space::X3)])) .padding(Padding::from([0.0, ui.space(Space::X3)]))
.style(move |_theme: &IcedTheme, status| { .style(action_style(ui, content, outline, radius))
let background = .on_press(message)
matches!(status, button::Status::Hovered | button::Status::Pressed).then(|| { .into()
// M237:这一行住在设置卡里(`frame::card`),hover 状态层叠卡底。 }
Background::Color(ui.state_over(
StateLayer::Hover, /// [`action_button`] 的样式闭包(抽成自由函数:单测要**直接**喂 `Hovered` / `Pressed` 两态)。
Role::OnSurface, ///
frame::BASE.role(), /// - **α 只从 [`Interaction`] 来**:hover 8% / pressed 10%(M3)。M284 之前两种态合并成一条
)) /// `matches!(status, Hovered | Pressed)` 分支、都叠 `StateLayer::Hover`,按下于是被压成 8%;
}); /// - **合成底是 [`frame::BASE`]**(M237:这一行住在设置卡里,状态层叠卡底而不是画布)。
fn action_style<'a>(
ui: &'a Ui,
content: Color,
outline: Color,
radius: Radius,
) -> impl Fn(&IcedTheme, button::Status) -> button::Style + 'a {
move |_theme, status| {
let background = interaction::layer_color(
ui,
Interaction::from_status(status),
Role::OnSurface,
frame::BASE.role(),
)
.map(Background::Color);
button::Style { button::Style {
background, background,
text_color: content, text_color: content,
border: Border { border: Border {
color: outline, color: outline,
width: 1.0, width: 1.0,
radius: ui.radius(ShapeScale::Full, height), radius,
}, },
shadow: Shadow::default(), shadow: Shadow::default(),
snap: true, snap: true,
} }
}) }
.on_press(message) }
.into()
#[cfg(test)]
mod tests {
use super::*;
use unlucky_design::color::{SchemeFlavor, StateLayer};
use unlucky_design::typography::Platform;
use crate::theme::Families;
fn ui() -> Ui {
Ui::with_platform(
SchemeFlavor::Light,
Platform::Windows,
Families {
text: "Segoe UI",
display: "Segoe UI",
mono: "Consolas",
},
)
}
fn solid(style: button::Style) -> Color {
match style.background {
Some(Background::Color(color)) => color,
None => panic!("hover/pressed 必须叠状态层"),
Some(other) => panic!("背景必须是实色,得到 {other:?}"),
}
}
/// hover(8%) 与 pressed(10%) 是**两档**,且各自逐值等于令牌表(把 10% 改成 11% 也会红)。
///
/// 抓得住:①回到 `matches!(status, Hovered | Pressed)` 合并分支(两色相等 ⇒ 第一条红);
/// ②pressed 用 hover 的 α、或合成底被换回画布(第二、三条红)。
/// 抓不住:观感;**过渡中间帧**(forced 路径不套 `tracked`,`layer_color` 直落端点值)。
///
/// 底取 `MaterialLayer::Card.surface_role()`(`frame::BASE` 就是它),不重复写字面量。
#[test]
fn the_logs_action_button_draws_pressed_darker_than_hover() {
let ui = ui();
let style = action_style(
&ui,
ui.color(Role::OnSurface),
ui.color(Role::OutlineVariant),
ui.radius(ShapeScale::Full, ui.metrics().control_height),
);
let theme = ui.theme();
let hover = solid(style(&theme, button::Status::Hovered));
let pressed = solid(style(&theme, button::Status::Pressed));
assert_ne!(hover, pressed, "hover 与 pressed 必须是两档(8% / 10%)");
let base = unlucky_design::elevation::MaterialLayer::Card.surface_role();
assert_eq!(
hover,
ui.state_over(StateLayer::Hover, Role::OnSurface, base)
);
assert_eq!(
pressed,
ui.state_over(StateLayer::Pressed, Role::OnSurface, base)
);
assert!(
style(&theme, button::Status::Active).background.is_none(),
"静息态不叠状态层"
);
}
} }