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,218 @@
|
||||
//! 命令行参数(只做骨架期需要的那些;业务开关等页面 mission 再加)。
|
||||
//!
|
||||
//! 取值语法与现役 Slint 版一致:`--key value` 与 `--key=value` 两种都认
|
||||
//! (`crates/app/src/screenshot.rs` 的 `value_of` 是同一口径),这样将来 CI 换 bin 不用改脚本。
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
|
||||
/// 渲染后端请求(D27)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum Backend {
|
||||
/// 探测后决定:有硬件 GPU 就让 iced 自己走 wgpu→tiny-skia 回退;没有就强制 CPU 起步。
|
||||
#[default]
|
||||
Auto,
|
||||
/// 优先 GPU(写 `ICED_BACKEND=wgpu,tiny-skia`:拿不到适配器仍会降级,不硬失败)。
|
||||
Gpu,
|
||||
/// 强制 CPU(`--software-renderer` 的等价物,写 `ICED_BACKEND=tiny-skia`)。
|
||||
Cpu,
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
/// 稳定名字(日志用)。
|
||||
pub const fn name(self) -> &'static str {
|
||||
match self {
|
||||
Backend::Auto => "auto",
|
||||
Backend::Gpu => "gpu",
|
||||
Backend::Cpu => "cpu",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析后的启动参数。
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Options {
|
||||
/// 渲染后端请求。
|
||||
pub backend: Backend,
|
||||
/// 主题:`None` = 探测系统主题。
|
||||
pub theme: Option<SchemeFlavor>,
|
||||
/// 截图通道:给了路径就出图后退出。
|
||||
pub screenshot: Option<PathBuf>,
|
||||
/// 截图时用的主题(`--screenshot-theme`,优先级高于 `--theme`)。
|
||||
pub screenshot_theme: Option<SchemeFlavor>,
|
||||
/// 强制窗口缩放因子(截图验收要 100% DPI;`None` = 用系统 DPI)。
|
||||
pub scale_factor: Option<f32>,
|
||||
/// `--diff <left> <right>`:只做两图逐像素对比,不开窗。
|
||||
pub diff: Option<(PathBuf, PathBuf)>,
|
||||
}
|
||||
|
||||
impl Default for Options {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
backend: Backend::Auto,
|
||||
theme: None,
|
||||
screenshot: None,
|
||||
screenshot_theme: None,
|
||||
scale_factor: None,
|
||||
diff: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Options {
|
||||
/// 从进程参数解析。
|
||||
///
|
||||
/// # Errors
|
||||
/// 参数值不合法(未知的 `--renderer` / `--theme` / 非法 `--scale-factor`)时返回人话。
|
||||
pub fn from_env() -> Result<Self, String> {
|
||||
Self::parse(std::env::args().skip(1))
|
||||
}
|
||||
|
||||
/// 从任意参数序列解析(`argv[1..]`,测试直接喂数组)。
|
||||
///
|
||||
/// # Errors
|
||||
/// 同 [`Options::from_env`]。
|
||||
pub fn parse(args: impl IntoIterator<Item = String>) -> Result<Self, String> {
|
||||
let mut options = Self::default();
|
||||
let mut args = args.into_iter();
|
||||
|
||||
while let Some(arg) = args.next() {
|
||||
let (key, inline) = match arg.split_once('=') {
|
||||
Some((key, value)) => (key.to_owned(), Some(value.to_owned())),
|
||||
None => (arg.clone(), None),
|
||||
};
|
||||
|
||||
match key.as_str() {
|
||||
"--software-renderer" => options.backend = Backend::Cpu,
|
||||
"--renderer" => {
|
||||
let value = take(&mut args, inline, "--renderer")?;
|
||||
options.backend = match value.as_str() {
|
||||
"auto" => Backend::Auto,
|
||||
"gpu" => Backend::Gpu,
|
||||
"cpu" => Backend::Cpu,
|
||||
other => {
|
||||
return Err(format!("--renderer 只认 auto|gpu|cpu,收到 {other:?}"));
|
||||
}
|
||||
};
|
||||
}
|
||||
"--theme" => {
|
||||
options.theme = Some(flavor(&take(&mut args, inline, "--theme")?)?);
|
||||
}
|
||||
"--screenshot-theme" => {
|
||||
options.screenshot_theme =
|
||||
Some(flavor(&take(&mut args, inline, "--screenshot-theme")?)?);
|
||||
}
|
||||
"--screenshot" => {
|
||||
options.screenshot =
|
||||
Some(PathBuf::from(take(&mut args, inline, "--screenshot")?));
|
||||
}
|
||||
"--scale-factor" => {
|
||||
let raw = take(&mut args, inline, "--scale-factor")?;
|
||||
let value: f32 = raw
|
||||
.parse()
|
||||
.map_err(|_| format!("--scale-factor 需要数字,收到 {raw:?}"))?;
|
||||
if !(value.is_finite() && value > 0.0) {
|
||||
return Err(format!("--scale-factor 必须 > 0,收到 {raw:?}"));
|
||||
}
|
||||
options.scale_factor = Some(value);
|
||||
}
|
||||
"--diff" => {
|
||||
let left = take(&mut args, inline, "--diff")?;
|
||||
let right = args
|
||||
.next()
|
||||
.ok_or_else(|| "--diff 需要两张图的路径".to_owned())?;
|
||||
options.diff = Some((PathBuf::from(left), PathBuf::from(right)));
|
||||
}
|
||||
other => return Err(format!("未知参数 {other:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(options)
|
||||
}
|
||||
|
||||
/// 截图时实际使用的主题(截图专用参数优先)。
|
||||
pub fn shot_flavor(&self) -> Option<SchemeFlavor> {
|
||||
self.screenshot_theme.or(self.theme)
|
||||
}
|
||||
}
|
||||
|
||||
fn take(
|
||||
args: &mut impl Iterator<Item = String>,
|
||||
inline: Option<String>,
|
||||
flag: &str,
|
||||
) -> Result<String, String> {
|
||||
inline
|
||||
.or_else(|| args.next())
|
||||
.ok_or_else(|| format!("{flag} 需要一个值"))
|
||||
}
|
||||
|
||||
fn flavor(raw: &str) -> Result<SchemeFlavor, String> {
|
||||
match raw {
|
||||
"light" => Ok(SchemeFlavor::Light),
|
||||
"dark" => Ok(SchemeFlavor::Dark),
|
||||
other => Err(format!("--theme 只认 light|dark,收到 {other:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse(args: &[&str]) -> Result<Options, String> {
|
||||
Options::parse(args.iter().map(|arg| (*arg).to_owned()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_are_auto_and_system_theme() {
|
||||
let options = parse(&[]).expect("空参数要能解析");
|
||||
assert_eq!(options.backend, Backend::Auto);
|
||||
assert_eq!(options.theme, None);
|
||||
assert_eq!(options.screenshot, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn software_renderer_is_an_alias_of_cpu() {
|
||||
assert_eq!(
|
||||
parse(&["--software-renderer"]).unwrap().backend,
|
||||
Backend::Cpu
|
||||
);
|
||||
assert_eq!(parse(&["--renderer=cpu"]).unwrap().backend, Backend::Cpu);
|
||||
assert_eq!(parse(&["--renderer", "gpu"]).unwrap().backend, Backend::Gpu);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screenshot_accepts_both_argv_forms() {
|
||||
let spaced = parse(&["--screenshot", "out.png"]).unwrap();
|
||||
let inline = parse(&["--screenshot=out.png"]).unwrap();
|
||||
assert_eq!(spaced.screenshot, Some(PathBuf::from("out.png")));
|
||||
assert_eq!(spaced.screenshot, inline.screenshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screenshot_theme_wins_over_theme() {
|
||||
let options = parse(&["--theme=dark", "--screenshot-theme=light"]).unwrap();
|
||||
assert_eq!(options.theme, Some(SchemeFlavor::Dark));
|
||||
assert_eq!(options.shot_flavor(), Some(SchemeFlavor::Light));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_values_are_reported_not_panicked() {
|
||||
assert!(parse(&["--renderer=metal"]).is_err());
|
||||
assert!(parse(&["--theme=blue"]).is_err());
|
||||
assert!(parse(&["--scale-factor=0"]).is_err());
|
||||
assert!(parse(&["--scale-factor=abc"]).is_err());
|
||||
assert!(parse(&["--screenshot"]).is_err());
|
||||
assert!(parse(&["--diff", "only-one.png"]).is_err());
|
||||
assert!(parse(&["--nope"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_takes_two_paths() {
|
||||
let options = parse(&["--diff", "a.png", "b.png"]).unwrap();
|
||||
assert_eq!(
|
||||
options.diff,
|
||||
Some((PathBuf::from("a.png"), PathBuf::from("b.png")))
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user