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:
AskaEth committed 2026-11-01 17:11:24 +08:00
1 parent 3fcdc3d3e1
commit df4ff373b7
16 files changed
+4244 -97

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
//! 入口。所有实际逻辑在 lib(`unlucky_app_iced`)里:
//!
//! - `--diff <a> <b>`:只读两张 PNG 做逐像素对比(D30 的 GPU/CPU 几何一致性检查),不开窗;
//! - 其余参数:开窗跑骨架,`--screenshot` 出图后退出。
//!
//! 退出码:`0` 成功 / `1` 截图有差异(diff 模式)/ `2` 参数或运行错误。
use std::process::ExitCode;
use unlucky_app_iced::shot;
use unlucky_app_iced::Options;
fn main() -> ExitCode {
let options = match Options::from_env() {
Ok(options) => options,
Err(message) => {
eprintln!("[cli] {message}");
eprintln!(
"用法:unlucky-launcher-iced [--renderer=auto|gpu|cpu] [--software-renderer] \
[--theme=light|dark] [--screenshot <png>] [--screenshot-theme=light|dark] \
[--scale-factor <f32>] [--diff <a.png> <b.png>]"
);
return ExitCode::from(2);
}
};
if let Some((left, right)) = options.diff.clone() {
return match shot::diff_files(&left, &right) {
Ok(diff) => {
eprintln!("{}", diff.describe());
if diff.is_identical() {
ExitCode::SUCCESS
} else {
ExitCode::from(1)
}
}
Err(message) => {
eprintln!("[diff] {message}");
ExitCode::from(2)
}
};
}
match unlucky_app_iced::run(options) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("[run] {error}");
ExitCode::FAILURE
}
}
}