feat(app-iced): M68 —— iced 0.14 骨架(后端探测/字体基座/自绘标题栏/导航轨/深浅主题/截图通道)
新增 crates/app-iced(package `unlucky-app-iced`,bin `unlucky-launcher-iced`),与 Slint 版并存, 为 D26「UI 从头写」打地基。本 mission 只做骨架:能开窗、颜色/字体对、层级在,不做业务页面。 - 后端(D27):启动探测 wgpu 适配器(只看 device_type,避开 WARP 误判)→ 写 ICED_BACKEND; auto/gpu/cpu 三条路径都保留 tiny-skia 兜底,绝不硬失败;`--renderer=cpu` 跳过探测(省 ~0.85s) - 字体(D28 前半):令牌三平台字体栈 → 用 iced 全局 FontSystem 查装机情况 → 族名与 400/500/700 字重落点(缺档按 Nearest/Synthetic 记录)打一行启动日志 - 令牌适配层 theme::Ui:唯一把 unlucky-design 的值翻成 iced Color/Font/Border/Radius/Theme 的地方; 49 个 M3 角色留在自己的 Scheme 里,iced 的 6 色 Palette 只给 devtools - 骨架:1180×760 无边框窗口、48 高自绘标题栏(drag + 东/南 drag_resize + 最小化/最大化/关闭)、 88 宽导航轨(56×32 SecondaryContainer 药丸,由令牌推导)、页头 56 + 一张 OutlinedCard - 主题:系统主题探测 + --theme,切主题只重建 Ui(Theme::name 变化触发重算),不动 UI 状态 - 截图通道:`--screenshot <png>` + `--screenshot-theme`(真开窗第 3 帧出图),以及 `--diff <a> <b>` 自带逐像素对比(CI 不依赖 Python/ImageMagick) - 图标:令牌 SVG path → iced svg 控件(颜色走 alpha 蒙版,深浅共用一份位图缓存) - 门禁:tests/no_hardcoded_tokens.rs 扫源码挡色值/尺寸字面量 + 挡 slint 依赖(自检也算过) 实测(Windows Server 2025 / 96 DPI / RX 6600 XT):浅/深 × GPU/CPU 四张真开窗截图;GPU vs CPU 差异 0.72% 像素、全部落在字形/图标抗锯齿与 1px 圆角描边、最佳对齐 (0,0)、平坦区颜色逐字节相同; release 体积双后端 6.44 MiB / 仅 CPU 3.75 MiB;首帧 GPU 1.18~1.31s、CPU 33~37ms。 未做/未验证:macOS 与 Wayland 全链路、西/北边缩放热区、letter spacing(iced 0.14 无此属性)、 中文 CDN 字体、玻璃;翻牌步骤写在 DevDocs/SubAgentsTalk/app-iced-骨架与翻牌.md(不入库)。
This commit is contained in:
1 parent
3fcdc3d3e1
commit
df4ff373b7
16 files changed
+4244
-97
No files matched your search
@@ -0,0 +1,163 @@
|
||||
//! `fonts` 的单元测试(拆出来只为「src 文件 ≤400 行」的门禁)。
|
||||
|
||||
use super::*;
|
||||
use unlucky_design::typography::WeightCoverage;
|
||||
|
||||
fn only(installed: &'static [&'static str]) -> impl Fn(&str) -> bool {
|
||||
move |family| installed.contains(&family)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_stack_falls_back_to_classic_segoe_when_variable_is_absent() {
|
||||
// Win10 没有 Segoe UI Variable Text
|
||||
let family = pick(
|
||||
Platform::Windows,
|
||||
FontFamily::Text,
|
||||
&only(&["Segoe UI", "Microsoft YaHei UI"]),
|
||||
);
|
||||
assert_eq!(family, "Segoe UI");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_stack_prefers_the_variable_face_on_win11() {
|
||||
let family = pick(
|
||||
Platform::Windows,
|
||||
FontFamily::Text,
|
||||
&only(&["Segoe UI Variable Text", "Segoe UI"]),
|
||||
);
|
||||
assert_eq!(family, "Segoe UI Variable Text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generic_family_names_are_never_reported_as_the_resolved_family() {
|
||||
// 一个都没装:返回栈头,且栈头不是通用族名
|
||||
let family = pick(Platform::Linux, FontFamily::Text, &only(&[]));
|
||||
assert_eq!(family, "Roboto");
|
||||
assert!(!is_generic(family));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_cjk_fonts_do_not_change_the_latin_pick() {
|
||||
// 装了苹方但没装 SF:macOS 栈应该选 Helvetica Neue 而不是中文族
|
||||
let family = pick(
|
||||
Platform::MacOs,
|
||||
FontFamily::Text,
|
||||
&only(&["PingFang SC", "Helvetica Neue"]),
|
||||
);
|
||||
assert_eq!(family, "Helvetica Neue");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weight_reports_use_the_token_degradation_path() {
|
||||
// 没有系统字面表时回落到令牌纯函数:Windows 栈里 Segoe UI Variable Text 是可变 300..700
|
||||
let medium = weight_report(
|
||||
Platform::Windows,
|
||||
"Segoe UI Variable Text",
|
||||
Weight::Medium,
|
||||
None,
|
||||
);
|
||||
assert_eq!(medium.requested, 500);
|
||||
assert_eq!(medium.quality, WeightMatch::Native);
|
||||
|
||||
// 手工构造一个「只有 400/700」的族(= 经典 Segoe UI),500 应该落到最近档
|
||||
let faces = [
|
||||
Face {
|
||||
family: "Segoe UI".to_owned(),
|
||||
weight: 400,
|
||||
},
|
||||
Face {
|
||||
family: "Segoe UI".to_owned(),
|
||||
weight: 700,
|
||||
},
|
||||
];
|
||||
let medium = weight_report(Platform::Windows, "Segoe UI", Weight::Medium, Some(&faces));
|
||||
assert_eq!(medium.available, 400, "400 比 700 更近");
|
||||
assert_eq!(medium.quality, WeightMatch::Nearest);
|
||||
|
||||
// 只有 400 却要 700 → 合成加粗
|
||||
let only_regular = [Face {
|
||||
family: "Narrow".to_owned(),
|
||||
weight: 400,
|
||||
}];
|
||||
let bold = weight_report(
|
||||
Platform::Windows,
|
||||
"Narrow",
|
||||
Weight::Bold,
|
||||
Some(&only_regular),
|
||||
);
|
||||
assert_eq!(bold.quality, WeightMatch::Synthetic);
|
||||
|
||||
// 原生有 500 就用 500
|
||||
let has_medium = [Face {
|
||||
family: "Noto".to_owned(),
|
||||
weight: 500,
|
||||
}];
|
||||
assert_eq!(
|
||||
weight_report(Platform::Linux, "Noto", Weight::Medium, Some(&has_medium)).quality,
|
||||
WeightMatch::Native
|
||||
);
|
||||
|
||||
let narrow = WeightCoverage::Static(&[400]);
|
||||
assert_eq!(narrow.nearest(700), 400);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn describe_is_one_line_and_mentions_all_three_stacks() {
|
||||
let resolved = Resolved {
|
||||
platform: Platform::Windows,
|
||||
families: [
|
||||
FamilyReport {
|
||||
stack: "text",
|
||||
family: "Segoe UI",
|
||||
installed: true,
|
||||
},
|
||||
FamilyReport {
|
||||
stack: "display",
|
||||
family: "Segoe UI Variable Display",
|
||||
installed: false,
|
||||
},
|
||||
FamilyReport {
|
||||
stack: "mono",
|
||||
family: "Cascadia Mono",
|
||||
installed: true,
|
||||
},
|
||||
],
|
||||
weights: [
|
||||
WeightReport {
|
||||
requested: 400,
|
||||
available: 400,
|
||||
quality: WeightMatch::Native,
|
||||
},
|
||||
WeightReport {
|
||||
requested: 500,
|
||||
available: 400,
|
||||
quality: WeightMatch::Nearest,
|
||||
},
|
||||
WeightReport {
|
||||
requested: 700,
|
||||
available: 400,
|
||||
quality: WeightMatch::Synthetic,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let line = resolved.describe();
|
||||
assert_eq!(line.lines().count(), 1);
|
||||
assert!(line.contains("text=\"Segoe UI\""), "{line}");
|
||||
assert!(line.contains("未装机"), "{line}");
|
||||
assert!(line.contains("w700=400(synthetic)"), "{line}");
|
||||
assert!(resolved.has_synthetic());
|
||||
assert_eq!(resolved.families().text, "Segoe UI");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_roles_pick_the_display_stack_for_big_text() {
|
||||
use unlucky_design::typography::TypeRole;
|
||||
|
||||
assert_eq!(
|
||||
TypeRole::TitleLarge.style().family,
|
||||
FontFamily::Display,
|
||||
"22px 标题走 Display 栈"
|
||||
);
|
||||
assert_eq!(TypeRole::BodyMedium.style().family, FontFamily::Text);
|
||||
}
|
||||
Reference in new issue
Block a user