复审(代码)r1 的两条 P1 + 两条必修。 P1-1 `SetSetting` 写不回 Float 键(整窗透明度重启回旧值) - `presenter/src/host/settings.rs::parse_value` 改成**按 schema 的 `ValueKind` 取类型**, 不再硬编码 Int/Bool 键名。历史:`appearance.window_opacity`(Float)被当 String 送进 `store.set` → schema 的 `as_f64()` 返回 None → `InvalidValue` → **`save()` 根本没执行**, α 只在内存生效、重启回旧值、每次拖动弹一条 Error toast。 - 测试(presenter 层,`host/settings_tests.rs`): ① `float_key_round_trips_through_set_setting`:set 0.8 → **新开 store(= 重启)** 读回 0.8; ② `every_float_key_in_the_schema_can_be_written_back`:遍历 schema 里**所有** `ValueKind::Float` 的键逐个写回并读回(集合级守门,下次新增 Float 键漏接会立刻红)。 P1-2 `presenter/src/prefs.rs` 404 行 → 拆测试 - `#[cfg(test)] mod tests` 整体挪到 `crates/presenter/src/prefs/tests.rs`,结构体与 re-export 一字未动。`wc -l`:**404 → 315**(tests.rs 90 行)。 P2-2 CPU 档判定只在 `--renderer=cpu` 时成立 - 新增 `app/backend.rs`:`effective_backend()` 把 `probe::apply` 写下的 `ICED_BACKEND` 候选列表翻回档位(`tiny-skia` / `tiny-skia,wgpu` → CPU;`wgpu,tiny-skia` → GPU;未设 → 请求值)。 `App::with_presenter` 用它喂 `appearance::State`,于是**默认 Auto 档在没有独显的机器上** 也会走「面板 α / 玻璃强制 1.0」。 - 测试:`every_iced_backend_list_maps_to_a_tier` + `auto_with_a_software_renderer_forces_the_panel_opaque`。 - 实跑:`ICED_BACKEND=tiny-skia`(= probe 在无独显机器上写下的值)→ `[appearance] boot … panel_alpha=1.000 (requested 0.550) backend=cpu`。 P2-1 平铺退化成「一条竖条」——根因不是几何,是**渲染层没吃 scale**(r3 的编辑没落到 `tiled()`) - `picture::tiled()` 原来仍用 `ContentFit::None` + 原图尺寸,所以 `tiles()` 算出的 `scale` 完全没被消费:小图被按 32dp 画在 129dp 的格子里 ⇒ 到处是缝。 - 现在:每块 `源尺寸 × tiles.scale`、`ContentFit::Fill`、行高显式 `Fixed`、整列 `Fill`。 - 诊断:新增 `background::small_tile_note()`(短边 < 200dp 的图在 tile 档下给一句 「scaled up so the window stays covered」),已接进设置页诊断块(首屏可见)。 - 测试:`tile_grid_always_covers_the_viewport_within_budget`(6 组尺寸,含 32×32 / 16×16 / 1×1, 断言覆盖 ≥ 视口且块数 ≤ 预算)、`small_images_get_a_tile_diagnostic_instead_of_silently_degrading`。 - 出图:`r5-tile-tiny32.png`(32×32 铺满全窗、右缘无裸白、提示可见)、`r5-tile-120.png`、 `r5-tile-big.png`。 顺手(复审 P2-3/P2-4/P2-5) - P2-4:`MAX_SOURCE_EDGE` 16384 → **8192**,并给解码器加 `image::Limits`(宽/高 + `max_alloc` 320MB)作第二道闸;补 `an_over_sized_image_reports_too_large_instead_of_trying_to_decode` (`TooLarge` 这条分支此前无测试)。 - P2-5:`transparency.rs` 补一段「为什么 `LWA_ALPHA` + `WS_EX_LAYERED` 不影响命中测试与 自绘标题栏拖动」(穿透要 `WS_EX_TRANSPARENT` 或 `LWA_COLORKEY`,两者都没用;`window::drag` 走 `WM_NCLBUTTONDOWN(HTCAPTION)`)。 - P2-3:断链 `[background::dimmed]` → `dim_alpha`;`iced/image` → `image-without-codecs`; 测试文档 204 → 205;马赛克残留说明只保留 r1→r2 的历史一句。 - 解码在 UI 线程同步做:**登记欠账**(挪后台线程超出本 mission)。
114 lines
4.5 KiB
Rust
114 lines
4.5 KiB
Rust
//! 背景层的**像素**:真图直出(`Cover` / `Contain` / `Tile`)+ 暗化叠层。
|
||
//!
|
||
//! 与 [`super::section`] 拆开只为单文件 ≤400 行的纪律;对外入口仍是
|
||
//! `pages::appearance::layer`(本文件 [`layer`] 的再导出)。
|
||
//!
|
||
//! ## 三种填充
|
||
//!
|
||
//! `Cover`/`Contain` 交给渲染器的 `ContentFit`(等比缩放 + 裁切/留白由它做);`Tile` 用
|
||
//! [`background::tiles`] 算行列后铺 N 个图块(单块尺寸 = 原尺寸 × `tiles.scale`)。
|
||
//!
|
||
//! 暗化不是逐像素改色,而是**盖一层「版面底色 @ α=dim」**:图不透明时它与「向底色插值」
|
||
//! 数学等价([`background::dim_alpha`] 有推导),而普通 alpha 混合 CPU/GPU 两档都支持。
|
||
|
||
use iced::widget::image;
|
||
use iced::widget::{column, container, image as image_widget, space};
|
||
use iced::{ContentFit, Element, Length};
|
||
|
||
use unlucky_design::color::Role;
|
||
use unlucky_presenter::prefs::BgFill;
|
||
|
||
use crate::pages::{frame, Message};
|
||
use crate::theme::Ui;
|
||
|
||
use super::background;
|
||
use super::state::State;
|
||
|
||
/// 背景层(铺满整窗、垫在最下层)。`None` = 没有可用的背景图(调用方就不叠这一层)。
|
||
///
|
||
/// 真图直出(M93 r2):`Cover` / `Contain` 交给 iced 的 `ContentFit`,`Tile` 用
|
||
/// [`background::tiles`] 算网格铺 N 个图块。暗化 = 一层「版面底色 @ α=dim」盖在图上
|
||
/// (见 [`background::dim_alpha`] 的等价性说明)。
|
||
#[must_use]
|
||
pub fn layer(ui: &Ui, state: &State, viewport: (f32, f32)) -> Option<Element<'static, Message>> {
|
||
let picture = state.image()?;
|
||
if !state.bg_enabled {
|
||
return None;
|
||
}
|
||
|
||
let handle = image_handle(state.bg_path.trim());
|
||
let art: Element<'static, Message> = match state.bg_fill {
|
||
BgFill::Cover => fitted(handle, ContentFit::Cover),
|
||
BgFill::Contain => fitted(handle, ContentFit::Contain),
|
||
BgFill::Tile => tiled(handle, picture.source, viewport),
|
||
};
|
||
|
||
// 暗化:底色 @ α=dim 盖在图上(等价于逐像素向底色插值,见 `background::dim_alpha`)。
|
||
let scrim_color = ui
|
||
.color(Role::Surface)
|
||
.scale_alpha(background::dim_alpha(state.bg_dim));
|
||
let scrim: Element<'static, Message> = container(space::horizontal())
|
||
.width(Length::Fill)
|
||
.height(Length::Fill)
|
||
.style(move |_theme| frame::filled(scrim_color))
|
||
.into();
|
||
|
||
Some(
|
||
iced::widget::stack![art, scrim]
|
||
.width(Length::Fill)
|
||
.height(Length::Fill)
|
||
.into(),
|
||
)
|
||
}
|
||
|
||
/// 文件字节 → iced 的图片句柄(渲染器自己解复用,不再读盘)。
|
||
fn image_handle(path: &str) -> image::Handle {
|
||
image::Handle::from_path(path)
|
||
}
|
||
|
||
/// `Cover` / `Contain`:交给渲染器的等比缩放档。
|
||
fn fitted(handle: image::Handle, fit: ContentFit) -> Element<'static, Message> {
|
||
image_widget(handle)
|
||
.content_fit(fit)
|
||
.width(Length::Fill)
|
||
.height(Length::Fill)
|
||
.into()
|
||
}
|
||
|
||
/// `Tile`:按 [`background::tiles`] 的网格平铺。
|
||
///
|
||
/// 三个细节都是为了「不留缝」(复审 r1 P2-1):
|
||
/// 1. 单块边长 = `源尺寸 × tiles.scale`——超预算时图块**等比例放大**(不是少铺),
|
||
/// 所以 `cols × 边长 >= 视口宽`、`rows × 边长 >= 视口高` 由 [`background::tiles`] 保证;
|
||
/// 2. `ContentFit::Fill`:`None` 会按**图片原生尺寸**画,比格子小就留空——那道缝正是它造成的;
|
||
/// 这里格子尺寸恰好等于「等比放大后的尺寸」,`Fill` 就是等比缩放本身(不变形);
|
||
/// 3. 行高显式写死(`Fixed`),列整体 `Fill`:不靠子控件撑,布局结果与算式一致。
|
||
fn tiled(
|
||
handle: image::Handle,
|
||
source: (u32, u32),
|
||
viewport: (f32, f32),
|
||
) -> Element<'static, Message> {
|
||
let tiles = background::tiles(source, viewport);
|
||
let (width, height) = tiles.tile_size(source);
|
||
|
||
let rows = (0..tiles.rows)
|
||
.map(|_| {
|
||
let cells = (0..tiles.cols)
|
||
.map(|_| -> Element<'static, Message> {
|
||
image_widget(handle.clone())
|
||
.content_fit(ContentFit::Fill)
|
||
.width(Length::Fixed(width))
|
||
.height(Length::Fixed(height))
|
||
.into()
|
||
})
|
||
.collect::<Vec<_>>();
|
||
iced::widget::row(cells)
|
||
.width(Length::Fill)
|
||
.height(Length::Fixed(height))
|
||
.into()
|
||
})
|
||
.collect::<Vec<_>>();
|
||
|
||
column(rows).width(Length::Fill).height(Length::Fill).into()
|
||
}
|