fix(app-iced): M75 复审 P1-A/P1-B/P2 —— 内容色跟态走 + 断言收紧 + 矩阵排版

P1-A 禁用内容色没下发(视觉 reviewer:text/standard 的 enabled 与 disabled 逐像素相同):
  assemble 里内容色恒取 ui.color(content) 再被 text().color() 钉死,Style.text_color 的 38%
  永远下发不出去。新增 assemble::content_color(ui, forced, content),文字按钮与 IconButton
  共用;probe 补两条像素断言(禁用 38% 墨迹必须存在、禁用容器内不得有满档 OnPrimary)。

P1-B 渲染层断言抓不住 2px 级回归(reviewer 变异验证:高度 42 仍绿):
  高度改成精确 == 40;补「带图标左内边距 16(墨迹 ≈21)」断言;分隔线从「行数」改成
  「墨量覆盖率 ≈1.0」(1.5dp 会到 ≈1.5)。

P2-1 probe 给了图却读不到时硬失败(只有完全没给 ULL_GALLERY_SHOT 才跳过)。
P2-2 Geometry::stroke 真正接到 Border.width;IconGeometry::icon 改由 side−2×padding 推导。
P2-3 矩阵 cell 宽 192→160(总宽 1040<1180,disabled 列不再被裁);禁用格保留前后图标。
P2-4 补注:snap 仅 wgpu 实现,CPU 验收图不受它影响。
This commit is contained in:
AskaEth committed 2026-11-02 04:50:51 +08:00
1 parent 8072f1cc7e
commit e905fa3922
8 files changed
+195 -68

No files matched your search

