Files
Unlucky-Launcher/crates/app-iced/src/shot/mod.rs
T
AskaEth a1adf024ba fix(app-iced): M68 r1 —— M69 迁移 + 守门补洞 + 药丸 56×32 + caption 图标 24 + 东侧热区回 1180
代码复审 p2-2items / 视觉复审 p2-2items 的修复轮(配色重拍等 M70 定案)。

M69 迁移(2 行)
- nav 轨宽:nav_rail_width(metrics) 函数体改用 metrics.nav_rail_width
- theme::STROKE_PX 改为 pub use unlucky_design::stroke::HAIRLINE as STROKE_PX

P2-1 零硬编码守门补洞(reviewer 造样 5 个漏网样本全部补上,并进自检正向断言)
- COLOR 调用表补 from_rgb8 / rgb8 / rgba(`from_rgb(` 子串匹配不到 `from_rgb8(` 是根因)
- 新增 `Color { r: .., g: .. }` 结构体字面量检查
- 尺寸规则遇到 Length::Fixed( / Padding::from( / Size::new( 往里剥一层再判数字
- 新增渲染层回归守门 pill_never_overrides_its_size_with_fill(禁 nav.rs 出现 center_*( ):
  视觉复审 P2-1 的病根就是 center_*(Length::Fill) 把容器尺寸覆盖成 Fill

视觉 P2-1 药丸 88×32 → 56×32(实测 x=16..71)
- 去掉 center_x/center_y(Fill);居中改由父 column 的 align_x + 显式 width(轨宽) 完成
  (只去掉 center_* 还不够:column 会 shrink 到 56,药丸贴左缘 x=0..55,是同一个坑的第二层)

视觉 P2-2 / 用户点名 caption 图标 46 → 24(按钮容器保持 46×48)
- button 内包 container(...).width(24).height(24).center(Length::Fill):
  只设 width/height 会让容器缩到 24 并贴 button 内容左上角;center(Fill) 才既撑满又居中

视觉 P3-1 东侧/南侧缩放热区改成叠层
- 原来是 row 的兄弟列,吃掉 4px 布局宽(右留白 28、关闭键离窗角 4px);
  现在用 stack! + 对齐容器叠在工作区上,布局宽回到 1180(卡片右缘 x=1155 = 右留白 24)

P3 清单
1. --data-dir 接受并忽略(翻牌前 CI 兼容;现役 smoke 命令行可原样跑,exit 0)+ 单测
2. --diff 内建 ±N px 平移扫描(--diff-scan),CI 不再依赖 numpy
3. 截图模式等 SystemTheme 到达才出图(否则暗色系统上会拍到浅色)
4. (Auto, 未编入 wgpu) 返回 tiny-skia,不再把 wgpu 写进候选列表
5. --renderer=gpu 也跳过适配器枚举(决策与探测无关,省 ~0.6~1.2s)
6. icons::handle 加 debug_assert_eq!(IconKind::ALL[kind as usize], kind)
7. 轨顶留白改 container padding-top,实测 24(原来 spacer + column spacing = 28)

顺带:shot.rs 拆成 shot/mod.rs + shot/diff.rs(≤400 行门禁)

门禁:fmt OK / clippy -D warnings 干净 / 46+5 测试全过 / cargo check --workspace --all-targets OK
体积:双后端 6,769,664 B(6.46 MiB)、仅 CPU 3,955,712 B(3.77 MiB)
截图:r1 代码验证图(r1-verify / hover / pressed / compare-nav-rail)已出;4 张交付主图待 M70 定案后重拍
2026-11-01 17:46:55 +08:00

101 lines
3.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.
//! 截图通道(D30 验收用):`iced::window::screenshot` 的真窗口出图 → PNG,以及两图逐像素对比。
//!
//! ## 为什么用真窗口而不是 `iced_test::Emulator`
//!
//! 任务书首选路径①:真开窗截图覆盖了标题栏/DPI/窗口管理的真实行为,离屏通道不覆盖
//! (M60 §Q6 的边界说明)。本机与 Windows CI 都有交互会话,所以走路径①。
//!
//! ## 关键事实(M60 踩坑 Q3)
//!
//! `Screenshot` 的字节字段在 0.14 叫 **`rgba`**(0.13 是 `bytes`),`size` 是**物理像素**,
//! 另带 `scale_factor`(换算逻辑坐标用)。落盘用 `image::save_buffer` + `ExtendedColorType::Rgba8`。
//!
//! ## 为什么自己带一个 diff
//!
//! D30 的硬项是「GPU 档与 CPU 档**几何逐像素一致**,只有颜色/材质不同」。CI 里不该假设有
//! Python/PIL/ImageMagick,所以把「读两张 PNG → 数差异像素 → 给最大/平均通道差」做进本程序
//! (`--diff a.png b.png`),复用同一个二进制。
use std::path::{Path, PathBuf};
use iced::window;
use crate::theme;
/// 把一帧截图写到 `path`(自动建父目录)。
///
/// # Errors
/// 目录建不出来、PNG 编码失败、字节数与尺寸对不上时返回人话(截图通道要能一眼看出哪一步断)。
pub fn save(screenshot: &window::Screenshot, path: &Path) -> Result<Summary, String> {
let width = screenshot.size.width;
let height = screenshot.size.height;
let bytes = screenshot.rgba.as_ref();
let expected = width as usize * height as usize * 4;
if bytes.len() != expected {
return Err(format!(
"截图字节数对不上:{width}x{height} 应该 {expected} B,实际 {} B",
bytes.len()
));
}
write_rgba(bytes, width, height, path)?;
Ok(Summary {
width,
height,
scale_factor: screenshot.scale_factor,
path: path.to_path_buf(),
})
}
fn write_rgba(bytes: &[u8], width: u32, height: u32, path: &Path) -> Result<(), String> {
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent)
.map_err(|error| format!("建目录 {} 失败:{error}", parent.display()))?;
}
}
image::save_buffer(path, bytes, width, height, image::ExtendedColorType::Rgba8)
.map_err(|error| format!("写 PNG {} 失败:{error}", path.display()))
}
/// 一次截图的结果摘要(打日志与报告用)。
#[derive(Debug, Clone, PartialEq)]
pub struct Summary {
/// 物理宽(px)。
pub width: u32,
/// 物理高(px)。
pub height: u32,
/// 窗口缩放因子(100% DPI = 1.0)。
pub scale_factor: f32,
/// 落盘路径。
pub path: PathBuf,
}
impl Summary {
/// 一行日志。
pub fn describe(&self) -> String {
format!(
"[shot] {}x{} scale={} -> {}",
self.width,
self.height,
self.scale_factor,
self.path.display()
)
}
}
mod diff;
pub use diff::{compare, diff_files, diff_files_with_scan, shift_scan, Diff, Image};
/// 主题名 → 归档文件名片段(`visual-acceptance-protocol.md` §7 的命名)。
pub fn flavor_slug(ui: &theme::Ui) -> &'static str {
match ui.flavor() {
unlucky_design::color::SchemeFlavor::Light => "light",
unlucky_design::color::SchemeFlavor::Dark => "dark",
}
}