Files
Unlucky-Launcher/crates/app-iced/src/pages/settings/general/theme.rs
T
AskaEth db541c820e feat(app-iced): Ink Cat / Silver Cat 换肤 —— 双色板默认 + 规格卡组件几何(M229)
产品出厂外观从 tone 派生的 `Scheme::baseline` 切到品牌色板:深色 = Ink(淡绿)、
浅色 = Silver(钢蓝),D38 取色链(背景图 / 壁纸)仍然优先,取色失败回落到色板。

主题三档接进设置页「通用 → 主题」:Ink / Silver / 跟随系统(落盘仍是
`appearance.theme_mode` 的 dark / light / auto,不新增配置键)。

组件几何按规格卡对齐(M228 的 `inkcat` 登记项逐条落地):
- 卡片圆角 12 → 16;实例元信息卡改用新的 ELEVATED CARD(无描边、SurfaceContainer
  底、内距 24、上标注 11/600)
- Snackbar 48/4 → 52/16(双行 72)
- Checkbox 20/rx2 → 18/rx4;Radio 20 → 18 Ø(内点仍 10)
- 按钮加 XS~XL 五档(产品仍只用 S=40),内距改走 ControlSize::pad_x
- 新增 FAB 四态(40/56/80/160×56)与图标按钮第四式 outlined
- 语言与主题两行 chips 统一成规格卡 chip(32 高 / rx8)

`theme.rs` 原本正好 400 行,先把单测纯搬移到 `theme/tests.rs` 再加色板字段;
`app/tests.rs`、`settings/general.rs` 同样按纯搬移拆分守 400 行硬线。

验证:cargo fmt --all --check、cargo check -p unlucky-app-iced --all-targets、
cargo test --workspace(除 clippy 前的一轮 flake,见报告)、cargo clippy --workspace
--all-targets -- -D warnings、全仓 400 行扫描。视觉矩阵 42 张在
DevDocs/artifacts/m229/(gitignored,不进提交)。
2026-11-11 01:49:13 +08:00

