产品出厂外观从 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,不进提交)。
162 lines
5.6 KiB
Rust
162 lines
5.6 KiB
Rust
//! 设置页「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);
|
||
}
|
||
}
|