Files
Unlucky-Launcher/crates/design/src/elevation.rs
T
AskaEth 65ca0ed29a feat(design): 加 unlucky-design 令牌 crate(M3 色彩/排版/形状/动效/图标)
D26 把视觉基线锁到 Material 3 Expressive,本 crate 是「UI 整体性」的地基:
令牌唯一真源,后续 iced 页面只消费不新增。零第三方依赖、零 UI 框架依赖
(cargo tree -p unlucky-design --edges normal 只有它自己一行)。

- 色彩:49 个 M3 语义角色 × light/dark,种子色派生 6 条 tone 调色板;
  tone 轴严格用 CIELAB L*(偏差 ≤1),色相/彩度用 OKLCh 近似 CAM16 并在
  模块文档里写明口径与已知偏差(不宣称是标准 HCT 动态取色)
- 对比度:26 组文字对 × 6 组种子 × 2 模式全部 ≥4.5:1;3 条例外显式登记
  (Outline 非文字 3:1、OutlineVariant 装饰、SurfaceVariant 是容器色)
- 排版:M3 type scale 15 档 + 三平台字体族回退栈(只写族名,不内嵌受限字体)
- 形状/高程/动效:Expressive 形状刻度 + shape morph 数据、elevation 级别与
  surface container 映射、时长/缓动/弹簧(纯函数可采样)
- 图标:24 个从 Slint 版迁移的 SVG path + 4 个新增,IconKind 冻结只可末尾追加;
  写明 Slint 的 stroke-width 是绝对像素、SVG 是 viewBox 用户单位的语义差异,
  换算封装成 Icon::stroke_width
- 组件档位表:20 个组件的形状/高程/字体/动效一次取全

TODO(M63):elevation shadow 的 blur/offset/alpha 与四组弹簧参数是占位值,
接口先定、数值待玻璃视觉规格表(tower 已通告边界)。
2026-11-01 15:34:04 +08:00

