fix(app-iced): M93 r3 —— 复审 r1 四条返工(Canvas 不吃 α / 平铺补缝 / 诊断上移 / 文档)

先 `git rebase main`(M92 `eae7ae0` 已在主线;`Cargo.lock` 与 `crates/app-iced/Cargo.toml`
自动合并,`image-without-codecs` 与 M92 的 `unlucky-i18n` 两段都在)。

P2-2(口径违约)Canvas 层不许吃面板 α
- `Ui::layer()` 改成按令牌 `MaterialLayer::allows_translucency()` 判定:Canvas 恒不透明,
  只有 Card / Overlay 乘 `panel_alpha`。判据取令牌的不变量,三个调用点(标题栏 / 导航轨 /
  页头带)没有各自特判。
- 守门:`the_canvas_layer_stays_opaque_while_panels_take_the_alpha`(α=0.55 时 Canvas.a==1.0、
  Card/Overlay.a==0.55;CPU 档连卡片都是 1.0)。

P2-1 平铺不许留缝
- `background::tiles()` 超 `MAX_TILES` 时改成**放大图块步长**(`Tiles { cols, rows, scale }`,
  按 1.15 逐级放大到块数进预算),而不是减列/减行;`MAX_TILE_SCALE` 提到 1024 只是安全阀
  (1×1 的图在 1180×760 上需要 ~118 倍才能用 64 块盖满)。
- `picture::tiled()` 用 `ContentFit::Fill` + 格子尺寸 = `源尺寸 × scale`(同比例,不变形):
  `None` 会按图片原生尺寸画、比格子小就留空,正是那道缝。
- 守门:`tile_grid_always_covers_the_viewport_within_budget`(6 组尺寸,含 120×120 与 1×1,
  断言 `covered >= viewport` 且 `count <= MAX_TILES`);背景层底下本来就有不透明画布底色兜底。

P2-3 诊断上移到首屏
- 诊断块(读盘失败红字 / CPU 降级 / 配色回落 / 当前配色层级)从卡片末尾移到「背景图路径」
  紧下方:1180×760 首屏可见(截图 `r4-broken-path.png` 里红字在 y≈594)。

P2-4 文档对齐
- `transparency.rs`:`WS_EX_LAYERED` 的归属改成「本文件的 `ensure_layered` 负责」,
  窗口创建侧只拿句柄;`platform/mod.rs` / `lib.rs` 的模块表同步。
- 删掉「马赛克取样」这类已删实现的现在时描述(`background` / `picture` / `app` 的注释),
  保留一句 r1→r2 的历史说明;`app/tests/appearance.rs` 的测试意图跟着改。

门禁(工作区级,最终树):`cargo fmt --all --check` 干净;
`cargo clippy --workspace --all-targets -- -D warnings` 无输出;
`cargo test --workspace` 全绿(app-iced 299 lib + 6 集成守门、presenter 126、其余 crate 全过)。
出图:`r4-canvas-opaque-panel55{,-dark}.png`(Canvas 不透明 + 面板透底)、
`r4-tile-120.png`(120×120 平铺右缘无裸白)、`r4-broken-path.png`(诊断首屏可见)。
This commit is contained in:
AskaEth committed 2026-11-02 18:18:37 +08:00
1 parent 22e4c423fa
commit 5c40c64b5f
13 files changed
+181 -85

No files matched your search

@@ -97,21 +97,44 @@ fn dim_is_clamped_to_the_supported_range() {
assert_eq!(BG_DIM_MAX, 0.95, "与 core 同源(presenter 重导出)");
}
/// **P2-1 的守门**:任何尺寸/视口组合下,平铺网格铺出来的覆盖宽高都必须 ≥ 视口(不许留缝),
/// 同时块数不超预算(不留缝的代价只能是「放大图块」,不能是「少铺」)。
///
/// 抓得住:谁把 `tiles()` 改回「超预算就减列」——120×120 @ 1180×760 那条会立刻红
/// (r1 实测右缘裸白 100px)。
/// 抓不住:渲染器把单块图铺进格子时的实际像素对齐(有没有半像素缝由截图看)。
#[test]
fn tile_geometry_counts_enough_tiles_and_stays_within_budget() {
// 抓得住:①图比视口小时按原尺寸铺够(ceil);②图比视口大时是 1×1(= 原尺寸裁切);
// ③总块数永远 ≤ MAX_TILES(小图不会拼出上万个控件)。
// 抓不住:铺出来的缝(渲染层的事,看截图)。
let small = tiles((200, 200), (640.0, 480.0));
assert_eq!((small.cols, small.rows), (4, 3));
assert_eq!(small.count(), 12);
fn tile_grid_always_covers_the_viewport_within_budget() {
let cases = [
((120, 120), (1180.0, 760.0)),
((300, 200), (1180.0, 760.0)),
((16, 16), (1920.0, 1080.0)),
((8, 8), (3840.0, 2160.0)),
((1600, 900), (1180.0, 760.0)),
((1, 1), (1180.0, 760.0)),
];
for (source, viewport) in cases {
let grid = tiles(source, viewport);
let (covered_w, covered_h) = grid.covered(source);
assert!(
covered_w >= viewport.0 - 0.01 && covered_h >= viewport.1 - 0.01,
"{source:?} @ {viewport:?} 覆盖 {covered_w}×{covered_h} 小于视口(会留缝):{grid:?}"
);
assert!(
grid.count() <= MAX_TILES,
"{source:?} @ {viewport:?} 块数 {} 超预算",
grid.count()
);
assert!(grid.scale >= 1.0, "图块只许放大,不许缩小:{grid:?}");
}
// 图比视口大 ⇒ 1×1 原尺寸裁切(scale 必须还是 1.0,否则等于缩小了原图)。
let big = tiles((1600, 900), (1180.0, 760.0));
assert_eq!((big.cols, big.rows), (1, 1), "图比视口大 ⇒ 以原尺寸裁切");
assert_eq!((big.cols, big.rows, big.scale), (1, 1, 1.0));
let tiny = tiles((16, 16), (3840.0, 2160.0));
assert!(tiny.count() <= MAX_TILES, "{tiny:?} 超预算");
assert!(tiny.cols >= 1 && tiny.rows >= 1, "至少铺一块");
// 小图按原尺寸铺够(不放大时)。
let small = tiles((300, 200), (1180.0, 760.0));
assert_eq!((small.cols, small.rows, small.scale), (4, 4, 1.0));
}
/// 编译期钉住:填充档解析走 core 的词表(`cover`/`contain`/`tile`)。