Files
Unlucky-Launcher/crates/app-iced/src/components/feedback/panel.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

154 lines
5.5 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.
//! 三件「面」的几何:Snackbar / Dialog / EmptyState(数值与出处见 [`super::geometry`] 的 G17 表)。
//!
//! 为什么与进度分家:进度那两个是**带节奏的图形**(keyframes 与相位),这三件是**静态盒子**
//! (宽高/内距/圆角)。合在一个文件里会顶到 400 行红线(AGENTS.md §3-1)。
use unlucky_design::component_map::Component;
use unlucky_design::inkcat::metrics as inkcat;
use unlucky_design::spacing::Space;
use crate::theme::Ui;
/// Snackbar 的几何(G17-c/d + A1 高/内距/上限)。
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SnackbarGeometry {
/// 最小宽度(344,`[估]`;iced 无 min-width,见组件文档)。
pub min_width: f32,
/// 最大宽度(600,`[源]` `SnackbarKt.ContainerMaxWidth`)。
pub max_width: f32,
/// 单行高(48)。
pub single_height: f32,
/// 双行高(68)。
pub two_height: f32,
/// 距宿主边距(12,`[规格]`)。
pub margin: f32,
/// 文本水平内距(16)。
pub pad_h: f32,
/// 文本垂直内距(6)。
pub pad_v: f32,
/// 文本 ↔ 动作间距(8)。
pub gap: f32,
/// 圆角(4)。
pub radius: f32,
}
impl SnackbarGeometry {
/// 从令牌求值。
///
/// M229 换肤把**高与圆角**改成规格卡口径(`inkcat::metrics::SNACKBAR`:340×**52**、rx=**16**):
/// 官方 `SnackbarTokens` 是 48 高 + `CornerExtraSmall(4)`,规格卡画成 52/16 —— M228 已把这条
/// 差异登记为「换肤时取 `SNACKBAR`」。双行高仍按官方「每多一行 +20」推成 **72**(52 + 20),
/// 与单行同 delta;宽上限 600 与边距 12 不动(它们不在规格卡上)。
pub fn of(ui: &Ui) -> Self {
let spec = inkcat::SNACKBAR;
let single_height = spec.height;
Self {
min_width: 344.0,
max_width: 600.0,
single_height,
two_height: single_height + 20.0,
margin: ui.space(Space::X3),
pad_h: ui.space(Space::X4),
pad_v: 6.0,
gap: ui.space(Space::X2),
radius: spec.radius_px(),
}
}
/// 按行数取高:`lines >= 2` → 68,否则 48(官方 `heightIn(min=...)` 的等价口径)。
pub fn height_for(&self, lines: u8) -> f32 {
if lines >= 2 {
self.two_height
} else {
self.single_height
}
}
}
/// Dialog 的几何。
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DialogGeometry {
/// 本项目宽度(480,落在官方 280..560 内)。
pub width: f32,
/// 官方下限(280)。
pub min_width: f32,
/// 官方上限(560)。
pub max_width: f32,
/// 圆角(28)。
pub radius: f32,
/// 四边内边距(24)。
pub padding: f32,
/// 标题下间距(16)。
pub title_gap: f32,
/// 正文下间距(24)。
pub body_gap: f32,
/// 动作主轴间距(8)。
pub action_gap: f32,
/// 动作交叉间距(12)。
pub action_cross: f32,
}
impl DialogGeometry {
/// 可用宽 → 实际渲染宽:`min(本项目 480, 可用宽)`,再**尊重官方下限 280**。
///
/// 复审 E:早先 `Dialog::width(200)` 会真的画 200(官方 `DialogMinWidth = 280` 只在测试里)。
/// 可用宽 < 280 时仍画 280(**溢出宿主**)—— iced 没有 min-width,这条是 G17-d 之二。
pub fn effective_width(&self, available: f32) -> f32 {
available.min(self.width).max(self.min_width)
}
/// 从令牌求值(内边距与三个间距全部落在 4px 栅格上 ⇒ 走 `Space`)。
pub fn of(ui: &Ui) -> Self {
let width = 480.0;
Self {
width,
min_width: 280.0,
max_width: 560.0,
radius: Component::Dialog.spec().shape.radius_px(width),
padding: ui.space(Space::X6),
title_gap: ui.space(Space::X4),
body_gap: ui.space(Space::X6),
action_gap: ui.space(Space::X2),
action_cross: ui.space(Space::X3),
}
}
}
/// 空态图标边长(`IconSize::Empty` = 32)。
///
/// M86 起 `icons::view_at` 会按请求尺寸**重新烘焙 stroke**,所以 32 渲染仍是视觉 1.5px 线宽
/// (早先 `view` 把 svg 钉成 24,32 的盒子只是居中不拉伸,实渲 24 —— 那个欠账已随 M86 消解)。
pub const EMPTY_ICON: f32 = 32.0;
/// 空态的几何(**项目自定**:官方没有这个组件;`[估]` 几何 + `[规格]` 排版 + `[源]` CTA)。
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EmptyStateGeometry {
/// 图标块边长(96)。
pub block: f32,
/// 图标块圆角(16,`ShapeScale::Large`)。
pub block_radius: f32,
/// 块内图标(**32**,`IconSize::Empty`;由 `icons::view_at` 按 32 烘焙,stroke 同步重算)。
pub icon: f32,
/// 块 ↔ 标题(16)。
pub gap_title: f32,
/// 标题 ↔ 副文案(8)。
pub gap_body: f32,
/// 副文案 ↔ 动作(24)。
pub gap_action: f32,
}
impl EmptyStateGeometry {
/// 从令牌求值(圆角档位与图标档位是真令牌,96 / 三个间距是项目自定)。
pub fn of(ui: &Ui) -> Self {
let block = 96.0;
Self {
block,
block_radius: Component::EmptyState.spec().shape.radius_px(block),
icon: EMPTY_ICON,
gap_title: ui.space(Space::X4),
gap_body: ui.space(Space::X2),
gap_action: ui.space(Space::X6),
}
}
}