Merge branch 'feat/statelayer-pressed-alpha-and-card-surfac'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
GPU evidence / GPU 证据 — 真实产物判据 (push) Canceled after 0s
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Canceled after 0s
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Canceled after 0s
Dev Guards / GPU 证据门禁(同 main 口径;无 GPU 只跑自测) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
GPU evidence / GPU 证据 — 真实产物判据 (push) Canceled after 0s
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Canceled after 0s
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Canceled after 0s
Dev Guards / GPU 证据门禁(同 main 口径;无 GPU 只跑自测) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
This commit is contained in:
commit
c8f820caec
4 files changed
+371
-59
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(),
|
||||||
|
"静息态不叠状态层"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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` 两态)。
|
||||||
|
///
|
||||||
|
/// - **α 只从 [`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,
|
Role::OnSurface,
|
||||||
frame::BASE.role(),
|
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(),
|
||||||
|
"静息态不叠状态层"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user