复审 r1(reviewer-accscope-code,p2-2items / fix-then-merge)的两条阻塞 + 四条顺手的 p3。 ## p2-1 `[window]` 的三个窗口属性曾是格式串常量(U1:常量冒充观测结果) 第一版把 `resizable=true decorations=false transparent=false` 写在格式化串里,与交给 iced 的 `window::Settings` 没有任何绑定;定向变异把 `settings.decorations` 改成 `true`,**371 个测试全绿**, 而验收包里 `[caps]` 与 `[window]` 会互相矛盾。 - 新增 `acceptance::window::WindowFacts` + `from_settings()`:字段只能从**真实 settings** 抽出来, 没有手抄入口;`window_text()` 逐字段打印。 - 窗口设置收成单一产地 `app/boot/diagnostics::window_settings()`,`launch()` 与 `[window]`/`[caps]` 两行日志共用同一个 `&window::Settings`;`pages/harness.rs` 同样从自己那份 settings 取。 - 守门测试 `window_settings_match_what_the_acceptance_logs_claim`,含「from_settings 必须逐字段 搬运」的反向断言。变异实证:`window_settings` 的 `decorations → true` ⇒ 红并点名; `from_settings` 里写死 `decorations: false` ⇒ 红(第一次写测试时这条漏抓,补强后实测红)。 ## p2-2 语言维度空转、且没有任何结论断言 `lang::resolve` 是 setting → 平台源 → env,macOS 的 AppleLanguages 几乎恒有值 ⇒ `LANG=zh_CN` / `LANG=en_US` 两档同值,而原脚本只断言「`[i18n]` 这行存在」——整条链退化成 `source=default lang=en` 时核心集一条都不红。 - 脚本加两条结论断言:`^\[i18n\].*platform=applelanguages:[A-Za-z-]+` 与 ` source=applelanguages` (退化日志上实测红、健康日志上实测绿)。 - `REPORT.md` 语言行下注明:env 优先级低于平台源,正常 Mac 上两档同值,这一维验的是「平台源 真读到了 + 中文字体回退」,不是中英对照。 ## p3(复审 + 第二路自跑复核同源的三条) - `[perf] boot probe=0ms` → `probe=skipped`(没计时就不报数;降级重开那一轮改为**真计时**)。 变异实证:把 skipped 写成 0ms ⇒ `perf_lines_carry_measured_millis` 红。 - `[env] gpu=` 排序改成 `DiscreteGpu > IntegratedGpu > 其它`:本机原来报核显,而 iced 实际选的是 独显(现在报 `AMD Radeon RX 6600 XT [DiscreteGpu]`,与 `Selected:` 一致)。 - `[i18n] setting=<未设置>` → `<未接线>`(配置侧根本没读取点,写「未设置」等于把「我们没读」 说成「用户没设」)。 - `[backend] final=` 失败行:`refuse` / `none` 两条路是 `final=none`(什么都没跑起来),只有 `respawn` 才是兜底名;`pages/harness.rs` 不再写死 `via="refuse"`,改用共用的 `failure_via()`。 实跑负例:`[backend] final=none result=failed primary=wgpu fallback=tiny-skia via=refuse`。 - 脚本 `[tick]` 那处唯一未锚定的 `field()`:加 `^` 锚定、正则改 `clock=[a-z0-9-]+`(真值是 `idle-1hz`,旧 `[a-z]+` 把它截成 `idle`,靠前缀巧合通过);断言也改成 `clock=idle-1hz`。 - 脚本 `[backend] final` 取值改 `field_backend()`:优先 `result=started`(respawn 场景一条日志里 有父 failed + 子 started 两条,旧 `head -1` 会把「这次没跑起来」写进报告)。 - 脚本 `exec 3>&1` 全程留痕(终端提示也进 `logs/00-run.log`)、跑前清空 `$OUT/shots/`、 python3 缺失时在 REPORT 里写明「几何列未采」而不是留空。 - `renderer/mod.rs` 补一段依据:为什么「单项 ICED_BACKEND ⇒ 没有静默回退」成立(`iced_tiny_skia` 的 `with_backend` 只接受 `tiny-skia`),以及它依赖 iced 实现细节的两层缓解。 ## 结构 `app/boot.rs` 400 行零余量 → 拆成 `boot.rs`(248) + `boot/fallback.rs`(117,兜底/重开/退出码) + `boot/diagnostics.rs`(110,窗口设置与四段报告);`run()` 回到「探测 → 字体 → 打印 → 起 iced」。 `acceptance` 再把 `[window]` 拆成 `window.rs`(142)。 门禁:`cargo fmt --check` / `clippy --all-targets -- -D warnings` / `test`(373 lib + 6 integration)全绿。
143 lines
5.8 KiB
Rust
143 lines
5.8 KiB
Rust
//! `[window]`:**本进程给窗口的那些设置**(不是我们从别处猜的)。
|
||
//!
|
||
//! 唯一产地是 `app/boot/diagnostics.rs::window_settings`(`pages_demo` 通道自己也有一份,
|
||
//! 它没有最小尺寸与自定标题)。这里只做「从 `window::Settings` 抽字段 → 排版」。
|
||
//!
|
||
//! **别再把这些字段写进格式化串**:M98 r1 p2-1 就是这么错的——日志说 `decorations=false`,
|
||
//! 而真实 `settings` 可以改成 `true` 而不惊动任何断言(踩坑记录 U1)。
|
||
|
||
/// 窗口尺寸覆盖(`ULL_WINDOW_SIZE=1520x900`)。
|
||
///
|
||
/// 为什么是环境变量而不是 `--width`:`cli.rs` 不在 M98 的 scope,而验收矩阵必须有「最小尺寸 /
|
||
/// 宽屏」两档,且必须是**真外壳**(自绘三键 + 标题栏)——`pages_demo` example 的标题栏是留白带,
|
||
/// 出不了「三键位置」那张图。语义与 `ULL_START_ROUTE` 一致:只影响这一次进程。
|
||
pub const WINDOW_SIZE_ENV: &str = "ULL_WINDOW_SIZE";
|
||
|
||
// ---------------------------------------------------------------- [window]
|
||
|
||
/// 窗口事实:**从真实的 `window::Settings` 抽出来**的那几个字段(唯一产地见
|
||
/// `app/boot/diagnostics.rs` 的 `window_settings`)。
|
||
///
|
||
/// 为什么要有这个结构:M98 r1 p2-1 —— 第一版把 `resizable=true decorations=false transparent=false`
|
||
/// 写在格式化串里,与真实 `settings` 无绑定;定向变异把 `settings.decorations` 改成 `true`,
|
||
/// 371 个测试全绿,而日志会与 `[caps]` 互相矛盾(踩坑记录 U1)。现在这些字段只能从
|
||
/// [`WindowFacts::from_settings`] 来,没有手抄的入口。
|
||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||
pub struct WindowFacts {
|
||
/// 窗口尺寸(逻辑像素)。
|
||
pub size: (f32, f32),
|
||
/// 最小尺寸(`None` = 没设)。
|
||
pub min: Option<(f32, f32)>,
|
||
/// 可缩放。
|
||
pub resizable: bool,
|
||
/// 系统装饰(`false` ⇒ 自绘三键)。
|
||
pub decorations: bool,
|
||
/// 整窗透明。
|
||
pub transparent: bool,
|
||
}
|
||
|
||
impl WindowFacts {
|
||
/// 从**真正交给 iced/winit 的那个** `window::Settings` 抽字段。
|
||
#[must_use]
|
||
pub fn from_settings(settings: &iced::window::Settings) -> Self {
|
||
Self {
|
||
size: (settings.size.width, settings.size.height),
|
||
min: settings.min_size.map(|size| (size.width, size.height)),
|
||
resizable: settings.resizable,
|
||
decorations: settings.decorations,
|
||
transparent: settings.transparent,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// `[window]` 的**请求值**一行(实际值见模块文档的表:脚本从 `[shot] … scale=` 与 PNG 换算)。
|
||
///
|
||
/// `title` 是 `Option`:`pages_demo` 那条通道没有自定标题,打 `<默认>` —— 编一个空串会让
|
||
/// 「标题是什么」变成猜的。
|
||
pub fn report_window(
|
||
facts: &WindowFacts,
|
||
scale: Option<f32>,
|
||
size_source: &str,
|
||
title: Option<&str>,
|
||
) {
|
||
eprintln!("{}", window_text(facts, scale, size_source, title));
|
||
}
|
||
|
||
/// 见 [`report_window`]。
|
||
#[must_use]
|
||
pub fn window_text(
|
||
facts: &WindowFacts,
|
||
scale: Option<f32>,
|
||
size_source: &str,
|
||
title: Option<&str>,
|
||
) -> String {
|
||
let min = facts
|
||
.min
|
||
.map_or_else(|| "none".to_owned(), |(w, h)| format!("{w}x{h}"));
|
||
let title = title.map_or_else(|| "<默认>".to_owned(), |text| format!("\"{text}\""));
|
||
format!(
|
||
"[window] requested size={}x{} min={min} resizable={} decorations={} transparent={} scale_requested={} size_source={size_source} title={title}",
|
||
facts.size.0,
|
||
facts.size.1,
|
||
facts.resizable,
|
||
facts.decorations,
|
||
facts.transparent,
|
||
scale.map_or_else(|| "system".to_owned(), |value| value.to_string()),
|
||
)
|
||
}
|
||
|
||
/// 解析 `ULL_WINDOW_SIZE`(`1520x900` / `1520X900` / `1520×900`)。
|
||
///
|
||
/// 返回 `None` = 不是这个格式(调用方保留默认尺寸并打一行警告,**不**静默忽略)。
|
||
#[must_use]
|
||
pub fn parse_window_size(raw: &str) -> Option<(f32, f32)> {
|
||
let trimmed = raw.trim();
|
||
let (width, height) = trimmed
|
||
.split_once(['x', 'X', '×', '*'])
|
||
.map(|(w, h)| (w.trim(), h.trim()))?;
|
||
let width: f32 = width.parse().ok()?;
|
||
let height: f32 = height.parse().ok()?;
|
||
(width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0)
|
||
.then_some((width, height))
|
||
}
|
||
|
||
/// 尺寸覆盖 → 实际要开多大 + **尺寸来源**(进 `[window] … size_source=`)。
|
||
///
|
||
/// 三种来源:`default`(没设 / 设了但解析不了)与 `ULL_WINDOW_SIZE`(生效)。
|
||
/// 小于最小尺寸时夹到最小并标 `:clamped-to-min`——iced 的 `min_size` 本来也会拦住,
|
||
/// 但那样日志会说「要求 640x480」而实际是 880x560,是典型的「日志说请求、报告当结果」。
|
||
#[must_use]
|
||
pub fn resolve_window_size(
|
||
raw: Option<&str>,
|
||
default: (f32, f32),
|
||
min: (f32, f32),
|
||
) -> ((f32, f32), &'static str) {
|
||
let Some(raw) = raw else {
|
||
return (default, "default");
|
||
};
|
||
let Some((width, height)) = parse_window_size(raw) else {
|
||
return (default, "ULL_WINDOW_SIZE:invalid");
|
||
};
|
||
|
||
let clamped = (width.max(min.0), height.max(min.1));
|
||
if clamped == (width, height) {
|
||
(clamped, "ULL_WINDOW_SIZE")
|
||
} else {
|
||
(clamped, "ULL_WINDOW_SIZE:clamped-to-min")
|
||
}
|
||
}
|
||
|
||
/// 窗尺寸(含来源)从进程环境取一遍——`app::boot` 与测试共用同一条路径。
|
||
#[must_use]
|
||
pub fn window_size(default: (f32, f32), min: (f32, f32)) -> ((f32, f32), &'static str) {
|
||
let raw = std::env::var(WINDOW_SIZE_ENV).ok();
|
||
let resolved = resolve_window_size(raw.as_deref(), default, min);
|
||
if resolved.1.ends_with("invalid") {
|
||
eprintln!(
|
||
"[window] {WINDOW_SIZE_ENV}={:?} 解析不了(要 1520x900 这种),按默认尺寸跑",
|
||
raw.unwrap_or_default()
|
||
);
|
||
}
|
||
resolved
|
||
}
|