feat(app-iced): M93 —— 外观接线:整窗 α / 面板 α / 背景图 / 配色来源

把 appearance.* 那批键接到窗口与像素上(M91 只落了键与判据,零调用方)。

整窗 α(D34/D36)
- `ensure_layered(hwnd)` 落在唯一 unsafe 文件 `platform/win32/transparency.rs`
  (`WS_EX_LAYERED` 只有 Get/SetWindowLongPtrW 能加,且 CI 的 unsafe-scope 是集合相等);
  `app/boot.rs` 只负责开窗,句柄到手后由 `Message::WindowHandle` → `window_alpha::dispatch`
  先补样式再下发。本机实测 `layered=ok outcome=Applied { byte: 205 }`(α=0.8)。
- 三条入口都会重算并下发:启动(句柄到达)、设置页改动、配置文件热切。
- `layered` 日志有三态:`n/a`(α≥1,压根不需要)/ `ok` / `FAILED`,避免把「没用到」写成假红。

面板 α 与 CPU 档(D36)
- 面板 α 独立于整窗 α:只作用到外壳层(根容器 / 标题栏 / 导航轨)与外观卡片;
  CPU 档强制 1.0、玻璃强制关(`pages/appearance/alpha.rs` 的真值表 + 单测)。
- 色卡档继续走令牌 baseline,只有取色档(①②级)才换 `Ui` 的 scheme(`Ui::from_scheme`)。

背景图(D38)
- 读盘解码(png)→ cover/contain/tile 三种填充几何 → `bg_dim` 向底色退 → 36dp 网格马赛克;
  iced 的 `image` widget 需要 `iced/image` feature(Cargo.toml 不在 scope),故为已登记降级。
- 读盘失败静默回落 + 一条可读诊断(设置页错误行 + 日志),配色经 `Provenance` 回落色卡。

设置页「外观」分组
- 整窗透明度滑块(下限 0.08)/ 面板透明度(CPU 档禁用并写明 55%→100% 的降级)/
  背景图开关与路径 / 填充三档 / 暗化 / 配色来源三档;改完即时生效并写回:
  bridge 覆盖的 5 个键走 `SettingsBridge::persist`,`window_opacity` / `palette_source`
  走 `AppCommand::SetSetting`(每个键只有一个写者)。
- `SettingsBridge::load_base()`(presenter)让 app-iced 不需要直接依赖 core 就能读回
  `window_opacity` / `palette_source`。

取证
- `ULL_APPEARANCE_*` / `ULL_START_ROUTE` 只影响本次进程(语义同 ULL_TICK_TRACE),
  截图通道不读用户配置(D30)。
This commit is contained in:
AskaEth committed 2026-11-02 18:07:31 +08:00
1 parent eae7ae0ff4
commit b8ab46662c
24 files changed
+3119 -48

No files matched your search

+59
View File
@@ -80,6 +80,22 @@ impl Ui {
self.scheme.flavor()
}
/// 用**已解析的方案**构造求值器(D38 的落点:背景图 / 桌面壁纸取色 → 49 个角色换掉 baseline)。
///
/// 只是**追加**的构造入口:既有的 [`Ui::new`] / [`Ui::with_platform`] 一字未动,默认仍是
/// [`Scheme::baseline`]。调用方(`app` 侧)拿到 `unlucky_design::palette::resolve_source` 的
/// 结果后走这里,于是「配色来源」真的能改颜色,而不只是一条日志。
///
/// 平台与骨架尺寸仍按当前环境求值——方案只带颜色,不带平台。
pub fn from_scheme(scheme: Scheme, families: Families) -> Self {
Self {
scheme,
platform: Platform::current(),
metrics: LayoutMetrics::DESKTOP,
families,
}
}
/// 当前平台(决定字体族与字重降级)。
pub fn platform(&self) -> Platform {
self.platform
@@ -331,4 +347,47 @@ mod tests {
assert_eq!(hovered.a, 1.0, "状态层必须给出不透明结果");
assert_ne!(hovered, ui.color(Role::Surface));
}
/// D38:配色来源解析出来的方案必须**真的换色**(M93)。
///
/// 抓得住:①`from_scheme` 用的是传进来的方案,不是 baseline(配一张蓝底的取色方案,
/// primary 与 baseline 不同);②默认构造(`new` / `with_platform`)逐字段等于 baseline
/// ——即「不传 scheme 时行为不变」,这条是防回流的那一半。
/// 抓不住:取色结果好不好看、设置页是否真的切过去(前者是设计判断,后者由 app 侧日志举证)。
#[test]
fn a_resolved_scheme_replaces_the_baseline_colours() {
use unlucky_design::palette::{resolve_source, CardKey, ImageInput};
// 4×4 的纯红图:种子彩度远高于门槛,必然走到「背景图取色」这一级。
let pixels = [0xC0_u8, 0x20, 0x20, 0xFF].repeat(16);
let input = ImageInput::pixels(&pixels, 4, 4);
let (scheme, provenance) =
resolve_source(Some(input), None, Some(CardKey::Sky), SchemeFlavor::Light);
assert_eq!(
provenance.level,
unlucky_design::palette::Level::BackgroundImage,
"纯红图必须取色成功(否则本测试的输入不合格)"
);
let tinted = Ui::from_scheme(scheme, FAMILIES);
let baseline = ui(SchemeFlavor::Light);
assert_ne!(
tinted.color(Role::Primary),
baseline.color(Role::Primary),
"取色方案必须换掉 primary"
);
// 深浅仍跟着方案走(方案自带 flavor),不会把 Light 变成 Dark。
assert_eq!(tinted.flavor(), SchemeFlavor::Light);
assert_eq!(tinted.name(), baseline.name());
// 默认构造不变:`with_platform` 与 baseline 逐块相等。
let explicit = Ui::from_scheme(Scheme::baseline(SchemeFlavor::Light), FAMILIES);
for role in Role::ALL {
assert_eq!(
explicit.color(role),
baseline.color(role),
"{role:?}:不传 scheme 时默认行为必须与改动前一致"
);
}
}
}