三条复审意见 + 六条 p3,全部落在原有文件里(另加两个拆分文件)。 P2-1 截图落盘「最后写入者说了算」(真竞态) `Message::Frame` 以前在 `frames >= CAPTURE_AT_FRAME` 之后每帧都发一次 `window::screenshot`, `Captured` 每次都覆盖同一文件才 `exit()`;已派发的采样不会因 exit 撤回 ⇒ 落盘内容是调度结果 (复审实测 12 次里 7 次写了两次;把窗口在两次采样之间改成 900×600,落盘就是 900×600)。 现在闸门 = `should_request_capture(frames, dispatched, capture_mode)`,派发即置位、只开一次; `Captured` 计数落盘次数并打进日志。实测 12/12 次「[shot] -> 文件」行恒为 1、`落盘次数=1`。 P2-2 密闭证据换成 OS 层 `platform/win32/transparency.rs` 加安全 getter `click_through`(unsafe 仍只在既有唯一落点); 该文件因超 400 行又拆出 `transparency/exstyle.rs`(子模块继承父模块的 `#![allow(unsafe_code)]`, `unsafe-scope` 门禁判据不受影响)。日志三态 `[caps] passthrough_ext=on|off|unknown`: `None` 记 `unknown`,绝不记 `off`。实测默认 on 12/12、`--passthrough=off` 为 off 3/3。 P2-3 头注释「下放」→「新增」,并订正「接真控件改 3 个文件」 (section 文件里的 body 绊线 + 该 section 的 `GROUP` 文件 + `tests/mod.rs` 跨 section 绊线)。 p3 六条:shell 断言说明改准(默认值那条归 cli 的测试)、harness 文档给「指针事件=0」加对照限定、 数字订正(纯搬移 234 / 新增 134)、三文件口径、ai+about 对本文件的两处必改、`Rest` 与真 Disabled 撞语义写进「抓不住」。 文档口径:「8 连跑全稳」不再当密闭证据——同 argv 开/关穿透 hash 相同(实测 0d7b4f5b 两边一样), 它只说明「帧静止 + 落盘确定」。密闭主证据是 `[caps] passthrough_ext=`。 顺带:`run()` 从 `harness.rs` 拆到 `harness/app.rs`(272 / 190 行)。搬移中丢过一条既有断言 `ensure_layered_refuses_a_missing_or_invalid_window`,被编译器 unused-import 警告抓出并复原。
288 lines
13 KiB
Rust
288 lines
13 KiB
Rust
//! 命令行参数(骨架期 + 组件库 harness 需要的那些;业务开关等页面 mission 再加)。
|
||
//!
|
||
//! 取值语法与现役 Slint 版一致:`--key value` 与 `--key=value` 两种都认
|
||
//! (`crates/app/src/screenshot.rs` 的 `value_of` 是同一口径),这样将来 CI 换 bin 不用改脚本。
|
||
//!
|
||
//! ## 参数总表
|
||
//!
|
||
//! | 参数 | 取值 | 语义 |
|
||
//! |---|---|---|
|
||
//! | `--software-renderer` | — | 等价 `--renderer=cpu`(D27 的 CPU 档) |
|
||
//! | `--renderer` | `auto`/`gpu`/`cpu` | 渲染后端请求 |
|
||
//! | `--theme` | `light`/`dark` | 主题;不给则跟随系统(仅交互模式) |
|
||
//! | `--screenshot` / `--screenshot-theme` | 路径 / `light`·`dark` | 截图通道(出图后退出)与截图专用主题 |
|
||
//! | `--screen` | `shell`/`gallery` | 启动画面;**`gallery` = 组件库视觉验收 harness**(D39 §5) |
|
||
//! | `--scale-factor` | `>0` | 强制缩放因子(验收要 100%,即 `1`) |
|
||
//! | `--diff` / `--diff-scan` | 两个路径 / 非负整数 | 逐像素对比(不开窗)与 ±n px 平移扫描 |
|
||
//! | `--force-state` | `rest`/`hover`/`pressed` | 把整屏可交互控件强制成某态(取证);**给了 `--screenshot` 就默认 `rest`** |
|
||
//! | `--data-dir` | 路径 | 翻牌兼容:**接受并忽略**(CI 冒烟在用) |
|
||
//!
|
||
//! **截图主题默认 Light(门禁依赖,不许改回 None)**,理由与不变量见
|
||
//! [`Options::capture_flavor`] 与 `app.rs` 的截图路径。
|
||
|
||
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",
|
||
}
|
||
}
|
||
|
||
/// 截图日志用的后端说明(`cfg!(feature = "gpu")` 决定有没有 wgpu 这一档)。
|
||
pub fn note(self) -> &'static str {
|
||
if !cfg!(feature = "gpu") {
|
||
return "cpu(build without wgpu)";
|
||
}
|
||
match self {
|
||
Backend::Cpu => "cpu(forced)",
|
||
Backend::Gpu => "gpu(preferred)",
|
||
Backend::Auto => "auto",
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 启动画面(D39 §5:`gallery` 是视觉验收 harness,不是产品页面)。
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||
pub enum Screen {
|
||
/// 产品骨架:自绘标题栏 + 导航轨 + 内容区。
|
||
#[default]
|
||
Shell,
|
||
/// 组件库 harness:整窗只放状态矩阵。
|
||
Gallery,
|
||
}
|
||
|
||
impl Screen {
|
||
/// 稳定名字(日志用)。
|
||
pub const fn name(self) -> &'static str {
|
||
match self {
|
||
Screen::Shell => "shell",
|
||
Screen::Gallery => "gallery",
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 解析后的启动参数。
|
||
#[derive(Debug, Clone, PartialEq, Default)]
|
||
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,即 `1`;`None` = 用系统 DPI)。
|
||
pub scale_factor: Option<f32>,
|
||
/// `--diff <left> <right>` 与 `--diff-scan <n>`:逐像素对比(不开窗)与 ±n px 平移扫描。
|
||
pub diff: Option<(PathBuf, PathBuf)>,
|
||
/// 见上(默认 0 = 不扫)。
|
||
pub diff_scan: u32,
|
||
/// `--force-state=hover|pressed`:把按钮强制成某个状态便于截图取证(D30 的状态图)。
|
||
pub force_state: Option<ForcedState>,
|
||
/// `--screen=shell|gallery`:启动画面(`gallery` = 组件库视觉验收 harness)。
|
||
pub screen: Screen,
|
||
/// `--data-dir <dir>`:当前**接受并忽略**(现役 Slint 版 CI 冒烟在用;
|
||
/// 真正的消费由后续页面 mission 接 `host`/`state` 时做)。
|
||
pub data_dir: Option<PathBuf>,
|
||
}
|
||
|
||
/// 截图取证用的强制交互状态(只影响观感,不改变任何状态机)。
|
||
///
|
||
/// 五态完整矩阵由 `--screen=gallery` 的 `Button::state(..)` 出,本开关只给整屏取证。
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum ForcedState {
|
||
/// **常态**(静息):压掉系统光标带来的 hover/pressed。
|
||
///
|
||
/// 截图通道(给了 `--screenshot`)**默认就是它**,理由是实测:M99 复审在同一个 exe 上连跑 8 次,
|
||
/// 第 6 次给出第三个 hash,差异区是某个按钮被画成 `Hovered`——因为 harness/产品都开真窗口,
|
||
/// `button::Status` 由系统光标位置决定。取证图不能因为「指针恰好压在某个按钮上」而变色。
|
||
/// 显式 `--force-state=hover|pressed` 仍然优先(那是**故意**要画那个态)。
|
||
///
|
||
/// ⚠️ **语义边界**:`Rest` 落成 `Interaction::Enabled`,而 `components::buttons::assemble()`
|
||
/// 把「没有 `on_press`」的按钮默认画成 **Disabled** —— 于是 `Rest` 会把一个本应禁用的按钮
|
||
/// 画成可用。当前零影响(所有 `forced_state(` 调用点与 shell 的 caption / nav 按钮都带消息),
|
||
/// 但给「无消息按钮」出取证图时别套 `Rest`。
|
||
Rest,
|
||
/// 所有可交互控件按 hover 画。
|
||
Hover,
|
||
/// 所有可交互控件按 pressed 画。
|
||
Pressed,
|
||
}
|
||
|
||
impl Options {
|
||
/// 从进程参数解析(错误信息是人话,不 panic、不 `exit`)。
|
||
///
|
||
/// # Errors
|
||
/// 取值不合法(未知 `--renderer`/`--theme`/`--screen`、非法 `--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)));
|
||
}
|
||
"--diff-scan" => {
|
||
let raw = take(&mut args, inline, "--diff-scan")?;
|
||
options.diff_scan = raw
|
||
.parse()
|
||
.map_err(|_| format!("--diff-scan 需要非负整数,收到 {raw:?}"))?;
|
||
}
|
||
"--force-state" => {
|
||
let raw = take(&mut args, inline, "--force-state")?;
|
||
options.force_state = Some(match raw.as_str() {
|
||
"rest" => ForcedState::Rest,
|
||
"hover" => ForcedState::Hover,
|
||
"pressed" => ForcedState::Pressed,
|
||
other => {
|
||
return Err(format!(
|
||
"--force-state 只认 rest|hover|pressed,收到 {other:?}"
|
||
));
|
||
}
|
||
});
|
||
}
|
||
"--screen" => {
|
||
let raw = take(&mut args, inline, "--screen")?;
|
||
options.screen = match raw.as_str() {
|
||
"shell" => Screen::Shell,
|
||
"gallery" => Screen::Gallery,
|
||
other => {
|
||
return Err(format!("--screen 只认 shell|gallery,收到 {other:?}"));
|
||
}
|
||
};
|
||
}
|
||
// 翻牌前的兼容参数:接受并忽略(见字段文档)
|
||
"--data-dir" => {
|
||
options.data_dir = Some(PathBuf::from(take(&mut args, inline, "--data-dir")?));
|
||
}
|
||
other => return Err(format!("未知参数 {other:?}")),
|
||
}
|
||
}
|
||
|
||
// **截图通道默认强制 `Rest`**(M100):取证图不许跟着系统指针的位置变。
|
||
// 交互模式保持 `None`——那是用户的真窗口,不许替用户把状态钉死(钉死了就永远看不到 hover)。
|
||
if options.screenshot.is_some() && options.force_state.is_none() {
|
||
options.force_state = Some(ForcedState::Rest);
|
||
}
|
||
|
||
Ok(options)
|
||
}
|
||
|
||
/// 截图时实际使用的主题(**只表达用户显式给了什么**;默认值见 [`Options::capture_flavor`])。
|
||
pub fn shot_flavor(&self) -> Option<SchemeFlavor> {
|
||
self.screenshot_theme.or(self.theme)
|
||
}
|
||
|
||
/// 本次进程真正要画的主题(**唯一取值口**)。
|
||
///
|
||
/// 截图模式(给了 `--screenshot`)**永远返回 `Some`**:`--screenshot-theme` > `--theme` > Light。
|
||
/// 交互模式只有显式指定才有值(`None` = 等系统主题)。
|
||
///
|
||
/// ⚠️ **门禁依赖,不许把截图分支改回 `None`**:CI 的 runtime-smoke 打
|
||
/// `--software-renderer --screenshot <png> --data-dir <dir>`(不带主题),xvfb 里没有桌面主题
|
||
/// 会话,M68 的「等系统主题到达再出图」闸门在这条路径上会把 runner 挂到超时(不是红,更难查)。
|
||
pub fn capture_flavor(&self) -> Option<SchemeFlavor> {
|
||
match self.screenshot {
|
||
Some(_) => Some(self.shot_flavor().unwrap_or(SchemeFlavor::Light)),
|
||
None => self.shot_flavor(),
|
||
}
|
||
}
|
||
|
||
/// 是否要问系统主题。截图模式**永远不问**——这就是「无主题环境不会卡住」的不变量。
|
||
pub fn wants_system_theme(&self) -> bool {
|
||
self.capture_flavor().is_none()
|
||
}
|
||
}
|
||
|
||
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)]
|
||
#[path = "cli/tests.rs"]
|
||
mod tests;
|