Merge branch 'feat/m127-shotci-make-pixel-guards-actually-r'

This commit is contained in:
AskaEth committed 2026-11-05 17:39:59 +08:00
commit 494a45fe12
8 files changed
+204 -42

No files matched your search

+52
View File
@@ -77,6 +77,58 @@ jobs:
libwayland-dev libxcb1-dev
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
# ---- 下载页选项区的四条像素守门(M127):先出图,再把路径喂给 `--lib` ----
# 之前这四条靠开发者本机手动设 `ULL_DL_*_SHOT`(不设就 SKIP),而本文件里 grep 这些
# 变量零命中 ⇒ **CI 里一次都没跑过**。空跑的守门长期等于没有,所以这两步是它们的第一处
# 常驻调用点。(另有一道保险:变量缺失且 `CI` 已设置时,守门自己会失败而不是跳过。
# 于是将来谁把下面出图那步重构掉,构建会红。)
#
# **为什么只在 Windows**:断言里有几条直接量墨迹(行高 44、版本号墨迹条数、图标药丸
# 40×40),这些数字按本仓字体栈(Segoe UI / Consolas)标定;Linux/macOS runner 上
# 这套字体不存在,回退字体的字高与行距一变就是假红。同一条限制也写在
# `crates/app-iced/src/pages/download/tests/options_shot/feed.rs` 的模块文档里
# (非 Windows 喂了图会当场失败),不靠「碰巧只配了一处」。
- name: 出四张像素守门用的图并写进 $GITHUB_ENV(仅 Windows)
if: runner.os == 'Windows'
shell: bash
run: |
set -euo pipefail
shots="$RUNNER_TEMP/dl-shots"
mkdir -p "$shots"
render() { # $1=宽 $2=文件名
cargo run -q -p unlucky-app-iced --example pages_demo -- --screen=download \
--width="$1" --height=760 --screenshot="$shots/$2" \
--screenshot-theme=light --software-renderer
}
# ready700:**不设** ULL_DL_DEMO(缺省就是 ready,还没选版本)
render 700 ready700.png
export ULL_DL_DEMO=options-fabric # 其余三张都是「选中版本 + 选项区展开」
render 700 chips700.png
render 736 compact736.png
render 1180 wide1180.png
for shot in ready700 chips700 compact736 wide1180; do
test -s "$shots/$shot.png" || { echo "::error::$shot.png 没出图"; exit 1; }
done
ls -l "$shots"
{
echo "ULL_DL_READY_SHOT=$shots/ready700.png"
echo "ULL_DL_CHIPS_SHOT=$shots/chips700.png"
echo "ULL_DL_COMPACT_SHOT=$shots/compact736.png"
echo "ULL_DL_OPTIONS_SHOT=$shots/wide1180.png"
} >> "$GITHUB_ENV"
# 单独跑一遍只为**取证**:日志里那行 `test result: ok. 4 passed` 是「守门真跑了」的
# 直接证据(紧接着的 `--workspace` 那步会再跑一遍,热的,代价可忽略)。
- name: 四条像素守门(已喂图,仅 Windows)
if: runner.os == 'Windows'
run: cargo test -p unlucky-app-iced --lib options_shot
# 断言红的时候要先看到那张图(否则「像素不对」只有一句话,没有现场)。
- name: 上传喂进守门的四张图(失败也传)
if: always() && runner.os == 'Windows'
uses: actions/upload-artifact@v4
with:
name: dl-shots
path: ${{ runner.temp }}/dl-shots/
if-no-files-found: warn
# 同 clippy:`--workspace` 不含 `unlucky-app`(M118),Slint 退役树不再被 `--all-targets` 拉起。
# 也因此**不要**再写 `--exclude unlucky-app`:它已不是成员,那面旗子只会打一条
# `excluded package(s) … not found in workspace` 的警告然后空转(实测 exit 0,不报错)。
+9 -6
View File
@@ -31,8 +31,10 @@
//! 修法是 [`TailShare`] 两档:三栏档照旧钉死(面板整块在屏内),chips 档**且面板真的占高度**
//! (选项区展开 / 安装中)时改成两半各 `Fill`——列表拿到 ≈182px(**4 整行**),面板拿到同样一半、
//! **自己可滚**(顶端那个实心「安装」按钮与五个 loader chip 仍在屏内,实例名那一格滚一下就到)。
//! 「刚进页面还没选版本」(`Hint`,面板只有一行提示)**不共享**:那一态 8 个版本能全在屏,
//! 五五分会砍成 4 行、下面留 ≈160px 空白(r1 复审 P2-1 的回归)。
//! 「刚进页面还没选版本」(`Hint`,面板只有一行提示)**不共享**:那一态 main 本来就有
//! **7 整行**在屏(不是 8:第 8 条墨迹其实是 `Pick a version, then install` 那句 hint,
//! 横向范围对不上版本号;700×760 上放 8 行要 352px,实测只剩 309~322px),五五分会砍成
//! **4 整行**、下面留 ≈160px 空白(r1 复审 P2-1 的回归,r2 修回 7 整行)。
//!
//! 这条判据按**档位**而不是按高度:页面的模型里只有 `window_width`,窗口高度没接进页面,
//! 所以「760 高到底够不够」在运行时是未知量(边界写在 [`TailShare`] 的文档里)。
@@ -133,7 +135,7 @@ pub(super) fn content<'a>(
/// |---|---|---|
/// | `Wide` / `Narrow` / `Compact` | [`TailShare::Pinned`]:面板按内容高钉在底部,列表吃剩下的 | 竖栏档在 760 高的窗口里列表还剩 158px(3 整行),面板整块在屏内——两样都对 |
/// | `Chips` + 面板**占高度**(选项区展开 / 安装中) | [`TailShare::Shared`]:两半各 `Fill`,面板自己可滚 | chips 档比三栏档多花两行 chip(tabs 48 + chips 48 + 间距),列表只剩 **18px**(M123 实测 0.4 行),面板 316px 全在屏内也没用——版本号读不出来就换不了版本 |
/// | 任何档 + 只画 `Hint`(没选版本,面板只有一行) | [`TailShare::Pinned`] | 一行提示不值得对半分:700×760 下 8 个版本能全在屏,对半分只剩 4 个、下面留 160px 空白(r1 复审 P2-1) |
/// | 任何档 + 只画 `Hint`(没选版本,面板只有一行) | [`TailShare::Pinned`] | 一行提示不值得对半分:700×760 下 main 是 **7 整行**在屏(不是 8——第 8 条墨迹其实是 `Pick a version, then install` 那句 hint,横向范围对不上版本号;要放 8 行得 352px,实测只剩 309~322px),对半分只剩 **4 整行**、下面留 160px 空白(r1 复审 P2-1,r2 修回 7) |
///
/// 判据是**档位**不是高度:页面的模型里只有 `window_width`(`pages/model.rs`),窗口高度
/// 没有接进页面(要接得改 `pages/mod.rs` 的入口签名,超出本页的边界)。所以这里按「哪一档
@@ -153,9 +155,10 @@ impl TailShare {
/// 1. 档位是 `Chips`(多花两行 chip,竖排预算最紧的那一档);
/// 2. 面板真的**占高度**——`Options`(选中了版本、选项区展开)或 `Install`(进度 / 失败)。
///
/// `Hint`(刚进页面还没选版本)只是**一行**提示(≈30px),对半分会让 8 个版本只显示 4 个、
/// 下半块留 ≈160px 空画布(r1 复审 P2-1 实测:修前 700×760 下 8 行全在屏,只判档位的写法
/// 把它压到 4 行)。所以 `Hint` **在任何档位都钉死**。
/// `Hint`(刚进页面还没选版本)只是**一行**提示(≈30px),对半分会让列表从 **7 整行**
/// 掉到 **4 整行**、下半块留 ≈160px 空画布(r1 复审 P2-1 实测:main 在 700×760 的
/// `Hint` 态是 7 整行——不是 8,第 8 条墨迹其实是 `Pick a version, then install` 那句
/// hint,横向范围对不上版本号;r2 修回 7 整行)。所以 `Hint` **在任何档位都钉死**。
pub(super) const fn of(tier: VersionRailTier, state: &PanelState<'_>) -> Self {
match (tier, state) {
(VersionRailTier::Chips, PanelState::Options(_) | PanelState::Install(_)) => {
@@ -7,21 +7,21 @@
//! 那些只有图能回答——所以这里直接量 `pages_demo` 出的 PNG,按**产品自己的裁决函数**
//! ([`VersionRails::of_metrics`] + [`frame_metrics`])推出每一栏该在的 x 区间,再要求像素同意。
//!
//! ## 开关与跑法(与 `components/*/probe.rs` 同一族范式:没有图就 SKIP,不假绿)
//! ## 开关与跑法(`ULL_DL_*_SHOT` 喂图,四条出图命令见 `feed.rs` 的模块文档)
//!
//! ```text
//! # ① 1180 宽(Wide 档,两栏都展开)
//! ULL_DL_DEMO=options-fabric cargo run -p unlucky-app-iced --example pages_demo -- \
//! --screen=download --width=1180 --height=760 --screenshot=opt.png \
//! --screenshot-theme=light --software-renderer
//! ULL_DL_OPTIONS_SHOT=opt.png cargo test -p unlucky-app-iced --lib options_shot
//! ```
//! 四条断言都靠环境变量拿到一张真 PNG。M127 之前的写法是「没有图就 SKIP」,于是在 CI 里
//! **全是空跑**(`.github/` 里 grep 这些变量零命中)。现在按环境分档:
//!
//! - **只在 Windows runner 上跑**:断言量的行高/墨迹条数/药丸位置是按本仓字体栈
//! (`Segoe UI`)标定的,Linux/macOS 的回退字体会让这些数字变样 ⇒ 假红。这条限制同时写在
//! `.github/workflows/ci.yml` 的作业条件(`if: runner.os == 'Windows'`,出图与断言两步都带)
//! 和 `feed.rs` 的模块文档里,不靠「碰巧只配了一处」。
//! - **CI 里没喂图 = 失败**,提示「守门未喂图」:将来谁把 CI 里出图那步重构掉,构建会红,
//! 而不是静静退回手动档。本机(非 CI)才允许跳过,并且会留一句提示。
//!
//! 颜色**不**写死:`run_at` 只要求「同一段颜色连续」,栏与栏的边界按「颜色变了」判,
//! 于是浅色/深色都能跑同一条断言(实测浅色下 Card=(235,245,255)、Canvas=(246,250,255))。
use std::path::PathBuf;
use unlucky_design::color::SchemeFlavor;
use unlucky_design::typography::Platform;
@@ -48,15 +48,6 @@ fn ui() -> Ui {
Ui::with_platform(SchemeFlavor::Light, Platform::Windows, FAMILIES)
}
fn shot_path(key: &str) -> Option<PathBuf> {
let path = PathBuf::from(std::env::var_os(key)?);
if path.is_absolute() || path.exists() {
return Some(path);
}
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
Some(root.join(path))
}
/// 一张 RGB 图(只留本文件要用的那两件事:宽高 + 取点)。
struct Shot {
width: usize,
@@ -293,11 +284,7 @@ fn bands(width: f32) -> Bands {
/// 深色主题下的对比度(本条只跑浅色图;深色另用同一 argv 换 `--screenshot-theme` 出图人看)。
#[test]
fn an_options_shot_shows_both_rails_and_the_whole_panel() {
let Some(path) = shot_path(WIDE_SHOT) else {
eprintln!(
"[options-shot] SKIPPED:未设置 {WIDE_SHOT}。这是**渲染层**几何断言,\
需要先出图(见本文件模块文档的 1180 那张)。"
);
let Some(path) = feed(WIDE_SHOT) else {
return;
};
let shot = Shot::load(&path);
@@ -381,4 +368,8 @@ mod chips;
mod compact;
mod feed;
mod ready;
use feed::feed;
@@ -1,6 +1,6 @@
//! Chips 档(窗口 < 736、可用宽 < 600)的像素守门:M125 缺陷 1。
use super::{bands, shot_path, ui, Shot, CHIPS_SHOT};
use super::{bands, feed, ui, Shot, CHIPS_SHOT};
/// `02-…-chips700.png`(700×760,`ULL_DL_CHIPS_SHOT`)——**M125 缺陷 1 的回归守门**。
///
@@ -24,8 +24,7 @@ use super::{bands, shot_path, ui, Shot, CHIPS_SHOT};
/// chips 档的面板本来就是可滚的一半,实例名那一格确实落在折线以下。
#[test]
fn a_chips_tier_shot_keeps_a_whole_version_row_readable() {
let Some(path) = shot_path(CHIPS_SHOT) else {
eprintln!("[options-shot] SKIPPED:未设置 {CHIPS_SHOT}。");
let Some(path) = feed(CHIPS_SHOT) else {
return;
};
let shot = Shot::load(&path);
@@ -1,6 +1,6 @@
//! Compact 档(可用宽 600~675)主版本栏的像素守门:M125 缺陷 2。
use super::{bands, shot_path, ui, Shot, COMPACT_SHOT};
use super::{bands, feed, ui, Shot, COMPACT_SHOT};
/// `04-…-compact736.png`(736×760,`ULL_DL_COMPACT_SHOT`)——**Compact 档主版本栏(缺陷 2)**。
///
@@ -41,8 +41,7 @@ use super::{bands, shot_path, ui, Shot, COMPACT_SHOT};
/// `the_icon_rail_text_box_is_32_and_only_bucket_labels_fit`(「全部版本」= 64 > 32)。
#[test]
fn a_compact_tier_shot_draws_an_icon_for_the_first_major_entry() {
let Some(path) = shot_path(COMPACT_SHOT) else {
eprintln!("[options-shot] SKIPPED:未设置 {COMPACT_SHOT}。");
let Some(path) = feed(COMPACT_SHOT) else {
return;
};
let shot = Shot::load(&path);
@@ -0,0 +1,116 @@
//! 像素守门的「喂图」通道:`ULL_DL_*_SHOT` 环境变量 → PNG 路径,以及**没喂图时该有多响**。
//!
//! 四条像素断言(`chips` / `compact` / `ready` / 父模块的 wide)都从 [`feed`] 拿图。
//! 为什么单独一个文件:调用侧只有一句 `let Some(path) = feed(KEY) else { return; };`,
//! 但「没图怎么办」是同一条策略——抄四份必然跑偏。M127 之前就是抄成了一律 `SKIP`,
//! 于是 CI 里四条全在**空跑**(`.github/` 与 `packaging/` 里 grep 这些变量零命中,
//! 说明从来没喂过图);M127 把裁决收到这里,并让 CI 里缺图直接失败。
//!
//! ## 手动出图的四条命令(CI 的 `test` 作业跑的就是这四条)
//!
//! ```text
//! # ready700:**不设** ULL_DL_DEMO(= ready,还没选版本)
//! cargo run -p unlucky-app-iced --example pages_demo -- --screen=download \
//! --width=700 --height=760 --screenshot=ready700.png \
//! --screenshot-theme=light --software-renderer
//!
//! # chips700 / compact736 / wide1180:都要 ULL_DL_DEMO=options-fabric
//! ULL_DL_DEMO=options-fabric cargo run -p unlucky-app-iced --example pages_demo -- \
//! --screen=download --width=700 --height=760 --screenshot=chips700.png \
//! --screenshot-theme=light --software-renderer
//! ULL_DL_DEMO=options-fabric cargo run -p unlucky-app-iced --example pages_demo -- \
//! --screen=download --width=736 --height=760 --screenshot=compact736.png \
//! --screenshot-theme=light --software-renderer
//! ULL_DL_DEMO=options-fabric cargo run -p unlucky-app-iced --example pages_demo -- \
//! --screen=download --width=1180 --height=760 --screenshot=wide1180.png \
//! --screenshot-theme=light --software-renderer
//!
//! ULL_DL_READY_SHOT=ready700.png ULL_DL_CHIPS_SHOT=chips700.png \
//! ULL_DL_COMPACT_SHOT=compact736.png ULL_DL_OPTIONS_SHOT=wide1180.png \
//! cargo test -p unlucky-app-iced --lib options_shot
//! ```
//!
//! ## 只在 Windows runner 上可信
//!
//! 断言里有几条直接量**墨迹**:`ready` 数「读得出几个版本号」的墨迹条数、`chips` 量首行
//! 底色带与分隔线上下的高度、`compact` 量图标药丸的 40×40。这些数字是按父模块 `ui()` 里
//! 那套字体栈(`Segoe UI` / `Consolas`)标定的。Linux / macOS runner 上这套字体不存在,
//! 回退字体的字高与行距一变,行高、墨迹条数、药丸位置跟着变 ⇒ **假红**。
//!
//! 所以「像素断言只在 Windows runner 上跑」这条约束钉在**三处**,不靠「碰巧只配了一处」:
//!
//! 1. `.github/workflows/ci.yml` 的 `test` 作业:出图那步带 `if: runner.os == 'Windows'`;
//! 2. 同一作业的断言那步也带同一个条件,并把「为什么」写在那里;
//! 3. 本文件的 [`feed`]:非 Windows 上**喂了图**会当场失败(CI 里),不是静静地跑出假红。
//!
//! ## 没喂图时:CI 里失败,本机跳过
//!
//! | 环境 | 变量在 | 变量不在 |
//! |---|---|---|
//! | Windows + CI | 正常断言 | **失败**:「守门未喂图」 |
//! | Windows 本机 | 正常断言 | 跳过(一句提示) |
//! | 非 Windows + CI | **失败**:这条守门的标定前提不成立 | 跳过(本仓 CI 不在 Linux/macOS 上跑像素断言) |
//! | 非 Windows 本机 | 跑,但提示「数字可能不适用」 | 跳过(一句提示) |
//!
//! 目标是:将来谁把 CI 里出图那一步重构掉,构建会**红**,而不是静静退回手动档。
use std::path::PathBuf;
/// CI 环境(GitHub Actions 在所有 runner 上都设这个)。
fn in_ci() -> bool {
std::env::var_os("CI").is_some_and(|value| !value.is_empty())
}
/// `key` 指的截图路径;`None` = 没喂图,调用侧 `return`(跳过与失败的裁决已经在这里做完了)。
///
/// 空串当作没设:GitHub 的条件 env 很容易求出 `''`,而 `PathBuf::from("")` 会被后面那条
/// 相对路径分支解析成**工作区根目录**,读它只会得到一句莫名其妙的「读截图」失败。
pub(super) fn feed(key: &str) -> Option<PathBuf> {
let Some(value) = std::env::var_os(key).filter(|value| !value.is_empty()) else {
not_fed(key);
return None;
};
let path = PathBuf::from(value);
if !cfg!(windows) {
// 非 Windows 上喂图:本机随意(只是数字可能不适用),CI 里说明标定前提被破坏了
assert!(
!in_ci(),
"[options-shot] 在非 Windows runner 上喂了 {key}:这几条断言按 Windows 字体栈\
(Segoe UI)标定,Linux/macOS 上跑出来的绿/红都不可信。\
像素守门只在 windows-latest 上跑,见 .github/workflows/ci.yml 的 test 作业。"
);
eprintln!(
"[options-shot] 注意:{key} 喂在非 Windows 上,字体栈与标定时不同,\
数字可能对不上(这不是产品问题)。"
);
}
if path.is_absolute() || path.exists() {
return Some(path);
}
// `cargo test` 的 cwd 是 crate 目录,相对路径按**工作区根**解释
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
Some(root.join(path))
}
/// 没喂图时的统一裁决:CI(Windows)里**失败**,本机跳过并留一句提示。
///
/// 由 [`feed`] 在返回 `None` 之前调用——「跳过」这件事必须是响的,不能 `return` 了事。
fn not_fed(key: &str) {
let message = format!(
"[options-shot] 守门未喂图:环境变量 {key} 没有设置。这条是**渲染层**像素断言,\
需要先用 pages_demo 现出真 PNG(四条命令见 tests/options_shot/feed.rs 的模块文档)。"
);
// 只在 Windows + CI 上硬失败:非 Windows 上本来就不该喂图(见模块文档的表),
// 那边缺图是**预期**,硬失败会误伤 Linux 那条矩阵。
assert!(
!(cfg!(windows) && in_ci()),
"{message}\n\
当前是 CI 环境(CI 已设置)却拿到空路径 ⇒ CI 里出图那一步被重构掉了 / 条件写错了,\
构建必须红:空跑的守门等于没有守门。\
修法:.github/workflows/ci.yml 的 test 作业里出图与断言两步都要带这条——\
`if: runner.os == 'Windows'`,且出图那步要把四个 ULL_DL_*_SHOT 写进 $GITHUB_ENV。"
);
eprintln!("{message} SKIPPED(本机,非 CI)。");
}
@@ -4,7 +4,9 @@
//!
//! M125 r1 的 `TailShare::of` **只判档位**,于是 chips 档的「刚进页面」也被五五分:
//! 面板里只有一行提示(`PanelState::Hint`,≈30px),却拿走一半高度 ⇒ 700×760 下
//! 8 个版本只显示 **4** 个、下半块留 ≈160px 空画布(修前是 8 行全在屏)。
//! 只剩 **4 整行**可读、下半块留 ≈160px 空画布(main 那一态是 **7 整行**:不是 8,
//! 第 8 条墨迹其实是 `Pick a version, then install` 那句 hint,横向范围对不上版本号,
//! 而且 700×760 上放 8 行要 352px、实测只剩 309~322px。r2 修回 7 整行)。
//! 同目录 `chips.rs` 只拍「选中 + 选项区展开」,正是这个盲区把回归放了过去。
//!
//! 现在的判据:`Chips` **且**面板真占高度(`Options` / `Install`)才共享;
@@ -23,7 +25,7 @@
//! 抓不住:提示文案本身、hover/tooltip 层(静态图拍不到);也不管「选中且展开」那一态
//!(那是 `chips.rs` 的活)。
use super::{bands, shot_path, ui, Shot, READY_SHOT};
use super::{bands, feed, ui, Shot, READY_SHOT};
/// `ready` 那一态(700×760):8 个版本里至少 6 行要在屏,尾部提示不许把列表压扁。
///
@@ -32,8 +34,7 @@ use super::{bands, shot_path, ui, Shot, READY_SHOT};
/// 其余是空白 —— 这条断言在那种图上是红的(4 < 6)。
#[test]
fn a_ready_shot_keeps_the_whole_visible_list() {
let Some(path) = shot_path(READY_SHOT) else {
eprintln!("[options-shot] SKIPPED:未设置 {READY_SHOT}。");
let Some(path) = feed(READY_SHOT) else {
return;
};
let shot = Shot::load(&path);
@@ -150,8 +150,9 @@ fn the_demo_data_is_sane() {
///
/// 修前(M123):面板一律按内容高钉在底部,700×760 的 chips 档里列表只剩 18px(0.4 行)。
/// M125 r1 修法只判档位,又踩出另一条回归:chips 档「刚进页面还没选版本」(`Hint`,面板只有
/// 一行提示)也被五五分,700×760 下 8 个版本只显示 4 个、下半块留 ≈160px 空画布
/// (复审 P2-1 实测,修前是 8 行全在屏)。
/// 一行提示)也被五五分,700×760 下只显示 **4 整行**、下半块留 ≈160px 空画布
/// (复审 P2-1 实测;main 那一态是 **7 整行**——不是 8:第 8 条墨迹其实是
/// `Pick a version, then install` 那句 hint,横向范围对不上版本号。r2 修回 7 整行)。
///
/// 现在的判据:`Chips` **且**面板真占高度(`Options` / `Install`)才 `Shared`;
/// `Hint` 在任何档位都 `Pinned`。