两条红的成因都是「Windows-only 的东西没有平台门」:Linux + `--all-targets` + `-D warnings`
三件事凑齐时,Windows 专有的 DWM/穿透代码在 Linux 上变成未用导入 + 死代码。
clippy(14 条错,docker 内 exit 101 → 0):
- dwm.rs `set_preference` 的 `#[cfg(not(windows))]` 分支以 `return` 结尾 ⇒ needless_return;
那个块本来就是该 cfg 下函数体的尾巴,去掉 `return`。
- dwm.rs 三常量 + 四读函数在非 Windows 上没有使用者(唯一调用方 `probe::run_one` 的
Windows 版被门着,`transparency.rs` 只 re-export 三项)⇒ dead_code。用**模块级**
`#![cfg_attr(not(target_os = "windows"), allow(dead_code))]`:
· 逐项 `cfg(target_os = "windows")` 会把它们从 Linux 构建里删掉,本文件那几条与平台
无关的单测(属性号照 Win32 头文件、无效句柄不许表现成成功)就连编译都过不了;
· 逐项 `cfg_attr` 要多 7 行,本文件 394 行 → 会顶破 guards.yml 的 400 行红线(现 398 行)。
代价写明在文件头:非 Windows 上本模块的 dead_code 不再报。
- exstyle.rs `use super::raw_ex_style`:唯一使用者 `click_through` 自己按平台门着 ⇒ 补 cfg。
- probe.rs 的 `use super::dwm::{…}`:`CornerOutcome` 与平台无关(`ProbeRow` 字段 + 非 Windows
的 `empty_row`),其余九个符号只服务于 Windows 的 `run_one` ⇒ 拆成两条 use。
- window_alpha.rs `use …RawWindowHandle`:只有 `RawWindowHandle::Win32` 那条臂用它 ⇒ 补 cfg。
test(docker 内 622 passed / 1 failed → 623 passed / 0 failed):
`opaque_or_refused_alpha_never_touches_the_window_style` 里「无效句柄 ⇒ Failed」是按 Windows
语义写死的;非 Windows 走 `transparency::set_layered` 的空实现,如实返回 `Unsupported`。
按平台拆成两条断言(Windows 断 `Failed`,其它断 `Unsupported`),两边都不许表现成
`Applied`/`Opaque`。顺带说明:`dispatch` 的 `layered=false` 那半条断言与平台无关,保留在
两条之外照跑。
验证(本机 Windows + Docker Debian 12,命令与 ci.yml 逐字一致):
- docker `cargo clippy --workspace --all-targets -- -D warnings` → exit 0
- docker `cargo test -p unlucky-app-iced --lib` → 623 passed / 0 failed
- Windows `cargo check -p unlucky-app-iced --all-targets` → 无 error/warning
- Windows `cargo test -p unlucky-app-iced --lib` → 624 passed / 0 failed / 2 ignored
148 lines
7.0 KiB
Rust
148 lines
7.0 KiB
Rust
//! 整窗 α 的**执行侧**:句柄探测 → `WS_EX_LAYERED` → `SetLayeredWindowAttributes` → 一行日志。
|
||
//!
|
||
//! ## 为什么句柄要绕一圈 iced
|
||
//!
|
||
//! iced 0.14 只给 `window::Id`(它自己的编号),没有「给我 HWND」的公开 API;唯一的入口是
|
||
//! `window::run(id, callback)`——回调里拿到的 `&dyn Window` 实现了 `HasWindowHandle`
|
||
//! (raw-window-handle 0.6),于是能取到 `RawWindowHandle::Win32(hwnd)`。
|
||
//!
|
||
//! ## 三层责任(顺序不能换)
|
||
//!
|
||
//! 1. **探测句柄**:[`probe_hwnd`],纯取;拿不到就整条链跳过(并如实打日志,不许假装成功);
|
||
//! 2. **补分层样式**:`ensure_layered`(唯一 `unsafe` 落点,`platform/win32/transparency.rs`);
|
||
//! `SetLayeredWindowAttributes` 在没有 `WS_EX_LAYERED` 的窗口上**必然失败**(Win32 返回 0,
|
||
//! 本机 `GetLastError` 常见 87),所以这一步是硬前提;
|
||
//! 3. **下发 α**:`apply_window_alpha`。`α ≥ 1` 时它返回 `Opaque` 并且**不调 FFI**,那时也就不必
|
||
//! 改扩展样式(1、2 步都是白改),[`dispatch`] 里按这个条件短路。
|
||
//!
|
||
//! ## 为什么日志要记「下发字节」而不是「看起来多透明」
|
||
//!
|
||
//! 本机是 RDP + 4 个虚拟显示适配器:`LWA_ALPHA` 的**观感**在这里不可信。所以证据链是
|
||
//! `配置值 → 判据 → 字节 → Win32 返回值`,全部经 [`super::App::log_appearance`] 落成一行文本。
|
||
|
||
use iced::window;
|
||
|
||
use crate::platform::win32::transparency::{apply_window_alpha, ensure_layered, AlphaOutcome};
|
||
|
||
/// 一次「补样式 + 下发」的结果(日志与单测共用)。
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub(super) struct Dispatch {
|
||
/// `WS_EX_LAYERED` 是否就位(`α ≥ 1` 或 `α = None` 时为 `false`:那次根本没动扩展样式)。
|
||
pub layered: bool,
|
||
/// 下发的结局。
|
||
pub alpha: AlphaOutcome,
|
||
}
|
||
|
||
impl Dispatch {
|
||
/// 日志尾巴:`layered=ok outcome=Applied { byte: 205 }`。
|
||
///
|
||
/// `layered` 有三个值,刻意区分「没用到」与「失败」:
|
||
/// - `n/a`:α ≥ 1 或被拒绝 —— 那次**根本不需要**分层窗口,不是错误;
|
||
/// - `ok`:这一位在(原本就有或本条补上);
|
||
/// - `FAILED`:需要补但补不上(无效句柄等)—— 只有这一种是坏消息。
|
||
pub(super) fn describe(&self) -> String {
|
||
let layered = match self.alpha {
|
||
AlphaOutcome::Opaque | AlphaOutcome::Rejected(_) => "n/a",
|
||
AlphaOutcome::Applied { .. } | AlphaOutcome::Failed { .. } if !self.layered => "FAILED",
|
||
_ => "ok",
|
||
};
|
||
format!("layered={layered} outcome={:?}", self.alpha)
|
||
}
|
||
}
|
||
|
||
/// 把 iced 的窗口句柄翻成 Win32 的 `HWND`(`isize`)。非 Windows 平台返回 `None`。
|
||
pub(super) fn probe_hwnd(handle: &dyn window::Window) -> Option<isize> {
|
||
// 只有 `RawWindowHandle::Win32` 那条臂用到它,其余平台只剩 `_ => None`
|
||
// ⇒ 无条件导入在非 Windows 上是 `unused_imports`(M147)。
|
||
#[cfg(target_os = "windows")]
|
||
use window::raw_window_handle::RawWindowHandle;
|
||
|
||
let handle = handle.window_handle().ok()?;
|
||
match handle.as_raw() {
|
||
#[cfg(target_os = "windows")]
|
||
RawWindowHandle::Win32(win32) => Some(win32.hwnd.get()),
|
||
#[cfg(not(target_os = "windows"))]
|
||
_ => None,
|
||
#[cfg(target_os = "windows")]
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// 补样式 + 下发(**幂等**:`α ≥ 1` 时什么都不做,返回 `Opaque`)。
|
||
pub(super) fn dispatch(hwnd: isize, alpha: Option<f64>) -> Dispatch {
|
||
// α 不透明 / 被拒绝时不需要分层窗口:`SetLayeredWindowAttributes` 那次调用也不会发生,
|
||
// 改扩展样式纯属副作用(还会让 DWM 换一条合成路径),所以短路掉。
|
||
let needs_layered = alpha.is_some_and(|alpha| alpha < 1.0);
|
||
let layered = needs_layered && ensure_layered(hwnd);
|
||
Dispatch {
|
||
layered,
|
||
alpha: apply_window_alpha(hwnd, alpha),
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::dispatch;
|
||
use crate::platform::win32::transparency::{AlphaOutcome, AlphaRejection};
|
||
|
||
/// 抓得住:①`α = None` / `α ≥ 1` / `α = 0` 时**不去碰扩展样式**(短路条件写反会在这里红);
|
||
/// ②拒绝路径的返回值与 `transparency` 的判据一致。
|
||
/// 抓不住:真实窗口(有效句柄)上 `WS_EX_LAYERED` 是否真的加上了——那要靠本机日志的 `layered=ok`。
|
||
#[test]
|
||
fn opaque_or_refused_alpha_never_touches_the_window_style() {
|
||
for none in [None, Some(1.0), Some(1.5), Some(0.0), Some(-1.0)] {
|
||
let result = dispatch(1234, none);
|
||
assert!(!result.layered, "{none:?} 不该改扩展样式");
|
||
}
|
||
assert_eq!(
|
||
dispatch(1234, None).alpha,
|
||
AlphaOutcome::Rejected(AlphaRejection::NoAlpha)
|
||
);
|
||
assert_eq!(dispatch(1234, Some(1.0)).alpha, AlphaOutcome::Opaque);
|
||
// 无效句柄 + 合法 α:样式补不上(layered=false),FFI 也不会成功——两层都如实报告。
|
||
let invalid = dispatch(1, Some(0.5));
|
||
assert!(!invalid.layered, "无效句柄补不上 WS_EX_LAYERED");
|
||
// 「做不到」的语义随平台不同(M147 实证:这条断言原先按 Windows 写死,Ubuntu 上
|
||
// 622 passed / 1 failed):Windows 上 FFI 真被调用并失败 ⇒ `Failed`;非 Windows 走
|
||
// `transparency.rs` 的空实现 ⇒ `Unsupported`。两边都不许表现成 `Applied` / `Opaque`。
|
||
#[cfg(target_os = "windows")]
|
||
assert!(
|
||
matches!(invalid.alpha, AlphaOutcome::Failed { .. }),
|
||
"{:?}",
|
||
invalid.alpha
|
||
);
|
||
#[cfg(not(target_os = "windows"))]
|
||
assert_eq!(
|
||
invalid.alpha,
|
||
AlphaOutcome::Unsupported,
|
||
"非 Windows 的空实现必须如实说「这个平台不支持」,不许冒充成功"
|
||
);
|
||
}
|
||
|
||
/// 本机真窗口的取证由 `App` 启动日志给(见 `app::appearance::log_appearance`),
|
||
/// 这里只钉日志格式:`Applied { byte: 205 }` 必须能一眼读出字节,且「没用到样式」不等于失败。
|
||
#[test]
|
||
fn the_log_tail_carries_the_byte_and_distinguishes_skipped_from_failed() {
|
||
let applied = super::Dispatch {
|
||
layered: true,
|
||
alpha: AlphaOutcome::Applied { byte: 205 },
|
||
};
|
||
assert!(applied.describe().starts_with("layered=ok outcome=Applied"));
|
||
assert!(applied.describe().contains("205"));
|
||
|
||
// α = 1.0:没补样式是**正常**的,不许写成 FAILED(否则报告里会出现假红)。
|
||
let opaque = super::Dispatch {
|
||
layered: false,
|
||
alpha: AlphaOutcome::Opaque,
|
||
};
|
||
assert!(opaque.describe().starts_with("layered=n/a"));
|
||
|
||
// 真的要补却没补上:这才是 FAILED。
|
||
let failed = super::Dispatch {
|
||
layered: false,
|
||
alpha: AlphaOutcome::Failed { code: 87 },
|
||
};
|
||
assert!(failed.describe().starts_with("layered=FAILED"));
|
||
}
|
||
}
|