原来只查 hover(8%)那一色,于是「把 pressed 列的状态层画成方块」照样绿——实测变异 mut-one-layer-box 当时是 passed。现在 hover(8%) 与 pressed/focus(10%) 两种颜色各查一遍, 每一块的四个角都不许有墨。 变异证据(六个变异全部转红,见报告): - mut-one-box-22 → 18.3:Primary 块不得超过 21,实测 22×22 @ (541,222) - mut-one-layer-box → 18.5:Pressed 状态层必须是圆(四角不该有墨)@ (532,212) - mut-chip-34 → 13.1:filter chip 应恰好四块,实测 3
327 lines
13 KiB
Rust
327 lines
13 KiB
Rust
//! 选择类家族的**渲染层**几何断言:对着真截图量像素(M68 的教训:纯函数对、图可能是错的)。
|
||
//!
|
||
//! 为什么住在 `selection/` 而不是 `gallery/`:`gallery/selection.rs` 只负责「把矩阵摆进 harness」,
|
||
//! 断言加上去会把它顶到 400 行红线。像素扫描的 4 个纯函数在 [`super::pixel`](同一目录、`cfg(test)`)。
|
||
//!
|
||
//! 量什么(每条对一个具体错误,容差 ±1):
|
||
//!
|
||
//! - **Checkbox 外缘 20,且选中/未选中同尺寸**(§18.3:描边向内收;向外画会变 22)——逐块断言;
|
||
//! - **Radio 内点 10 不是 12**(§19.2)· 环是真圆、不是圆角方(§19.6);
|
||
//! - **状态层直径 40 且四角无墨**(§18.5 / §19.7)——**每一块**都查;
|
||
//! - 勾形墨迹 ≈14.8(端点 12 + 方头外扩)· **indeterminate 是短横**(§18.4/§18.6);
|
||
//! - Switch 轨道**恰好一块** 52×32(§10.1)· Chip **恰好一块** 32 高(§13.1);
|
||
//! - 禁用:chip 只降不删(P2-1)· 选中禁用的 Checkbox 容器是 38% 不是 12%(P2-3)。
|
||
//!
|
||
//! 复现(worktree 根目录):
|
||
//!
|
||
//! ```text
|
||
//! cargo build -p unlucky-app-iced --bin unlucky-launcher-iced
|
||
//! ULL_GALLERY_SECTION=selection ./target/debug/unlucky-launcher-iced \
|
||
//! --software-renderer --screen=gallery \
|
||
//! --screenshot target/m76/selection-light.png --screenshot-theme=light
|
||
//! ULL_GALLERY_SHOT=target/m76/selection-light.png cargo test -p unlucky-app-iced selection::probe
|
||
//! ```
|
||
|
||
use unlucky_design::color::{Role, SchemeFlavor};
|
||
use unlucky_design::typography::Platform;
|
||
|
||
use super::super::interaction::{self, Interaction};
|
||
use super::pixel::{center, dims, ink_boxes, inner_ink, matches, near, rgb, shot_path, Shot};
|
||
use crate::theme::{Families, Ui};
|
||
|
||
const FAMILIES: Families = Families {
|
||
text: "Segoe UI",
|
||
display: "Segoe UI",
|
||
mono: "Consolas",
|
||
};
|
||
|
||
fn ui(flavor: SchemeFlavor) -> Ui {
|
||
Ui::with_platform(flavor, Platform::Windows, FAMILIES)
|
||
}
|
||
|
||
/// 一条测试覆盖全部几何断言(浅/深各跑一次)。
|
||
#[test]
|
||
fn selection_geometry_matches_the_reference() {
|
||
let Some(path) = shot_path() else {
|
||
eprintln!(
|
||
"[selection-probe] SKIPPED:未设置 ULL_GALLERY_SHOT。这是**渲染层**几何断言,\
|
||
需要先出图(见本文件模块文档,注意 ULL_GALLERY_SECTION=selection)。"
|
||
);
|
||
return;
|
||
};
|
||
let shot = Shot::load(&path).expect("读 selection 截图");
|
||
assert_eq!(
|
||
(shot.width, shot.height),
|
||
(1180, 760),
|
||
"验收图必须 1180×760 @100% DPI"
|
||
);
|
||
let light = matches(
|
||
shot.at(2, 2),
|
||
rgb(ui(SchemeFlavor::Light).color(Role::Surface)),
|
||
);
|
||
let flavor = if light {
|
||
SchemeFlavor::Light
|
||
} else {
|
||
SchemeFlavor::Dark
|
||
};
|
||
let ui = ui(flavor);
|
||
let surface = ui.color(Role::Surface);
|
||
let primary = ui.color(Role::Primary);
|
||
let on_primary = ui.color(Role::OnPrimary);
|
||
let unselected = ui.color(Role::OnSurfaceVariant);
|
||
|
||
// ---- §18.1 / §18.3 / §19.1:20 的外缘,选中与未选中**同尺寸** -----------------
|
||
// 逐块断言(复审 P2-4):**每一块** Primary 都必须是 20(±1)——任何一块到 22 就说明描边
|
||
// 画到了框外。只数「20×20 有几块」的时候,「把其中一格放大成 22」照样绿。
|
||
let filled = ink_boxes(&shot, primary, surface, 15, 24);
|
||
for bbox in &filled {
|
||
assert!(
|
||
bbox.width() <= 21 && bbox.height() <= 21,
|
||
"18.3:Primary 块不得超过 21(22 = 描边画到框外),实测 {}×{} @ {:?}",
|
||
bbox.width(),
|
||
bbox.height(),
|
||
(bbox.left, bbox.top)
|
||
);
|
||
}
|
||
let twenty: Vec<_> = filled
|
||
.iter()
|
||
.copied()
|
||
.filter(|bbox| near(bbox.width(), 20) && near(bbox.height(), 20))
|
||
.collect();
|
||
assert_eq!(
|
||
twenty.len(),
|
||
12,
|
||
"18.1:应**恰好** 12 块 20(±1) 的 Primary(checkbox 选中 4 + 部分选中 4 + radio 环 4),\
|
||
实际 {}:{}",
|
||
twenty.len(),
|
||
dims(&filled)
|
||
);
|
||
|
||
// 未选中(OnSurfaceVariant 描边):逐块都必须 20(±1),且内部透底
|
||
let candidates = ink_boxes(&shot, unselected, surface, 17, 24);
|
||
let rings: Vec<_> = candidates
|
||
.iter()
|
||
.copied()
|
||
.filter(|bbox| !matches(shot.at(center(bbox).0, bbox.top + 3), rgb(unselected)))
|
||
.collect();
|
||
assert!(
|
||
!rings.is_empty(),
|
||
"18.2:没找到未选中的复选框;候选:{}",
|
||
dims(&candidates)
|
||
);
|
||
for bbox in &rings {
|
||
assert!(
|
||
near(bbox.width(), 20) && near(bbox.height(), 20),
|
||
"18.3:未选中外缘必须与选中同尺寸 20(描边向内收),实测 {}×{} @ {:?}",
|
||
bbox.width(),
|
||
bbox.height(),
|
||
(bbox.left, bbox.top)
|
||
);
|
||
assert!(
|
||
!matches(shot.at(center(bbox).0, center(bbox).1), rgb(unselected)),
|
||
"18.2:未选中框内部必须透出底色"
|
||
);
|
||
}
|
||
// A1:hover/pressed/focused 三列的未选中描边提到 `OnSurface`
|
||
let on_surface = ui.color(Role::OnSurface);
|
||
let lifted: Vec<_> = ink_boxes(&shot, on_surface, surface, 17, 24)
|
||
.into_iter()
|
||
.filter(|bbox| !matches(shot.at(center(bbox).0, bbox.top + 3), rgb(on_surface)))
|
||
.collect();
|
||
assert!(
|
||
lifted.len() >= 3,
|
||
"A1:未选中复选框在 hover/pressed/focused 的描边应是 OnSurface(3 个),实际 {}",
|
||
lifted.len()
|
||
);
|
||
for bbox in &lifted {
|
||
assert!(
|
||
near(bbox.width(), 20) && near(bbox.height(), 20),
|
||
"18.3:提到 OnSurface 之后外缘仍是 20,实测 {}×{}",
|
||
bbox.width(),
|
||
bbox.height()
|
||
);
|
||
}
|
||
|
||
// ---- §19.2:Radio 内点 **10 不是 12**;§19.6:环是真圆 ------------------------
|
||
// 环/实心看**上沿内缩 3px 的点**(环在那里是内孔);不看正中心——短横正好压中心。
|
||
let mut dots = 0;
|
||
for bbox in &twenty {
|
||
if matches(shot.at(center(bbox).0, bbox.top + 3), rgb(primary)) {
|
||
continue;
|
||
}
|
||
assert!(
|
||
!matches(shot.at(bbox.left + 1, bbox.top + 1), rgb(primary)),
|
||
"19.6:Radio 外圈必须是真圆(不是圆角方)@ {:?}",
|
||
(bbox.left, bbox.top)
|
||
);
|
||
let dot = inner_ink(&shot, bbox, primary, surface, 4).expect("选中的 radio 必须有内点");
|
||
assert!(
|
||
near(dot.width(), 10) && near(dot.height(), 10),
|
||
"19.2:Radio 内点可视直径 10(12 是含描边前的名义值),实测 {}×{}",
|
||
dot.width(),
|
||
dot.height()
|
||
);
|
||
assert_ne!(dot.width(), 12, "19.2:照名义值抄 12 会明显偏大");
|
||
dots += 1;
|
||
}
|
||
assert_eq!(
|
||
dots, 4,
|
||
"19.2:应恰好 4 个选中的 radio(四态),实际 {dots}"
|
||
);
|
||
|
||
// ---- §18.4 / §18.6:勾形墨迹跨度;indeterminate 是**短横**不是勾 --------------
|
||
let mut bars = 0;
|
||
let mut checks = 0;
|
||
for bbox in &twenty {
|
||
let Some(glyph) = inner_ink(&shot, bbox, on_primary, primary, 3) else {
|
||
continue;
|
||
};
|
||
if glyph.height() <= 4 {
|
||
bars += 1;
|
||
assert!(
|
||
glyph.width() >= 12,
|
||
"18.6:indeterminate 必须是居中短横,实测 {}×{}",
|
||
glyph.width(),
|
||
glyph.height()
|
||
);
|
||
} else {
|
||
checks += 1;
|
||
// 端点 12 + 方头两侧各 2/√2 ≈ 1.41 → 墨迹 ≈14.8(见 `ControlGeometry`)
|
||
assert!(
|
||
(13..=16).contains(&glyph.width()),
|
||
"18.4:勾形墨迹横向应 ≈14.8(端点 12 + 方头外扩),实测 {}",
|
||
glyph.width()
|
||
);
|
||
assert!(
|
||
glyph.height() >= 6,
|
||
"18.4:勾形有折点,实测高 {}",
|
||
glyph.height()
|
||
);
|
||
}
|
||
}
|
||
assert!(
|
||
bars >= 4 && checks >= 4,
|
||
"18.4/18.6:勾 {checks} 个、短横 {bars} 个(各应 ≥4)"
|
||
);
|
||
|
||
// ---- §18.5 / §19.7:状态层直径 40,且**每一块**都是圆(四角无墨)----------------
|
||
// hover 是 8%、pressed/focus 是 10%:**两种颜色都要查**。只查 8% 会漏掉「按下那一列被画成
|
||
// 方块」——代码 reviewer 的第二次变异正好落在这个盲点上(实测它当时是绿的)。
|
||
let mut circles = 0;
|
||
for state in [Interaction::Hovered, Interaction::Pressed] {
|
||
let layer = interaction::overlay_color(&ui, state, Role::OnSurfaceVariant, Role::Surface)
|
||
.expect("可交互态有状态层");
|
||
let found: Vec<_> = ink_boxes(&shot, layer, surface, 37, 43)
|
||
.into_iter()
|
||
.filter(|bbox| near(bbox.width(), 40) && near(bbox.height(), 40))
|
||
.collect();
|
||
assert!(
|
||
found.len() >= 2,
|
||
"18.5:{state:?} 的 Ø40 状态层应 ≥2 块(checkbox×3 + radio + switch),实测 {}:{}",
|
||
found.len(),
|
||
dims(&found)
|
||
);
|
||
for circle in &found {
|
||
for (x, y) in [
|
||
(circle.left, circle.top),
|
||
(circle.right, circle.top),
|
||
(circle.left, circle.bottom),
|
||
(circle.right, circle.bottom),
|
||
] {
|
||
assert!(
|
||
!matches(shot.at(x, y), rgb(layer)),
|
||
"18.5:{state:?} 状态层必须是圆(外接框四角不该有墨)@ ({x},{y})"
|
||
);
|
||
}
|
||
}
|
||
circles += found.len();
|
||
}
|
||
|
||
// ---- §10.1 Switch 轨道 52×32 · §13.1 Chip 高 32 -------------------------------
|
||
let on_tracks: Vec<_> = ink_boxes(&shot, primary, surface, 28, 57)
|
||
.into_iter()
|
||
.filter(|bbox| near(bbox.width(), 52) && near(bbox.height(), 32))
|
||
.collect();
|
||
assert_eq!(
|
||
on_tracks.len(),
|
||
1,
|
||
"10.1:选中的 switch 轨道应**恰好一块** 52×32,实测 {}:{}",
|
||
on_tracks.len(),
|
||
dims(&on_tracks)
|
||
);
|
||
let chips: Vec<_> = ink_boxes(&shot, ui.color(Role::SecondaryContainer), surface, 20, 140)
|
||
.into_iter()
|
||
.filter(|bbox| near(bbox.height(), 32))
|
||
.collect();
|
||
// 选中的 chip 在**四态**里容器都是 `SecondaryContainer`(状态层只叠在未选中的透明底上),
|
||
// 所以这里应恰好 4 块——多于 4 说明别的组件的容器也是这个角色、且高 32。
|
||
assert_eq!(
|
||
chips.len(),
|
||
4,
|
||
"13.1:选中的 filter chip 应**恰好四块**(enabled/hover/pressed/focused 各一,32 高),\
|
||
实测 {}:{}",
|
||
chips.len(),
|
||
dims(&chips)
|
||
);
|
||
assert!(
|
||
chips[0].width() >= 58,
|
||
"13.1:chip 宽应 ≥58(图标 16 + 两侧各 8 + 文字),实测 {}",
|
||
chips[0].width()
|
||
);
|
||
|
||
// ---- 复审 P2-1:禁用的 chip **只降不删**(选中态与图标都要在)------------------
|
||
// 判据:禁用列里必须**恰好有一块** 12% 填充的 chip(= filter·on)。旧 API 把 selected
|
||
// 一起删掉,这一块根本不存在(实测 0 块);能查到它、且宽度 ≥58,说明选中态与图标都还在。
|
||
// (未选中的那三个禁用 chip 只有 1px 描边,落在半像素上时覆盖率不足 50%,量不稳,故不在此断言。)
|
||
let disabled_container = interaction::disabled_colors(&ui, Role::Surface).0;
|
||
let disabled_chips: Vec<_> = ink_boxes(&shot, disabled_container, surface, 28, 140)
|
||
.into_iter()
|
||
.filter(|bbox| near(bbox.height(), 32) && bbox.width() >= 58)
|
||
.collect();
|
||
assert_eq!(
|
||
disabled_chips.len(),
|
||
1,
|
||
"P2-1:禁用列应**恰好一块**填充 chip(filter·on 的选中态不能被删掉),实测 {}:{}",
|
||
disabled_chips.len(),
|
||
dims(&disabled_chips)
|
||
);
|
||
assert!(
|
||
disabled_chips[0].width() >= 58,
|
||
"P2-1:禁用 chip 宽度应 ≥58(图标还在),实测 {}",
|
||
disabled_chips[0].width()
|
||
);
|
||
|
||
// ---- 复审 P2-3:**选中且禁用**的 Checkbox 容器是 38%,不是 12% -----------------
|
||
let disabled_ink = interaction::disabled_content_over_role(&ui, Role::OnSurface, false);
|
||
let ink38: Vec<_> = ink_boxes(&shot, disabled_ink, surface, 17, 24)
|
||
.into_iter()
|
||
.filter(|bbox| near(bbox.width(), 20) && near(bbox.height(), 20))
|
||
.collect();
|
||
assert!(
|
||
ink38.len() >= 3,
|
||
"18.8:禁用列应有 ≥3 块 20(±1) 的 38% 叠色(选中禁用填充 + 未选中禁用描边 + radio 环),\
|
||
实测 {}:{}",
|
||
ink38.len(),
|
||
dims(&ink38)
|
||
);
|
||
let container_20: Vec<_> = ink_boxes(&shot, disabled_container, surface, 19, 21)
|
||
.into_iter()
|
||
.filter(|bbox| near(bbox.width(), 20) && near(bbox.height(), 20))
|
||
.collect();
|
||
assert!(
|
||
container_20.is_empty(),
|
||
"P2-3:20×20 的禁用块不许是 12% 容器色(那是普通禁用容器,选中禁用是 38%),实测 {}",
|
||
dims(&container_20)
|
||
);
|
||
|
||
eprintln!(
|
||
"[selection-probe] OK flavor={flavor:?}:20(±1) Primary 块 {} · 未选中环 {} · 内点 {dots} · \
|
||
勾 {checks} / 短横 {bars} · 状态层 {} · 禁用 38% {} · 禁用 chip {}",
|
||
twenty.len(),
|
||
rings.len(),
|
||
circles,
|
||
ink38.len(),
|
||
disabled_chips.len()
|
||
);
|
||
}
|