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:
1 parent
22e4c423fa
commit
5c40c64b5f
13 files changed
+181
-85
No files matched your search
@@ -110,7 +110,7 @@ pub struct App {
|
||||
pub(super) refreshes: u64,
|
||||
/// 首屏快照到位后是否已经自动选中过第一行(list-detail 的桌面惯例,见 `update::auto_select`)。
|
||||
pub(super) auto_selected: bool,
|
||||
/// 窗口高度(背景图马赛克的视口判据;宽度在 `width`)。
|
||||
/// 窗口高度(背景图平铺网格的视口判据;宽度在 `width`)。
|
||||
pub(super) height: f32,
|
||||
/// 外观运行期状态(M93):面板 α / 背景图 / 配色来源 / 诊断。
|
||||
pub(super) appearance: pages::appearance::State,
|
||||
|
||||
@@ -144,9 +144,9 @@ fn a_broken_background_path_falls_back_silently_but_says_why() {
|
||||
);
|
||||
}
|
||||
|
||||
/// 抓得住:真 PNG → 背景真的生效(有马赛克、有格数、配色走「背景图取色」这一级)。
|
||||
/// 抓不住:马赛克的观感(36dp 网格是已登记的降级),以及填充三种档的视觉差异(几何由
|
||||
/// `background` 的纯函数测试守)。
|
||||
/// 抓得住:真 PNG → 背景真的生效(解出原图尺寸、路径留给渲染器、配色走「背景图取色」这一级)。
|
||||
/// 抓不住:渲染出来的像素(由截图举证),以及三种填充的覆盖范围(由 `background::tiles` 的
|
||||
/// 「覆盖 ≥ 视口」断言守)。
|
||||
#[test]
|
||||
fn a_decodable_png_makes_the_background_and_the_palette_active() {
|
||||
let dir = std::env::temp_dir().join(format!("ull-appear-png-{}", std::process::id()));
|
||||
@@ -211,6 +211,47 @@ fn the_settings_page_renders_the_real_appearance_group() {
|
||||
let _tree = pages::page_with_appearance(&app.ui, &app.view, &app.appearance);
|
||||
}
|
||||
|
||||
/// **P2-2 的守门(复审 r1)**:面板 α 只作用到允许半透明的层。
|
||||
///
|
||||
/// 抓得住:`Ui::layer(Canvas)` 在 α=0.55 时仍是不透明(标题栏/导航轨/页头带都取 Canvas,
|
||||
/// 它们一旦透底,三个窗口键与导航文字就压在背景照片上);`Card` / `Overlay` 必须真的吃到 α。
|
||||
/// 抓不住:真实合成后的观感(那由截图看)。
|
||||
#[test]
|
||||
fn the_canvas_layer_stays_opaque_while_panels_take_the_alpha() {
|
||||
use unlucky_design::elevation::MaterialLayer;
|
||||
|
||||
let mut app = headless(Options::default(), SchemeFlavor::Light);
|
||||
app.appearance.apply(Change::PanelAlpha(0.55));
|
||||
app.rebuild_ui();
|
||||
|
||||
let ui = app.ui();
|
||||
assert_eq!(ui.panel_alpha(), 0.55, "生效值进了 Ui");
|
||||
assert_eq!(
|
||||
ui.layer(MaterialLayer::Canvas).a,
|
||||
1.0,
|
||||
"窗口底(标题栏/导航轨/页头带)永远不透明——令牌的不变量"
|
||||
);
|
||||
for layer in [MaterialLayer::Card, MaterialLayer::Overlay] {
|
||||
assert!(
|
||||
(ui.layer(layer).a - 0.55).abs() < 1e-6,
|
||||
"{layer:?} 必须吃面板 α(实际 {})",
|
||||
ui.layer(layer).a
|
||||
);
|
||||
}
|
||||
|
||||
// CPU 档:α 被强制 1.0 ⇒ 连卡片也不透明。
|
||||
let mut cpu = headless(
|
||||
Options {
|
||||
backend: Backend::Cpu,
|
||||
..Options::default()
|
||||
},
|
||||
SchemeFlavor::Light,
|
||||
);
|
||||
cpu.appearance.apply(Change::PanelAlpha(0.55));
|
||||
cpu.rebuild_ui();
|
||||
assert_eq!(cpu.ui().layer(MaterialLayer::Card).a, 1.0, "CPU 档强制 1.0");
|
||||
}
|
||||
|
||||
/// 抓得住:**把滑块从头拖到尾**(0.00 → 1.00 的 11 个档)时,状态里的整窗 α 与下发字节逐档单调
|
||||
/// 上升、且最低一档就是下限字节 21(= 0.0824 ≥ 0.08)、最后一档不透明(不给字节)。
|
||||
/// 这就是「拖动 → 实际 α」的链条在数值上的等价物;真机拖动的那一次由二进制日志举证。
|
||||
|
||||
@@ -87,7 +87,7 @@ impl App {
|
||||
Message::Window(event) => {
|
||||
if let window::Event::Resized(size) = event {
|
||||
self.width = size.width;
|
||||
// 高度是背景马赛克的视口判据之一(宽度早就有),以前只记宽是因为没有背景层。
|
||||
// 高度是背景平铺网格的视口判据之一(宽度早就有),以前只记宽是因为没有背景层。
|
||||
self.height = size.height;
|
||||
self.view_dirty = true;
|
||||
self.refresh_view();
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
//! | `app` / `shell` | Elm 三件套 + 自绘标题栏 / 导航轨 / 内容骨架 | 业务逻辑 |
|
||||
//! | `shot` | `window::screenshot` → PNG(验收通道) | 图像比对 |
|
||||
//! | `icons` | 令牌里的 SVG path → iced `svg` 控件 | 画新图标 |
|
||||
//! | `platform` | 平台窗口能力(D36):整窗 α 的纯判据 + win32 FFI —— **全仓唯一** `unsafe` 落点 | 窗口创建 / 业务开关 |
|
||||
//! | `platform` | 平台窗口能力(D36):整窗 α 的纯判据 + win32 FFI(补分层样式 + 下发 α)—— **全仓唯一** `unsafe` 落点 | 窗口创建 / 业务开关 |
|
||||
//!
|
||||
//! ## 消费纪律
|
||||
//!
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
//! 背景图:**读盘解码(png / jpeg / bmp)→ 三种填充 → 暗化系数 → 可读诊断**。
|
||||
//!
|
||||
//! ## 真图直出(M93 r2 修订)
|
||||
//! ## 真图(M93 r2 起)
|
||||
//!
|
||||
//! r1 因为 iced 的 `image` feature 没开,只能把像素画成 36dp 网格的马赛克。r2 打开了
|
||||
//! `iced/image`(`crates/app-iced/Cargo.toml`),于是**真图直出**:填充由 iced 的 `ContentFit`
|
||||
//! 负责(`Cover` / `Contain`),平铺由本模块的 [`tiles`] 算网格、渲染层真的铺 N 个图块。
|
||||
//! 马赛克那条路已删除(不再有「每格一个颜色」的降级)。
|
||||
//! 渲染走 iced 的 `image`(`crates/app-iced/Cargo.toml` 的 `image-without-codecs`):`Cover` /
|
||||
//! `Contain` 交给 `ContentFit`,`Tile` 用本模块的 [`tiles`] 算网格后由 `picture` 真铺图块。
|
||||
//! 本模块只负责「解码一次(取色 + 诊断 + 尺寸)」与「网格/暗化的纯数学」。
|
||||
//!
|
||||
//! | 档 | 渲染 | 露底色 |
|
||||
//! |---|---|---|
|
||||
@@ -36,10 +35,19 @@ pub const MAX_EDGE: u32 = 256;
|
||||
/// 解码**之前**允许的原始边长上限(挡住「头里写着 40000×40000」的解压炸弹)。
|
||||
pub const MAX_SOURCE_EDGE: u32 = 16_384;
|
||||
|
||||
/// 平铺档一次最多铺多少个图块(列 × 行)。超了就不铺——避免一张 8×8 的小图在 4K 上拼出
|
||||
/// 上万个子控件。
|
||||
/// 平铺档一次最多铺多少个图块(列 × 行)。超了**放大图块**而不是少铺——见 [`tiles`] 的不变量。
|
||||
pub const MAX_TILES: u32 = 64;
|
||||
|
||||
/// 图块放大步长(每次乘这个数,直到块数落进预算)。
|
||||
pub const TILE_SCALE_STEP: f32 = 1.15;
|
||||
|
||||
/// 图块缩放上限(**只是安全阀**,正常情况下远达不到)。
|
||||
///
|
||||
/// 取 1024 而不是 64:`cols·rows ≤ MAX_TILES` 与「覆盖 ≥ 视口」在**极小图**上要求很大的缩放
|
||||
/// (1×1 的图在 1180×760 上要 ~118 倍才能用 64 块盖满)。取小了就等于两者不可兼得——那一侧
|
||||
/// 会退化成「留缝」,正是 r1 的 P2-1。真实图片不会走到这个上限。
|
||||
pub const MAX_TILE_SCALE: f32 = 1024.0;
|
||||
|
||||
/// 解码失败的原因(人话在 [`LoadError::describe`])。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum LoadError {
|
||||
@@ -165,13 +173,15 @@ pub fn dim_alpha(dim: f32) -> f32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// 平铺档的网格(列 × 行)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// 平铺档的网格(列 × 行 × 图块缩放)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Tiles {
|
||||
/// 列数。
|
||||
pub cols: u32,
|
||||
/// 行数。
|
||||
pub rows: u32,
|
||||
/// 单块图相对原尺寸的缩放(`1.0` = 原尺寸;超预算时**只放大图块**,不减少覆盖)。
|
||||
pub scale: f32,
|
||||
}
|
||||
|
||||
impl Tiles {
|
||||
@@ -180,33 +190,51 @@ impl Tiles {
|
||||
pub fn count(self) -> u32 {
|
||||
self.cols * self.rows
|
||||
}
|
||||
|
||||
/// 一块图的实际边长(dp):`源尺寸 × scale`。
|
||||
#[must_use]
|
||||
pub fn tile_size(self, source: (u32, u32)) -> (f32, f32) {
|
||||
(
|
||||
source.0.max(1) as f32 * self.scale,
|
||||
source.1.max(1) as f32 * self.scale,
|
||||
)
|
||||
}
|
||||
|
||||
/// 铺满后覆盖的宽高(dp):**恒 ≥ 视口**([`tiles`] 的硬不变量,单测钉住)。
|
||||
#[must_use]
|
||||
pub fn covered(self, source: (u32, u32)) -> (f32, f32) {
|
||||
let (w, h) = self.tile_size(source);
|
||||
(self.cols as f32 * w, self.rows as f32 * h)
|
||||
}
|
||||
}
|
||||
|
||||
/// 平铺档要铺几列几行(原图 1 像素 = 1 dp;总块数不超过 [`MAX_TILES`])。
|
||||
/// 平铺档的网格(原图 1 像素 = 1 dp)。
|
||||
///
|
||||
/// 图比视口大时是 `1×1`(等于「以原尺寸裁切」),与小图平铺是同一套算式。
|
||||
/// **硬不变量:`covered >= viewport`**——任何情况都不许留缝(复审 r1 P2-1 的返工点)。
|
||||
///
|
||||
/// r1 的写法是「超预算就减列/减行」,于是 120×120 的图需要 10×7=70 块 > [`MAX_TILES`],
|
||||
/// 列数被砍到 9 ⇒ 覆盖宽 1080 < 1180,右缘 100px 裸白。现在的做法是**放大图块步长**:
|
||||
/// `scale` 从 1.0 起按 [`TILE_SCALE_STEP`] 逐级变大,直到 `ceil(视口/图块) 的乘积 ≤ 预算`。
|
||||
/// 图块按同一比例放大 ⇒ 图不变形;`ceil` 保证每行/每列都盖过视口边界(多出来的部分由渲染器裁掉)。
|
||||
///
|
||||
/// 图比视口大时是 `1×1, scale=1.0`(= 以原尺寸裁切),与小图平铺是同一套算式。
|
||||
#[must_use]
|
||||
pub fn tiles(source: (u32, u32), viewport: (f32, f32)) -> Tiles {
|
||||
let width = source.0.max(1) as f32;
|
||||
let height = source.1.max(1) as f32;
|
||||
let cols = ceil_div(viewport.0.max(1.0), width);
|
||||
let rows = ceil_div(viewport.1.max(1.0), height);
|
||||
|
||||
// 超预算时按比例退:先保列再保行,至少留 1×1(否则什么都画不出来)。
|
||||
let mut tiles = Tiles {
|
||||
cols: cols.max(1),
|
||||
rows: rows.max(1),
|
||||
cols: 1,
|
||||
rows: 1,
|
||||
scale: 1.0,
|
||||
};
|
||||
while tiles.count() > MAX_TILES {
|
||||
if tiles.cols > 1 {
|
||||
tiles.cols -= 1;
|
||||
} else if tiles.rows > 1 {
|
||||
tiles.rows -= 1;
|
||||
} else {
|
||||
break;
|
||||
|
||||
loop {
|
||||
let (width, height) = tiles.tile_size(source);
|
||||
tiles.cols = ceil_div(viewport.0.max(1.0), width);
|
||||
tiles.rows = ceil_div(viewport.1.max(1.0), height);
|
||||
if tiles.count() <= MAX_TILES || tiles.scale >= MAX_TILE_SCALE {
|
||||
return tiles;
|
||||
}
|
||||
tiles.scale = (tiles.scale * TILE_SCALE_STEP).min(MAX_TILE_SCALE);
|
||||
}
|
||||
tiles
|
||||
}
|
||||
|
||||
fn ceil_div(value: f32, step: f32) -> u32 {
|
||||
|
||||
@@ -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`)。
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
//! | 文件 | 是什么 | 有没有 IO |
|
||||
//! |---|---|---|
|
||||
//! | [`alpha`] | 两层 α 的钳位链、CPU 档降级、滑块映射、下发字节 | 无(纯函数) |
|
||||
//! | [`background`] | 背景图解码、三种填充几何、暗化、马赛克取样 | 读盘一次(`load`) |
|
||||
//! | [`background`] | 背景图解码(png/jpeg/bmp)、平铺网格、暗化系数、可读诊断 | 读盘一次(`load`) |
|
||||
//! | `palette` | D38 三级优先序的调用侧 + 提示文案 | 无(像素由 `background` 给) |
|
||||
//! | `state` | 运行期状态:把上面三件的输入输出连起来 | 只在 `refresh_background` |
|
||||
//! | `section` | 设置页「外观」分组(真控件) | 无 |
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
//! 与 [`super::section`] 拆开只为单文件 ≤400 行的纪律;对外入口仍是
|
||||
//! `pages::appearance::layer`(本文件 [`layer`] 的再导出)。
|
||||
//!
|
||||
//! ## 为什么是「真图」
|
||||
//! ## 三种填充
|
||||
//!
|
||||
//! M93 r1 走的是「36dp 网格马赛克」(那时 iced 的 `image` feature 没开)。r2 打开后改成本文件:
|
||||
//! `Cover`/`Contain` 交给渲染器的 `ContentFit`(等比缩放 + 裁切/留白由它做),`Tile` 用
|
||||
//! [`background::tiles`] 算行列数后真的铺 N 个图块(`ContentFit::None` = 原尺寸)。
|
||||
//! `Cover`/`Contain` 交给渲染器的 `ContentFit`(等比缩放 + 裁切/留白由它做);`Tile` 用
|
||||
//! [`background::tiles`] 算行列后铺 N 个图块(单块尺寸 = 原尺寸 × `tiles.scale`)。
|
||||
//!
|
||||
//! 暗化不是逐像素改色,而是**盖一层「版面底色 @ α=dim」**:图不透明时它与「向底色插值」
|
||||
//! 数学等价([`background::dim_alpha`] 有推导),而普通 alpha 混合 CPU/GPU 两档都支持。
|
||||
|
||||
@@ -120,7 +120,21 @@ pub fn section<'a>(ui: &'a Ui, state: &'a State) -> Element<'a, Message> {
|
||||
.width(Length::Fill),
|
||||
);
|
||||
|
||||
// ⑤ 填充
|
||||
// ⑤ 诊断(读盘失败 / CPU 降级 / 配色回落)——**不许静默**,且必须在**首屏可见处**
|
||||
// (复审 r1 P2-3:文案落在折叠线以下等于没提示)。位置就在背景图那两行的紧下方。
|
||||
for note in state.diagnostics() {
|
||||
rows = rows.push(note_line(ui, note, ui.color(Role::Error)));
|
||||
}
|
||||
if let Some(note) = state.backend_note() {
|
||||
rows = rows.push(note_line(ui, note, ui.color(Role::OnSurfaceVariant)));
|
||||
}
|
||||
if let Some(note) = state.palette_note() {
|
||||
rows = rows.push(note_line(ui, note, ui.color(Role::OnSurfaceVariant)));
|
||||
}
|
||||
let resolved = format!("Colours: {}", state.palette_level());
|
||||
rows = rows.push(note_line(ui, &resolved, ui.color(Role::OnSurfaceVariant)));
|
||||
|
||||
// ⑥ 填充
|
||||
rows = rows.push(choice_row(
|
||||
ui,
|
||||
"Fill",
|
||||
@@ -146,7 +160,7 @@ pub fn section<'a>(ui: &'a Ui, state: &'a State) -> Element<'a, Message> {
|
||||
],
|
||||
));
|
||||
|
||||
// ⑥ 暗化
|
||||
// ⑦ 暗化
|
||||
rows = rows.push(slider_row(
|
||||
ui,
|
||||
"Dim",
|
||||
@@ -161,7 +175,7 @@ pub fn section<'a>(ui: &'a Ui, state: &'a State) -> Element<'a, Message> {
|
||||
),
|
||||
));
|
||||
|
||||
// ⑦ 配色来源
|
||||
// ⑧ 配色来源
|
||||
rows = rows.push(choice_row(
|
||||
ui,
|
||||
"Colour source",
|
||||
@@ -187,19 +201,6 @@ pub fn section<'a>(ui: &'a Ui, state: &'a State) -> Element<'a, Message> {
|
||||
],
|
||||
));
|
||||
|
||||
// ⑧ 诊断(读盘失败 / CPU 降级 / 配色回落)——**不许静默**
|
||||
for note in state.diagnostics() {
|
||||
rows = rows.push(note_line(ui, note, ui.color(Role::Error)));
|
||||
}
|
||||
if let Some(note) = state.backend_note() {
|
||||
rows = rows.push(note_line(ui, note, ui.color(Role::OnSurfaceVariant)));
|
||||
}
|
||||
if let Some(note) = state.palette_note() {
|
||||
rows = rows.push(note_line(ui, note, ui.color(Role::OnSurfaceVariant)));
|
||||
}
|
||||
let resolved = format!("Colours: {}", state.palette_level());
|
||||
rows = rows.push(note_line(ui, &resolved, ui.color(Role::OnSurfaceVariant)));
|
||||
|
||||
card(ui, rows)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! - 本文件 = 纯判据 [`effective_window_alpha`]:不读环境变量、不调 FFI、不碰窗口句柄,
|
||||
//! 只回答「按这个渲染后端和用户要的 α,最终该给窗口什么值」;
|
||||
//! - [`win32::transparency`] = 唯一允许 `unsafe` 的文件(`SetLayeredWindowAttributes`)。
|
||||
//! - [`win32::transparency`] = 唯一允许 `unsafe` 的文件(补 `WS_EX_LAYERED` + `SetLayeredWindowAttributes`)。
|
||||
//! - [`locale`] = 系统语言真值源(D28):`reg.exe` 子进程读注册表,**零 `unsafe` / 零 FFI**。
|
||||
//!
|
||||
//! 为什么判据要单独拎出来:D36 的降级规则(α 下限 0.08 / `≤0` 一律拒绝 / CPU 档也允许
|
||||
|
||||
@@ -11,8 +11,12 @@
|
||||
//! ## 只做 LWA_ALPHA
|
||||
//!
|
||||
//! `LWA_COLORKEY` / `DwmEnableBlurBehindWindow` 一律不用:色键会把某个颜色整块变透明(视觉
|
||||
//! 事故),模糊是另一条路线(D36 已排除)。`WS_EX_LAYERED` 是调用前提,由窗口创建侧设置,
|
||||
//! 本文件不负责——它只做「给定句柄,把 α 下发下去」。
|
||||
//! 事故),模糊是另一条路线(D36 已排除)。
|
||||
//!
|
||||
//! `WS_EX_LAYERED`(`SetLayeredWindowAttributes` 的调用前提)**也归本文件**:加扩展位只有
|
||||
//! `Get/SetWindowLongPtrW` 能走,必须 `unsafe`,而全仓放行 unsafe 的文件集合恰好只有本文件。
|
||||
//! 所以窗口创建侧(`app::boot`)不碰扩展样式,它只负责拿到句柄后先调 [`ensure_layered`]
|
||||
//! 再调 [`apply_window_alpha`]——两件事都在同一个 unsafe 落点里,门禁仍是「集合相等」。
|
||||
//!
|
||||
//! ## 非 Windows
|
||||
//!
|
||||
|
||||
@@ -63,7 +63,7 @@ pub fn view_with_content<'a>(
|
||||
view_with_background(ui, selected, app_title, None, content, forced)
|
||||
}
|
||||
|
||||
/// 同 [`view_with_content`],但可以在整棵树**最底下**垫一层背景(M93 的背景图马赛克)。
|
||||
/// 同 [`view_with_content`],但可以在整棵树**最底下**垫一层背景(M93 的真图背景层)。
|
||||
///
|
||||
/// `background` 由 `pages::appearance::layer` 产出(已与 `Message::Page` 映射过);垫在 `stack!`
|
||||
/// 最底层,于是「面板 α < 1」时能真的看见它——这就是 D34 在像素上的落点。
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
//! 令牌 → iced 类型(`Color` / `Font` / `Border` / `Radius` / `Theme`)。
|
||||
//!
|
||||
//! ## 边界(M61 `design-令牌约定.md` §0)
|
||||
//!
|
||||
//! 本模块是**唯一**允许把 `unlucky-design` 的值翻成 iced 类型的地方:组件层只调 [`Ui`],
|
||||
//! 绝不出现色值/字号/半径/间距字面量(`tests/no_hardcoded_tokens.rs` 扫 `src/**` 守门)。
|
||||
//! 本模块是**唯一**允许把 `unlucky-design` 的值翻成 iced 类型的地方(M61 §0):组件层只调
|
||||
//! [`Ui`],绝不出现色值/字号/半径/间距字面量(`tests/no_hardcoded_tokens.rs` 扫 `src/**`)。
|
||||
//!
|
||||
//! ## 为什么不用 `iced::theme::Base` 承载 49 个角色
|
||||
//!
|
||||
//! iced 的 `Palette` 只有 6 个颜色,M3 有 49 个角色(M60 Q5 + M61 §9-0)。所以真源是本模块持有
|
||||
//! 的 [`Scheme`],iced 的 `Theme` 只用来「换名字」——名字变了 iced 才重算受主题影响的缓存,
|
||||
//! `palette()` 只给 devtools 看,控件外观由各自的 `Style` 注入。
|
||||
//!
|
||||
//! `success`/`warning` 在 M3 里没有对应角色,按语义最近的现成角色映射
|
||||
//! (`success → Secondary`、`warning → Tertiary`),任何控件都不要读这两个槽位。
|
||||
//! iced 的 `Palette` 只有 6 色,M3 有 49 个角色(M60 Q5):真源是本模块的 [`Scheme`],`Theme`
|
||||
//! 只用来「换名字」(名字变了 iced 才重算缓存),控件外观由各自的 `Style` 注入。
|
||||
//! `success`/`warning` 没有对应角色,按邻近语义映射(→ `Secondary` / `Tertiary`),不许读。
|
||||
|
||||
use iced::border::Radius;
|
||||
use iced::theme::Palette;
|
||||
@@ -113,14 +108,19 @@ impl Ui {
|
||||
to_iced(self.scheme.get(role))
|
||||
}
|
||||
|
||||
/// 材质层(Canvas/Card/Overlay)→ 容器色,**已乘上面板 α**(M93 的唯一来源)。
|
||||
/// 材质层(Canvas/Card/Overlay)→ 容器色;**只有令牌允许半透明的层才乘面板 α**(M93 唯一来源)。
|
||||
///
|
||||
/// 为什么 α 施加在这里:面板 α 是**表面色**的属性,而全仓所有表面色都从这个函数出去
|
||||
/// (外壳的根容器/标题栏/导航轨、`pages::frame::card`、空态图标块……),乘一次就让
|
||||
/// 四页 + 外壳自动同源,调用点没有机会「各处自乘」。`α = 1.0` 时逐字节等于原色。
|
||||
/// α 乘在这里而不是调用点:所有表面色都从这里出去(根容器/标题栏/导航轨/页头带/卡片),
|
||||
/// 一次同源、调用点无从「各自自乘」。Canvas 层恒不透明(r1 P2-2):令牌
|
||||
/// [`MaterialLayer::allows_translucency`] 写死 `Canvas => false`(窗口底),而三个窗口键与
|
||||
/// 导航轨、页头带都取 Canvas——给它们乘 α 会让文字直接压在背景照片上(α=0.55 实测不可读)。
|
||||
pub fn layer(&self, layer: MaterialLayer) -> Color {
|
||||
self.color(layer.surface_role())
|
||||
.scale_alpha(self.panel_alpha)
|
||||
let base = self.color(layer.surface_role());
|
||||
if layer.allows_translucency() {
|
||||
base.scale_alpha(self.panel_alpha)
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前面板 α(CPU 档由 `app` 侧强制成 1.0)。
|
||||
|
||||
Reference in new issue
Block a user