+111 -6
View File
@@ -35,6 +35,50 @@ fn ui(flavor: SchemeFlavor) -> Ui {
}
/// `ULL_GALLERY_SHOT` → 截图路径(相对路径按**工作区根**解释:`cargo test` 的 cwd 是 crate 目录)。
/// 一条 1dp 细线在竖直方向的「墨量覆盖率」:各行相对底色的不透明度之和(1dp ⇒ ≈1.0)。
fn line_coverage(shot: &Shot, surface: [u8; 3], line: [u8; 3]) -> f32 {
let mut total = 0.0;
for y in 0..shot.height {
let mut sum = 0.0f32;
let mut count = 0usize;
for x in 0..shot.width {
let pixel = shot.at(x, y);
if !matches(pixel, surface) {
count += 1;
sum += delta_ratio(pixel, surface, line);
}
}
if count > 300 {
total += sum / count as f32;
}
}
total
}
/// `pixel` 相对 `surface` 的偏移占「满档 line 色偏移」的比例(0..=1)。
fn delta_ratio(pixel: [u8; 3], surface: [u8; 3], line: [u8; 3]) -> f32 {
let channel = |i: usize| {
let full = f32::from(line[i]) - f32::from(surface[i]);
if full.abs() < 1.0 {
return 0.0;
}
((f32::from(pixel[i]) - f32::from(surface[i])) / full).clamp(0.0, 1.0)
};
let (dr, dg, db) = (channel(0), channel(1), channel(2));
(dr + dg + db) / 3.0
}
/// 图上某个精确色的像素数(带容差)。
fn count_pixels(shot: &Shot, color: [u8; 3]) -> usize {
(0..shot.height)
.map(|y| {
(0..shot.width)
.filter(|x| matches(shot.at(*x, y), color))
.count()
})
.sum()
}
fn shot_path() -> Option<std::path::PathBuf> {
let raw = std::env::var_os("ULL_GALLERY_SHOT")?;
let path = std::path::PathBuf::from(raw);
@@ -138,7 +182,33 @@ fn gallery_geometry_matches_the_tokens() {
"药丸圆角应 = 20(12b 1.2):顶部行左内缩应 ≈14,实测 {top_inset}(8 圆角 ≈4,直角 0)"
);
// ---- Q9:IconButton 的字形不许撑满 40 盒 --- // ---- Q9:IconButton 的字形不许撑满 40 盒 -----------------------------------
// ---- 12b 1.4 / 1.6:内边距 16、图标↔文字 8(用带前置图标的 Filled 格量)----
let ink = shot.ink_inside(&label_button, [255, 255, 255]);
if let Some(ink) = ink {
let icon_pad = ink.left - label_button.left;
// 图标盒左缘 = 16;字形在 24 viewBox 里自带 ≈5.3 的左边距 ⇒ 墨迹落在 21 附近
assert!(
(19..=23).contains(&icon_pad),
"12b 1.4:带图标左内边距应 16(墨迹 ≈21),实测 {icon_pad}"
);
} else {
panic!("Filled 格应带前置图标(量内边距用)");
}
// ---- Q9:IconButton 的字形不许撑满 40 盒 --- // ---- 12b 1.4 / 1.6:内边距 16、图标↔文字 8(用带前置图标的 Filled 格量)----
let ink = shot.ink_inside(&label_button, [255, 255, 255]);
if let Some(ink) = ink {
let icon_pad = ink.left - label_button.left;
// 图标盒左缘 = 16;字形在 24 viewBox 里自带 ≈5.3 的左边距 ⇒ 墨迹落在 21 附近
assert!(
(19..=23).contains(&icon_pad),
"12b 1.4:带图标左内边距应 16(墨迹 ≈21),实测 {icon_pad}"
);
} else {
panic!("Filled 格应带前置图标(量内边距用)");
}
// ---- Q9:IconButton 的字形不许撑满 40 盒 -----------------------------------
let tonal = rgb(ui.color(Role::SecondaryContainer));
let tonal_boxes = shot.bbox_of(tonal);
let icon_states = tonal_boxes
@@ -191,10 +261,18 @@ fn gallery_geometry_matches_the_tokens() {
runs += 1;
}
}
// 两条分隔线 × 各摊在 1~2 行上(抗锯齿)⇒ 2..=6 行;>6 说明线粗了或画了第三条
// 两条线 × 各摊 1~2 行 ⇒ 2..=4 行
assert!(
(2..=6).contains(&runs),
"分隔线应 2 条 × 1px(抗锯齿时各占 2 行),实测 {runs} 行"
(2..=4).contains(&runs),
"分隔线应 2 条(每条 1px,抗锯齿最多摊 2 行)⇒ 2..=4 行,实测 {runs} 行"
);
// **墨量覆盖率**:一条 1dp 的线,其各行相对底色的不透明覆盖率之和应 ≈1.0。
// 1.5dp 的线会到 ≈1.5,2dp 会到 ≈2.0 —— 这是「容忍 1.5 倍粗」的补丁。
let full = rgb(ui.color(Role::OutlineVariant));
let coverage = line_coverage(&shot, surface, full);
assert!(
(0.8..=1.3).contains(&coverage),
"分隔线墨量覆盖率应 ≈1.0(= 1dp),实测 {coverage:.2}(1.5dp 会到 ~1.5)"
);
// ---- 12b 1.8:禁用格必须与静息格不同 ------------------------------------
@@ -207,20 +285,47 @@ fn gallery_geometry_matches_the_tokens() {
"没找到禁用态实心按钮(12b 1.8:容器 = on-surface@12%)"
);
// ---- 12b 1.8(视觉 reviewer 的 P1-A):禁用态的内容色必须真的下发 ----
let (disabled_container, disabled_ink) =
crate::app::components::interaction::disabled_colors(&ui, Role::Surface);
let ink_px = count_pixels(&shot, rgb(disabled_ink));
assert!(
ink_px > 200,
"禁用内容色(on-surface@38%)在图上只出现 {ink_px} 像素: 说明 text / icon 的字形没走 38%(旧实现 enabled 与 disabled 逐像素相同)"
);
// filled 禁用容器里不许出现满档 OnPrimary(旧实现是 #FFFFFF 压在 12% 容器上 = 1.23:1)
let full_content = rgb(ui.color(Role::OnPrimary));
for bbox in shot.bbox_of(rgb(disabled_container)) {
if bbox.width() < 60 {
continue;
}
for y in (bbox.top + 2)..=(bbox.bottom - 2) {
for x in (bbox.left + 2)..=(bbox.right - 2) {
assert!(
!matches(shot.at(x, y), full_content),
"禁用容器内出现满档 OnPrimary @({x},{y}):禁用标签没降到 38%"
);
}
}
}
eprintln!(
"[probe] OK flavor={:?}:
Filled 药丸 {}×{}(参考 高 40 / 宽 ≥58)
顶行左内缩 {}(r=20 应 ≈14;8 圆角 ≈4;直角 0)
实心块 {} 个(含 40×40 IconButton)
分隔线长行 {} 行(每条 1px)
禁用容器 (on-surface@12%) 命中
禁用容器 (on-surface@12%) 命中 · 禁用内容 38% 墨迹 {} px
分隔线墨量覆盖率 {:.2}(1dp 应 ≈1.0)
字形墨迹 {}px(24 盒应 16~26)",
flavor,
label_button.width(),
true_height,
top_inset,
primary.len(),
runs,
ink_px,
coverage,
glyph_width
);
}