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:
1 parent
eae7ae0ff4
commit
b8ab46662c
24 files changed
+3119
-48
No files matched your search
@@ -29,6 +29,12 @@ use unlucky_presenter::prefs::WINDOW_ALPHA_MIN;
|
||||
/// 单独立成常量是为了让「写错成 `LWA_COLORKEY`(`0x1`)等于什么都没做」这件事有断言盯着。
|
||||
pub const LWA_ALPHA: u32 = 0x0000_0002;
|
||||
|
||||
/// `WS_EX_LAYERED`:分层窗口扩展样式(`0x0008_0000`)。**整窗 α 的调用前提**。
|
||||
pub const WS_EX_LAYERED: isize = 0x0008_0000;
|
||||
|
||||
/// `GWL_EXSTYLE`:扩展样式在 `Get/SetWindowLongPtrW` 里的槽位号(`-20`)。
|
||||
pub const GWL_EXSTYLE: i32 = -20;
|
||||
|
||||
/// 一次整窗 α 应用的结果。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AlphaOutcome {
|
||||
@@ -77,11 +83,96 @@ pub fn alpha_byte(alpha: f64) -> u8 {
|
||||
(clamped * 255.0).ceil() as u8
|
||||
}
|
||||
|
||||
/// 给窗口补上 `WS_EX_LAYERED`(幂等)。
|
||||
///
|
||||
/// ## 为什么这件事必须在这里(而不是「窗口创建侧」)
|
||||
///
|
||||
/// 加扩展样式只有 `GetWindowLongPtrW` / `SetWindowLongPtrW` 一条路,都是 `unsafe`;而
|
||||
/// [`super::super`] 的 CI 门禁(`.github/workflows/guards.yml` 的 `unsafe-scope`)要求
|
||||
/// 全仓显式放行 `unsafe_code` 的文件集合**恰好等于本文件**。所以窗口创建侧的
|
||||
/// `app/boot.rs` 只负责「拿句柄 → 调本函数 → 调 [`apply_window_alpha`]」,
|
||||
/// 不新增第二个放行文件。
|
||||
///
|
||||
/// ## 为什么创建侧无法预先带上这一位
|
||||
///
|
||||
/// iced 0.14 的 `window::Settings` 没有扩展样式入口;winit 0.30 只在
|
||||
/// `IGNORE_CURSOR_EVENT`(鼠标穿透,`WS_EX_TRANSPARENT | WS_EX_LAYERED`)时置位,
|
||||
/// `transparent: true` 走的是 DWM 那条路(`window.rs` 的 `ENABLE BLUR BEHIND WINDOW`),
|
||||
/// 与 `LWA_ALPHA` 无关。所以只能拿到句柄后补。
|
||||
///
|
||||
/// ## 返回值
|
||||
///
|
||||
/// `true` = 这一位**已就位**(原本就有,或本次加上了)。判据是**再加读一次**验证,
|
||||
/// 而不是信 `SetWindowLongPtrW` 的返回值:Win32 的它「失败返回 0 且成功也可能返回 0」
|
||||
/// (前值就是 0 时),只能靠回读取证。句柄为 0 / 无效 → `false`(`GetWindowLongPtrW`
|
||||
/// 也读不到这一位)。
|
||||
pub fn ensure_layered(hwnd: isize) -> bool {
|
||||
if hwnd == 0 {
|
||||
return false;
|
||||
}
|
||||
if has_layered(hwnd) {
|
||||
return true;
|
||||
}
|
||||
raw_set_ex_style(hwnd, raw_ex_style(hwnd) | WS_EX_LAYERED);
|
||||
has_layered(hwnd)
|
||||
}
|
||||
|
||||
/// 读扩展样式(跨位宽的唯一落点:64 位走 `GetWindowLongPtrW`,32 位走 `GetWindowLongW`)。
|
||||
#[cfg(all(target_os = "windows", target_pointer_width = "64"))]
|
||||
fn raw_ex_style(hwnd: isize) -> isize {
|
||||
// SAFETY: 只读一个窗口属性,按值传参、不持有指针、无所有权转移;无效句柄只会返回 0。
|
||||
unsafe { GetWindowLongPtrW(hwnd, GWL_EXSTYLE) }
|
||||
}
|
||||
|
||||
/// 32 位目标:符号是 `GetWindowLongW`(`link_name` 已映射),返回 `i32`,扩到 `isize`。
|
||||
#[cfg(all(target_os = "windows", target_pointer_width = "32"))]
|
||||
fn raw_ex_style(hwnd: isize) -> isize {
|
||||
// SAFETY: 同 64 位分支。
|
||||
unsafe { isize::from(GetWindowLongPtrW(hwnd, GWL_EXSTYLE)) }
|
||||
}
|
||||
|
||||
/// 非 Windows:没有扩展样式这个概念,读 0(`ensure_layered` 因此恒 `false`)。
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn raw_ex_style(_hwnd: isize) -> isize {
|
||||
0
|
||||
}
|
||||
|
||||
/// 写扩展样式(位宽分档同上)。
|
||||
#[cfg(all(target_os = "windows", target_pointer_width = "64"))]
|
||||
fn raw_set_ex_style(hwnd: isize, style: isize) {
|
||||
// SAFETY: 同 `raw_ex_style`:按值传参、不持有指针;返回值刻意不用(改完靠回读取证)。
|
||||
unsafe {
|
||||
SetWindowLongPtrW(hwnd, GWL_EXSTYLE, style);
|
||||
}
|
||||
}
|
||||
|
||||
/// 32 位目标:符号是 `SetWindowLongW`;扩展样式本身就是 32 位量,收窄无损。
|
||||
#[cfg(all(target_os = "windows", target_pointer_width = "32"))]
|
||||
fn raw_set_ex_style(hwnd: isize, style: isize) {
|
||||
let style = i32::try_from(style).unwrap_or(i32::MAX);
|
||||
// SAFETY: 同 64 位分支。
|
||||
unsafe {
|
||||
SetWindowLongPtrW(hwnd, GWL_EXSTYLE, style);
|
||||
}
|
||||
}
|
||||
|
||||
/// 非 Windows:空实现。
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn raw_set_ex_style(_hwnd: isize, _style: isize) {}
|
||||
|
||||
/// 回读判据:这一位在不在扩展样式里。
|
||||
fn has_layered(hwnd: isize) -> bool {
|
||||
raw_ex_style(hwnd) & WS_EX_LAYERED != 0
|
||||
}
|
||||
|
||||
/// 把整窗 α 下发到窗口。
|
||||
///
|
||||
/// `alpha` 一般直接来自 [`super::super::effective_window_alpha`](`None` = 拒绝整窗穿透)。
|
||||
/// 本函数是**执行层**:不再看渲染后端、不读环境变量,只认「有没有一个合法的值」+
|
||||
/// 「有没有句柄」,然后(仅 Windows)调一次 `SetLayeredWindowAttributes`。
|
||||
///
|
||||
/// 调用前提是窗口已带 `WS_EX_LAYERED`(见 [`ensure_layered`]):没有它 `SetLayeredWindowAttributes`
|
||||
/// 一定失败(Win32 返回 0,本机实测 `GetLastError` 常见 87 / `ERROR_INVALID_PARAMETER`)。
|
||||
pub fn apply_window_alpha(hwnd: isize, alpha: Option<f64>) -> AlphaOutcome {
|
||||
let Some(alpha) = alpha.filter(|value| value.is_finite() && *value > 0.0) else {
|
||||
return AlphaOutcome::Rejected(AlphaRejection::NoAlpha);
|
||||
@@ -102,6 +193,26 @@ extern "system" {
|
||||
///
|
||||
/// 签名照 Win32 头文件:`BOOL SetLayeredWindowAttributes(HWND, COLORREF, BYTE, DWORD)`。
|
||||
fn SetLayeredWindowAttributes(hwnd: isize, color_key: u32, alpha: u8, flags: u32) -> i32;
|
||||
|
||||
/// `GetWindowLongPtrW`:读窗口属性(`GWL_EXSTYLE` = `-20`)。
|
||||
///
|
||||
/// 头文件里 `GetWindowLongPtrW` 是宏:64 位映射到 `GetWindowLongPtrW`、32 位映射到
|
||||
/// `GetWindowLongW`。user32 在 32 位导出里**没有** `...PtrW`,所以这里按指针宽度分档声明,
|
||||
/// 否则 i686 目标会在链接期失败(本仓的 Win7 冒烟是 x64,但编译不该只在一个位宽上成立)。
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
fn GetWindowLongPtrW(hwnd: isize, index: i32) -> isize;
|
||||
/// `SetWindowLongPtrW`:写窗口属性(同上,32 位映射到 `SetWindowLongW`)。
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
fn SetWindowLongPtrW(hwnd: isize, index: i32, new_value: isize) -> isize;
|
||||
|
||||
/// 32 位目标的实际符号(见上)。
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[link_name = "GetWindowLongW"]
|
||||
fn GetWindowLongPtrW(hwnd: isize, index: i32) -> i32;
|
||||
/// 32 位目标的实际符号(见上)。
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[link_name = "SetWindowLongW"]
|
||||
fn SetWindowLongPtrW(hwnd: isize, index: i32, new_value: i32) -> i32;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -127,9 +238,28 @@ fn set_layered(_hwnd: isize, _byte: u8) -> AlphaOutcome {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{alpha_byte, apply_window_alpha, AlphaOutcome, AlphaRejection, LWA_ALPHA};
|
||||
use super::{
|
||||
alpha_byte, apply_window_alpha, ensure_layered, AlphaOutcome, AlphaRejection, GWL_EXSTYLE,
|
||||
LWA_ALPHA, WS_EX_LAYERED,
|
||||
};
|
||||
use unlucky_presenter::prefs::WINDOW_ALPHA_MIN;
|
||||
|
||||
#[test]
|
||||
fn layered_constants_match_the_win32_header() {
|
||||
// 抓得住:常量写错(例如把 GWL_EXSTYLE 写成 -16 = GWL_STYLE,或 WS_EX_LAYERED 少写一个 0)。
|
||||
// 抓不住:这些值在真实窗口上是否真的生效(那要靠真机日志,见 app 侧)。
|
||||
assert_eq!(WS_EX_LAYERED, 0x0008_0000);
|
||||
assert_eq!(GWL_EXSTYLE, -20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_layered_refuses_a_missing_or_invalid_window() {
|
||||
// 句柄 0 = 还没拿到窗口:不许「假设成功」。
|
||||
assert!(!ensure_layered(0), "句柄 0 必须返回 false");
|
||||
// 1 号句柄永远无效:读不到扩展样式,也就加不上这一位。
|
||||
assert!(!ensure_layered(1), "无效句柄必须返回 false(不 panic)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alpha_byte_quantizes_upward_never_below_the_floor() {
|
||||
assert_eq!(alpha_byte(1.0), 255);
|
||||
|
||||
Reference in new issue
Block a user