Files
Unlucky-Launcher/crates/app-iced/src/renderer/acceptance/window.rs
T
AskaEth 8153e54f87 fix(acceptance): M98 r1 —— [window] 绑定真 settings、语言维度补断言、boot.rs 拆分
复审 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)全绿。
2026-11-03 12:59:57 +08:00

143 lines
5.8 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.
//! `[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
}