feat(app-iced): M93 r2 —— 背景图真图直出 + 面板 α 收敛到 Ui::layer

复审前补齐两处(tower 扩 scope:Cargo.toml / pages/frame.* / theme.rs / presenter::prefs)。

1) 背景图**真图直出**
- `iced` 开 `image-without-codecs`(刻意不用 `image`:后者会打开 `image/default` 全套编解码器);
  编解码能力由本 crate 的 `image` features 决定,补了 `jpeg` / `bmp`(用户挑的背景图/壁纸多半是 jpg)。
- `pages/appearance/picture.rs`:`Cover`/`Contain` 走渲染器 `ContentFit`,`Tile` 按 `background::tiles`
  算行列(上限 64 块)真的铺 N 个图块;暗化 = 一层「版面底色 @ α=dim」叠在图上。
- 36dp 马赛克那条路**已删除**(不再是主路径,也不留死代码)。
- 踩坑记录:`Handle::from_bytes` 的纹理分配赶不上截图第 3 帧(实测画面全空),改用
  `Handle::from_path`(渲染器自己解码),已在 `picture::image_handle` 的文档里写明。

2) 面板 α **收敛到唯一来源**
- `Ui::layer()` 直接乘上面板 α(新增 `Ui::panel_alpha` / `with_panel_alpha`,默认 1.0、
  `scale_alpha(1.0)` 逐字节不变),`app::rebuild_ui` 注入生效值。
- 于是外壳(根容器/标题栏/导航轨)、`frame::card`(实例/下载/日志三页卡片)、
  `frame::header_bar`(新增底色)、外观分组卡片、空态图标块**全部同源**,
  调用点不再有任何 `panel_color(..)` 自乘(该 helper 已删)。
- 有背景图时根容器不再盖一层实色画布(否则 panel α=1 时背景图整块被挡)。

3) `presenter::prefs` 补重导出 `PANEL_ALPHA_MIN` / `BG_DIM_MAX`,app-iced 里两份 0.4 / 0.95
   字面量改为转发常量。

验收:11+3 张图(`C:\Temp\ull-shots\wt-93\r2-*.png`)——三档填充肉眼可分、`.jpg` 能加载、
GPU 档 panel55 时卡片/分组卡/页头带都透出背景、CPU 档仍强制 1.0 且背景在面板空隙可见;
另附实例/下载/日志三页 panel55 各一张,证明三页都吃到同一个 α。
This commit is contained in:
AskaEth committed 2026-11-02 18:07:32 +08:00
1 parent b8ab46662c
commit 22e4c423fa
18 files changed
+399 -535

No files matched your search

@@ -44,56 +44,6 @@ use super::background::BG_DIM_MAX;
use super::state::State;
use super::Change;
/// 一处面板/卡片表面色:令牌色 × 生效的面板 α。
///
/// `alpha == 1.0` 时返回**逐字节相同**的令牌色(`scale_alpha(1.0)` 不引入误差),所以没开面板
/// 透明度时不会动到任何既有验收图的像素。
#[must_use]
pub fn panel_color(ui: &Ui, layer: MaterialLayer, alpha: f32) -> Color {
let alpha = if alpha.is_finite() {
alpha.clamp(0.0, 1.0)
} else {
1.0
};
ui.layer(layer).scale_alpha(alpha)
}
/// 背景层(铺满整窗、垫在最下面的马赛克)。`None` = 没有背景图(调用方就不叠这一层)。
#[must_use]
pub fn layer(ui: &Ui, state: &State, viewport: (f32, f32)) -> Option<Element<'static, Message>> {
let fallback = ui.layer(MaterialLayer::Canvas);
let mosaic = state.mosaic(viewport, rgb8(fallback))?;
let rows = (0..mosaic.rows)
.map(|row| {
let cells = (0..mosaic.cols)
.map(|col| cell(mosaic.at(row, col), fallback))
.collect::<Vec<_>>();
iced::widget::row(cells)
.width(Length::Fill)
.height(Length::Fill)
.into()
})
.collect::<Vec<_>>();
Some(column(rows).width(Length::Fill).height(Length::Fill).into())
}
/// iced 颜色 → 8 bit RGB(暗化的「底色」方向)。
fn rgb8(color: Color) -> [u8; 3] {
let channel = |value: f32| (value.clamp(0.0, 1.0) * 255.0).round() as u8;
[channel(color.r), channel(color.g), channel(color.b)]
}
fn cell<'a>(color: Option<[u8; 3]>, fallback: Color) -> Element<'a, Message> {
let color = color.map_or(fallback, |[r, g, b]| Color::from_rgb8(r, g, b));
container(iced::widget::space::horizontal())
.width(Length::Fill)
.height(Length::Fill)
.style(move |_theme| frame::filled(color))
.into()
}
/// 「外观」分组整块(一张卡)。
pub fn section<'a>(ui: &'a Ui, state: &'a State) -> Element<'a, Message> {
let mut rows = column![].spacing(ui.space(Space::X1)).width(Length::Fill);
@@ -250,16 +200,13 @@ pub fn section<'a>(ui: &'a Ui, state: &'a State) -> Element<'a, Message> {
let resolved = format!("Colours: {}", state.palette_level());
rows = rows.push(note_line(ui, &resolved, ui.color(Role::OnSurfaceVariant)));
card_with_panel_alpha(ui, state, rows)
card(ui, rows)
}
fn card_with_panel_alpha<'a>(
ui: &'a Ui,
state: &State,
content: impl Into<Element<'a, Message>>,
) -> Element<'a, Message> {
/// 外观卡片:底色走 `ui.layer`(面板 α 的唯一来源),与其它页的卡片同源。
fn card<'a>(ui: &'a Ui, content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
let spec = Component::OutlinedCard.spec();
let background = panel_color(ui, MaterialLayer::Card, state.effective_panel_alpha());
let background = ui.layer(MaterialLayer::Card);
container(content)
.width(Length::Fill)