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 已通告边界)。
This commit is contained in:
1 parent
fef0922073
commit
65ca0ed29a
25 files changed
+4622
-1
No files matched your search
@@ -0,0 +1,181 @@
|
||||
//! 三平台字体族回退栈(Win10+ / macOS 12+ / Linux)。
|
||||
//!
|
||||
//! 只写族名,不写文件路径、不内嵌字体(D14 + D28)。每条栈的**最后一项必须是通用族名**,
|
||||
//! 否则干净安装的系统上会退化成豆腐块。
|
||||
|
||||
/// 平台(决定字体族回退栈)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Platform {
|
||||
/// Windows 10/11。
|
||||
Windows,
|
||||
/// macOS 12+。
|
||||
MacOs,
|
||||
/// Linux(X11/Wayland,桌面发行版)。
|
||||
Linux,
|
||||
}
|
||||
|
||||
impl Platform {
|
||||
/// 当前编译目标对应的平台(运行时用户覆盖请显式传 [`Platform`])。
|
||||
pub const fn current() -> Self {
|
||||
if cfg!(target_os = "windows") {
|
||||
Platform::Windows
|
||||
} else if cfg!(target_os = "macos") {
|
||||
Platform::MacOs
|
||||
} else {
|
||||
Platform::Linux
|
||||
}
|
||||
}
|
||||
|
||||
/// 稳定字符串名。
|
||||
pub const fn name(self) -> &'static str {
|
||||
match self {
|
||||
Platform::Windows => "windows",
|
||||
Platform::MacOs => "macos",
|
||||
Platform::Linux => "linux",
|
||||
}
|
||||
}
|
||||
|
||||
/// 字体族回退栈(由首选到兜底,最后一项是通用族名)。
|
||||
///
|
||||
/// iced/cosmic-text 按顺序找第一个装了的族;`sans-serif` / `monospace` 是通用兜底,
|
||||
/// 保证「一台干净安装的系统」也不会渲染成豆腐块。
|
||||
pub const fn font_stack(self, family: FontFamily) -> &'static [&'static str] {
|
||||
match (self, family) {
|
||||
// Win10 起 Segoe UI 一定在;Win11 有光学尺寸分离的 Variable 版。
|
||||
(Platform::Windows, FontFamily::Display) => &[
|
||||
"Segoe UI Variable Display",
|
||||
"Segoe UI",
|
||||
"Microsoft YaHei UI",
|
||||
"Microsoft YaHei",
|
||||
"sans-serif",
|
||||
],
|
||||
(Platform::Windows, FontFamily::Text) => &[
|
||||
"Segoe UI Variable Text",
|
||||
"Segoe UI",
|
||||
"Microsoft YaHei UI",
|
||||
"Microsoft YaHei",
|
||||
"sans-serif",
|
||||
],
|
||||
(Platform::Windows, FontFamily::Mono) => {
|
||||
&["Cascadia Mono", "Consolas", "Courier New", "monospace"]
|
||||
}
|
||||
// macOS 12+:SF Pro 分 Display/Text 两种光学尺寸;中文走苹方。
|
||||
(Platform::MacOs, FontFamily::Display) => &[
|
||||
"SF Pro Display",
|
||||
"Helvetica Neue",
|
||||
"PingFang SC",
|
||||
"sans-serif",
|
||||
],
|
||||
(Platform::MacOs, FontFamily::Text) => {
|
||||
&["SF Pro Text", "Helvetica Neue", "PingFang SC", "sans-serif"]
|
||||
}
|
||||
(Platform::MacOs, FontFamily::Mono) => &["SF Mono", "Menlo", "Monaco", "monospace"],
|
||||
// Linux:不假设发行版预装什么,Roboto/Noto 是最常见的两套。
|
||||
(Platform::Linux, FontFamily::Display) | (Platform::Linux, FontFamily::Text) => &[
|
||||
"Roboto",
|
||||
"Noto Sans",
|
||||
"Noto Sans CJK SC",
|
||||
"DejaVu Sans",
|
||||
"sans-serif",
|
||||
],
|
||||
(Platform::Linux, FontFamily::Mono) => &[
|
||||
"JetBrains Mono",
|
||||
"Noto Sans Mono",
|
||||
"DejaVu Sans Mono",
|
||||
"monospace",
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 字体族的用途分类(决定取哪条栈)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum FontFamily {
|
||||
/// 大字号展示(display/headline):光学尺寸更大。
|
||||
Display,
|
||||
/// 正文(title/body/label)。
|
||||
Text,
|
||||
/// 等宽(日志、代码、版本号)。
|
||||
Mono,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const PLATFORMS: [Platform; 3] = [Platform::Windows, Platform::MacOs, Platform::Linux];
|
||||
const FAMILIES: [FontFamily; 3] = [FontFamily::Display, FontFamily::Text, FontFamily::Mono];
|
||||
|
||||
#[test]
|
||||
fn font_stacks_are_well_formed() {
|
||||
for platform in PLATFORMS {
|
||||
for family in FAMILIES {
|
||||
let stack = platform.font_stack(family);
|
||||
assert!(
|
||||
!stack.is_empty(),
|
||||
"{} 的 {family:?} 栈为空",
|
||||
platform.name()
|
||||
);
|
||||
let last = stack[stack.len() - 1];
|
||||
let generic = match family {
|
||||
FontFamily::Mono => "monospace",
|
||||
_ => "sans-serif",
|
||||
};
|
||||
assert_eq!(
|
||||
last,
|
||||
generic,
|
||||
"{} 的 {family:?} 栈必须以通用族名兜底",
|
||||
platform.name()
|
||||
);
|
||||
for name in stack {
|
||||
assert!(!name.is_empty());
|
||||
assert!(
|
||||
!name.contains(['/', '\\', '.', ':']),
|
||||
"字体栈只允许族名,不得出现路径/文件名:{name}"
|
||||
);
|
||||
assert!(
|
||||
name.is_ascii(),
|
||||
"族名应为 ASCII(非 ASCII 名交给系统匹配):{name}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_and_text_stacks_differ_on_windows_and_macos() {
|
||||
assert_ne!(
|
||||
Platform::Windows.font_stack(FontFamily::Display)[0],
|
||||
Platform::Windows.font_stack(FontFamily::Text)[0]
|
||||
);
|
||||
assert_ne!(
|
||||
Platform::MacOs.font_stack(FontFamily::Display)[0],
|
||||
Platform::MacOs.font_stack(FontFamily::Text)[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chinese_fallback_is_present_but_not_first() {
|
||||
// 中文族名必须在栈里(否则 CJK 走系统随机匹配),但不能排第一(纯英文环境不加载中文字体)。
|
||||
for platform in PLATFORMS {
|
||||
let stack = platform.font_stack(FontFamily::Text);
|
||||
let cjk = ["Microsoft YaHei UI", "PingFang SC", "Noto Sans CJK SC"];
|
||||
assert!(
|
||||
stack.iter().any(|name| cjk.contains(name)),
|
||||
"{} 的正文栈缺 CJK 兜底",
|
||||
platform.name()
|
||||
);
|
||||
assert!(
|
||||
!cjk.contains(&stack[0]),
|
||||
"{} 的首选字体不该是 CJK 族",
|
||||
platform.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_platform_returns_a_stack() {
|
||||
let platform = Platform::current();
|
||||
assert!(!platform.font_stack(FontFamily::Text).is_empty());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user