227 lines
7.4 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.
//! 高程令牌:elevation level、阴影参数、surface container 映射。
//!
//! M3 在 2024 之后改了「怎么表达高程」:**不再**往表面上叠半透明的 surface tint,
//! 而是用 `surface-container-*` 这一族中性容器色表达层级(GPU 路径再叠阴影做空间感)。
//! 本模块两种口径都给出,旧口径显式标为 deprecated,避免页面线混用。
use crate::color::Role;
/// M3 高程级别 0..5(对应 0 / 1 / 3 / 6 / 8 / 12 dp)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Elevation {
/// 0dp:贴底内容(窗口底、内联区)。
Level0,
/// 1dp:卡片、可抬起的列表项。
Level1,
/// 3dp:可交互卡片、导航栏。
Level2,
/// 6dp:FAB、侧栏、浮动工具条。
Level3,
/// 8dp:菜单、popover。
Level4,
/// 12dp:对话框、模态。
Level5,
}
impl Elevation {
/// 全部级别。
pub const ALL: [Elevation; 6] = [
Elevation::Level0,
Elevation::Level1,
Elevation::Level2,
Elevation::Level3,
Elevation::Level4,
Elevation::Level5,
];
/// 标称高度(dp)。
pub const fn dp(self) -> f32 {
match self {
Elevation::Level0 => 0.0,
Elevation::Level1 => 1.0,
Elevation::Level2 => 3.0,
Elevation::Level3 => 6.0,
Elevation::Level4 => 8.0,
Elevation::Level5 => 12.0,
}
}
/// 该级别应使用的表面容器色角色(M3 现行口径)。
///
/// 层级靠**换底色**表达,不靠 tint;浅深模式都成立。
pub const fn surface_role(self) -> Role {
match self {
Elevation::Level0 => Role::SurfaceContainerLowest,
Elevation::Level1 => Role::SurfaceContainerLow,
Elevation::Level2 => Role::SurfaceContainer,
Elevation::Level3 => Role::SurfaceContainerHigh,
Elevation::Level4 | Elevation::Level5 => Role::SurfaceContainerHighest,
}
}
/// 阴影参数(**占位值,待 M63 视觉规格** — 见下面的 TODO)。
///
/// TODO(M63): 取值待视觉规格。官方只给 dp 与设计稿示意,没有可移植的 blur/alpha 公式;
/// 这里的 blur/offset/alpha 是临时占位(按「blur ≈ dp × 1.3 + 1.5」凑的数),
/// M63 的玻璃规格表落地后以那张表为准,别在页面里引用这些绝对值。
pub const fn shadow(self) -> ShadowSpec {
match self {
Elevation::Level0 => ShadowSpec {
blur_px: 0.0,
offset_y_px: 0.0,
alpha: 0.0,
},
Elevation::Level1 => ShadowSpec {
blur_px: 3.0,
offset_y_px: 1.0,
alpha: 0.12,
},
Elevation::Level2 => ShadowSpec {
blur_px: 6.0,
offset_y_px: 2.0,
alpha: 0.14,
},
Elevation::Level3 => ShadowSpec {
blur_px: 9.0,
offset_y_px: 4.0,
alpha: 0.16,
},
Elevation::Level4 => ShadowSpec {
blur_px: 12.0,
offset_y_px: 6.0,
alpha: 0.18,
},
Elevation::Level5 => ShadowSpec {
blur_px: 17.0,
offset_y_px: 8.0,
alpha: 0.20,
},
}
}
/// 旧口径(M3 2022):把 `surface-tint` 按这个 alpha 叠在 `surface` 上表达高程。
///
/// **deprecated**——现行 M3 用 [`Elevation::surface_role`];保留只为读旧稿时能对上数,
/// 新代码不要用。
pub const fn deprecated_tint_alpha(self) -> f32 {
match self {
Elevation::Level0 => 0.0,
Elevation::Level1 => 0.05,
Elevation::Level2 => 0.08,
Elevation::Level3 => 0.11,
Elevation::Level4 => 0.12,
Elevation::Level5 => 0.14,
}
}
}
/// 一层阴影的绘制参数。
///
/// TODO(M63): 取值待视觉规格(这份结构只是「接口预留」,数值由 M63 的玻璃规格表填充)。
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ShadowSpec {
/// 高斯模糊半径(px)。
pub blur_px: f32,
/// 向下偏移(px)。
pub offset_y_px: f32,
/// 阴影色 alpha(叠在 [`crate::color::Scheme::shadow`] 之上)。
pub alpha: f32,
}
impl ShadowSpec {
/// 是否可见(0 表示这一级不画阴影)。
pub fn is_visible(&self) -> bool {
self.alpha > 0.0 && self.blur_px > 0.0
}
}
/// 三个「材质层」的语义(沿用 `ui-integration-contract.md` §2 的三层,映射到 M3 令牌)。
///
/// 页面线按层取色,不许自己决定「这一层比那一层深一点」。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MaterialLayer {
/// 底层(canvas):窗口底,永远不透明。
Canvas,
/// 卡片层:页面内容/设置分组/AI 面板。
Card,
/// 浮层:对话框、菜单、Toast。
Overlay,
}
impl MaterialLayer {
/// 该层的底色角色。
pub const fn surface_role(self) -> Role {
match self {
MaterialLayer::Canvas => Role::Surface,
MaterialLayer::Card => Role::SurfaceContainerLow,
MaterialLayer::Overlay => Role::SurfaceContainerHigh,
}
}
/// 该层的高程级别。
pub const fn elevation(self) -> Elevation {
match self {
MaterialLayer::Canvas => Elevation::Level0,
MaterialLayer::Card => Elevation::Level1,
MaterialLayer::Overlay => Elevation::Level3,
}
}
/// 该层是否允许半透明(D27:**只有 GPU 路径**才允许;CPU 路径一律纯色)。
pub const fn allows_translucency(self) -> bool {
matches!(self, MaterialLayer::Card | MaterialLayer::Overlay)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn elevation_dp_ladder() {
let ladder: Vec<f32> = Elevation::ALL.iter().map(|e| e.dp()).collect();
assert_eq!(ladder, vec![0.0, 1.0, 3.0, 6.0, 8.0, 12.0]);
}
#[test]
fn shadow_grows_with_elevation() {
let mut prev = Elevation::Level0.shadow();
for level in &Elevation::ALL[1..] {
let cur = level.shadow();
assert!(cur.blur_px > prev.blur_px, "{level:?} 的模糊应更大");
assert!(cur.offset_y_px >= prev.offset_y_px);
assert!(cur.alpha >= prev.alpha);
assert!(cur.is_visible());
prev = cur;
}
assert!(!Elevation::Level0.shadow().is_visible());
}
#[test]
fn higher_levels_use_higher_surface_containers() {
assert_eq!(
Elevation::Level5.surface_role(),
Role::SurfaceContainerHighest
);
assert_eq!(
Elevation::Level0.surface_role(),
Role::SurfaceContainerLowest
);
}
#[test]
fn material_layers_are_ordered() {
assert_eq!(MaterialLayer::Canvas.surface_role(), Role::Surface);
assert_eq!(
MaterialLayer::Card.surface_role(),
Role::SurfaceContainerLow
);
assert_eq!(
MaterialLayer::Overlay.surface_role(),
Role::SurfaceContainerHigh
);
assert!(!MaterialLayer::Canvas.allows_translucency());
assert!(MaterialLayer::Overlay.allows_translucency());
}
}