162 lines
5.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 设置页「General → 主题」行:Ink / Silver / 跟随系统三档(M229)。
//!
//! 从 `general.rs` 拆出来的理由只有一条:加这三档之后那个文件 474 行(400 行硬线)。
//! 拆法是「整段搬走 + 一个 `pub(super) fn row` 入口」,排版与 handler 一字未改。
//!
//! 三档的语义:**Ink = 深色主方案、Silver = 浅色主方案**,落盘取值仍是 `appearance.theme_mode`
//! 的 `dark` / `light`;第三档「跟随系统」= `auto`,由系统深浅落到对应色板
//! (`BrandBoard::for_flavor`)。所以这里**不新增配置键**,旧配置读进来语义也一致。
use iced::widget::{button, column, container, row};
use iced::{Alignment, Background, Border, Element, Length, Padding, Shadow, Theme as IcedTheme};
use unlucky_design::color::{Role, StateLayer};
use unlucky_design::inkcat::metrics as chipbox;
use unlucky_design::spacing::Space;
use unlucky_design::typography::TypeRole;
use unlucky_i18n::catalog::{Catalog, Key};
use unlucky_presenter::prefs::ThemeMode;
use crate::pages::{frame, Message};
use crate::theme::Ui;
/// 三档全序(Ink / Silver / 跟随系统):`ThemeMode` 在 presenter,声明序是 `Auto/Light/Dark`,
/// 画出来的顺序按「深 → 浅 → 跟随」,所以这里另给一张表,不靠枚举声明序。
pub(super) const THEME_MODES: [ThemeMode; 3] = [ThemeMode::Dark, ThemeMode::Light, ThemeMode::Auto];
/// 主题行:标题 + 三档互斥 chip + 一句说明(排版与语言行同族)。
pub(super) fn view<'a>(ui: &'a Ui, catalog: &Catalog, mode: ThemeMode) -> Element<'a, Message> {
let head = row![
container(frame::text_role(
ui,
TypeRole::BodyMedium,
false,
catalog.text(Key::SettingsTheme),
ui.color(Role::OnSurface),
))
.width(Length::Fill)
.align_y(Alignment::Center),
frame::text_role(
ui,
TypeRole::LabelLarge,
true,
catalog.text(mode_label(mode)),
ui.color(Role::OnPrimaryFixedVariant),
),
]
.width(Length::Fill)
.align_y(Alignment::Center);
let chips = row(THEME_MODES
.iter()
.map(|choice| {
chip(
ui,
catalog.text(mode_label(*choice)),
*choice == mode,
*choice,
)
})
.collect::<Vec<_>>())
.spacing(ui.space(Space::X1))
.align_y(Alignment::Center);
column![
head,
chips,
frame::text_role(
ui,
TypeRole::BodySmall,
false,
catalog.text(Key::SettingsThemeHint),
ui.color(Role::OnSurfaceVariant),
),
]
.spacing(ui.space(Space::X1))
.width(Length::Fill)
.padding(Padding::from([ui.space(Space::X2), 0.0]))
.into()
}
/// 一档主题的选项名(Ink / Silver / 跟随系统)。
fn mode_label(mode: ThemeMode) -> Key {
match mode {
ThemeMode::Dark => Key::SettingsThemeInk,
ThemeMode::Light => Key::SettingsThemeSilver,
ThemeMode::Auto => Key::SettingsThemeFollowSystem,
}
}
/// 一枚主题档位按钮(选中态 `SecondaryContainer`,未选中透明 + 1px `OutlineVariant`)。
///
/// 几何取规格卡的 chip(高 32、圆角 8,`inkcat::metrics::CHIP`):主题三档是**可并排的筛选**,
/// 不是主行动按钮,用 chip 档比语言行那枚 40 高药丸更贴规格卡。
fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, mode: ThemeMode) -> Element<'a, Message> {
let box_ = chipbox::CHIP;
let height = box_.height;
let content_color = if selected {
ui.color(Role::OnSecondaryContainer)
} else {
ui.color(Role::OnSurfaceVariant)
};
let border_color = ui.color(Role::OutlineVariant);
let stroke = crate::theme::STROKE_PX;
button(
container(frame::text_role(
ui,
TypeRole::LabelLarge,
selected,
text,
content_color,
))
.align_y(Alignment::Center),
)
.height(height)
.padding(Padding::from([0.0, chipbox::CHIP_PAD_X]))
.style(move |_theme: &IcedTheme, status| {
let background = if selected {
Some(Background::Color(ui.color(Role::SecondaryContainer)))
} else if matches!(status, button::Status::Hovered | button::Status::Pressed) {
Some(Background::Color(ui.state_over(
StateLayer::Hover,
Role::OnSurface,
Role::Surface,
)))
} else {
None
};
button::Style {
background,
text_color: content_color,
border: Border {
color: border_color,
width: if selected { 0.0 } else { stroke },
radius: ui.radius(box_.shape, height),
},
shadow: Shadow::default(),
snap: true,
}
})
.on_press(Message::Theme(mode))
.into()
}
#[cfg(test)]
mod tests {
use super::*;
/// 三档 = 两块板 + 一种跟随,顺序是深 → 浅 → 跟随(换序会让「Ink 在最左」这条观感失守)。
#[test]
fn the_three_modes_are_ink_silver_and_follow_system() {
assert_eq!(
THEME_MODES,
[ThemeMode::Dark, ThemeMode::Light, ThemeMode::Auto]
);
assert_eq!(mode_label(ThemeMode::Dark), Key::SettingsThemeInk);
assert_eq!(mode_label(ThemeMode::Light), Key::SettingsThemeSilver);
assert_eq!(mode_label(ThemeMode::Auto), Key::SettingsThemeFollowSystem);
}
}