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

+19 -20
View File
@@ -34,7 +34,6 @@ use unlucky_design::typography::TypeRole;
use crate::app::Message;
use crate::cli::ForcedState;
use crate::icons;
use crate::pages::appearance::panel_color;
use crate::theme::Ui;
/// 整棵树(**内容槽位版**:外壳画标题栏 / 缩放热区 / 导航轨,中间那块交给调用方)。
@@ -52,8 +51,8 @@ use crate::theme::Ui;
/// 而页面自己的 `layout::frame_metrics` 仍以**窗口宽**为输入(它内部会再减 rail 与页边距),
/// 两边的账不会算串。
///
/// M93 起面板 α 是**外壳层**的属性(根容器 / 标题栏 / 导航轨的底色按它乘 alpha):要背景层的
/// 调用方走 [`view_with_background`]。
/// 面板 α(D36 / M93)不从这里传:它是**表面色**的属性,唯一施加点是 `Ui::layer`
/// (`crate::theme`)。外壳的根容器 / 标题栏 / 导航轨都走 `ui.layer(..)`,因此自动同源。
pub fn view_with_content<'a>(
ui: &'a Ui,
selected: NavItem,
@@ -61,7 +60,7 @@ pub fn view_with_content<'a>(
content: Element<'a, Message>,
forced: Option<ForcedState>,
) -> Element<'a, Message> {
view_with_background(ui, selected, app_title, None, 1.0, content, forced)
view_with_background(ui, selected, app_title, None, content, forced)
}
/// 同 [`view_with_content`],但可以在整棵树**最底下**垫一层背景(M93 的背景图马赛克)。
@@ -73,20 +72,19 @@ pub fn view_with_background<'a>(
selected: NavItem,
app_title: &'a str,
background: Option<Element<'a, Message>>,
panel_alpha: f32,
content: Element<'a, Message>,
forced: Option<ForcedState>,
) -> Element<'a, Message> {
let work_area = column![
titlebar(ui, app_title, forced, panel_alpha),
row![nav::rail(ui, selected, forced, panel_alpha), content]
titlebar(ui, app_title, forced),
row![nav::rail(ui, selected, forced), content]
.width(Length::Fill)
.height(Length::Fill),
]
.width(Length::Fill)
.height(Length::Fill);
window_frame(ui, background, panel_alpha, work_area.into())
window_frame(ui, background, work_area.into())
}
/// 整棵树(M68 骨架版:内容区是「页头 + 一张占位卡」)。
@@ -112,12 +110,9 @@ pub fn view<'a>(
/// 无边框窗口的「外壳壳子」:背景层 + 工作区 + 两条缩放热区 + 根容器底色。
///
/// 面板 α 落在两处:根容器底色(= 整窗 Canvas 层)与标题栏 / 导航轨底色。`panel_alpha == 1.0`
/// 时颜色逐字节不变,所以没开面板透明度时不动既有验收图。
fn window_frame<'a>(
ui: &'a Ui,
background: Option<Element<'a, Message>>,
panel_alpha: f32,
work_area: Element<'a, Message>,
) -> Element<'a, Message> {
// 无边框窗口没有系统缩放边框 → 自己给两条热区(东/南)。
@@ -150,7 +145,10 @@ fn window_frame<'a>(
.height(Length::Fill)
.align_y(Alignment::End);
// 背景在最底层:面板 α < 1 时从它的空隙里透出来(M93 / D34)。
// 背景在最底层(M93 / D34)。有背景层时**根容器不再盖实色画布**:否则那张不透明的
// `Canvas` 会把背景图整块挡住(r2 实测:panel α=1 时背景完全看不见)。
// 各面板(标题栏 / 导航轨 / 卡片 / 页头带)自己画底色,空隙里露出的就是背景图。
let has_background = background.is_some();
let shell = match background {
Some(layer) => stack![layer, work_area, east, south],
None => stack![work_area, east, south],
@@ -159,7 +157,13 @@ fn window_frame<'a>(
container(shell)
.width(Length::Fill)
.height(Length::Fill)
.style(move |_theme| filled(panel_color(ui, MaterialLayer::Canvas, panel_alpha)))
.style(move |_theme| {
if has_background {
container::Style::default()
} else {
filled(ui.layer(MaterialLayer::Canvas))
}
})
.into()
}
@@ -182,12 +186,7 @@ fn resize_edge<'a>(
// ---------------------------------------------------------------- 标题栏
fn titlebar<'a>(
ui: &'a Ui,
title: &'a str,
forced: Option<ForcedState>,
panel_alpha: f32,
) -> Element<'a, Message> {
fn titlebar<'a>(ui: &'a Ui, title: &'a str, forced: Option<ForcedState>) -> Element<'a, Message> {
let metrics = ui.metrics();
// 拖动热区:从窗口左缘到第一个按钮(按钮自己不能被拖区吞掉,所以只包左侧一段)
@@ -219,7 +218,7 @@ fn titlebar<'a>(
container(bar)
.width(Length::Fill)
.height(metrics.titlebar_height)
.style(move |_theme| filled(panel_color(ui, MaterialLayer::Canvas, panel_alpha)))
.style(move |_theme| filled(ui.layer(MaterialLayer::Canvas)))
.into()
}