fix(app-iced): M116 r4/r5 —— 产品窗口不是 WS_POPUP(真凶是整窗 α 的 layered)+ 裁定 A 落地

r4 复审 p1(证据链前提错误)+ 裁定 A(D47:整窗透明度优先于 DWM 圆角)。

## 事实订正(四处错前提 + 两处残留断言)
- `probe.rs`:删掉「winit 在 decorations=false 下给 WS_POPUP」与「popup+layered = 本产品
  窗口的完整形状」。产品样式按 winit `window_state.rs::to_window_styles` 的来源写成
  `PRODUCT_STYLE`(`WS_CAPTION|WS_BORDER|WS_CLIPSIBLINGS|WS_SYSMENU|WS_THICKFRAME|
  WS_MINIMIZEBOX|WS_MAXIMIZEBOX` = `0x04CF0000`,**不含 WS_POPUP**),并新增
  `product` / `product+alpha` 两行(五行实验)。裸 popup 行改名为 `probe:popup*`,
  明确标注「对照,不是产品形状」。
- `corner.rs`:平台矩阵第 ③ 条改成「调整带只给带 WS_CAPTION|WS_THICKFRAME 的窗口;
  裸 popup 假形状上的 `+0,+0` 不能解释产品」;补第 ④ 条「整窗 α 会关掉圆角(裁定 A)」
  并说明日志里 `pref=` 有值即证明请求确实发出。
- `app/update/appearance.rs` 与 `transparency/{dwm.rs,transparency.rs}` 的注释:把「第一嫌疑是
  无边框 WS_POPUP」改成「整窗 α 引入的 WS_EX_LAYERED」;`frame=` 的真进程取值写明是
  `overlapped`(关 min/max 则 `other`),**永远不会是 popup**。
- 实测复核(`m116_corner_controlled_experiment`,本机 Server 2025 / RDP 断开):
  `product` 行 `Δframe = +0,-7`(与 `decorated` 同格),`product+alpha` 也是 `+0,-7`
  ⇒ 分层位不改变那条带子,`Δframe` 不能解释圆角失效。

## 裁定 A 落地
- 日志新增中立观测栏 `suppressed_by=window_alpha`(`format_line` 多一个参数;进 `CornerApplied`
  指纹,α 1.0↔<1 各触发一次重下/一行日志)。只陈述「分层位来自我们的整窗 α」。
- 设置页:外观说明句写明「开启整窗透明度时 Windows 不会给窗口圆角,回到 100% 自动恢复」;
  新增 `SettingsAppearanceCornerSuspended` 一档,仅在「跟随系统 + α<1」时作为**当前状态提示**
  出现;档位本身照常显示用户选的值(不偷偷改成「直角」)。中英双份 + golden。
- 未动:per-pixel α、D36 的 LWA_ALPHA 实现、α 与圆角的优先级。

## 顺带
- `presenter/prefs.rs` 的 `AppearanceBase` 文档「三个键/只读」→「四个键」(r4 P3)。
- 证据目录(DevDocs/artifacts/m116/{README.md,probe-table.md})按上面重写:五行真值表原样贴出、
  产品行标明「跑出来的不是手写的」、旧的「无边框 ⇒ 没有边框带」结论改为「只在假形状上成立,
  产品不用这个形状」。

门禁:check 0 / fmt --check 0 / `cargo test -p unlucky-app-iced --lib` 0(448 passed, 0 failed,
2 ignored)。ignore 探针实跑:`PROBE_RC=0`,两行产品读数见报告。
This commit is contained in:
AskaEth committed 2026-11-05 06:35:19 +08:00
1 parent bc3a2b56a4
commit abc51a957b
11 files changed
+246 -71

No files matched your search

+15 -5
View File
@@ -17,7 +17,13 @@
//!
//! 用户报的现象是「别的窗口都有圆角,只有我们的没有」。没有这一行,事后无法区分
//! 「没生效」「生效了但看不见」「这台系统根本没有这个能力」;`frame` / `layered` 两栏则是
//! 「我们被 DWM 排除」的第一嫌疑(`WS_POPUP` 无边框、`WS_EX_LAYERED` 整窗 α)的现场读数。
//! 「圆角为什么不生效」的两个现场读数:`frame=`(窗口形状)与 `layered=`(整窗 α 引入的分层位)。
//!
//! ⚠️ **产品窗口不是 `WS_POPUP`**(M116 r4 复审订正):winit 在 `decorations = false` 下给的是
//! `WS_CAPTION|WS_BORDER|WS_CLIPSIBLINGS|WS_SYSMENU`(可缩放时再加 `WS_THICKFRAME` 与 min/max 键),
//! `WS_POPUP` 只在**带 owner** 的弹出窗才加;无边框观感来自 `WM_NCCALCSIZE` 把客户区铺满。
//! 所以真进程日志里 `frame=` 应是 `overlapped`(关掉 min/max 则 `other`),**永远不会是 `popup`**。
//! 真正让系统忽略圆角请求的是**整窗 α 补的 `WS_EX_LAYERED`**(`layered=on`)。
//!
//! `radius=system` 是刻意的:系统不暴露 DWM 的圆角半径,我们也不自己定一个数字
//! (M116 取消了 8/12/16 固定档)。
@@ -47,15 +53,19 @@ impl App {
platform::current_windows_version(),
self.appearance.backend,
);
let next = CornerApplied::new(policy, &plan, self.hwnd.is_some());
// 整窗 α < 1 ⇒ `window_alpha::dispatch` 会给窗口补 `WS_EX_LAYERED`;系统因此忽略圆角请求
// (tower 裁定 A / D47)。这里只记**事实**:α 是否把我们自己的分层位打开了。
let alpha_layered = self.appearance.window_opacity < 1.0;
let suppressed_by = alpha_layered.then_some("window_alpha");
let next = CornerApplied::new(policy, &plan, self.hwnd.is_some(), alpha_layered);
if !corner::needs_redispatch(self.corner_applied.as_ref(), &next) {
return;
}
self.corner_applied = Some(next);
// 行的形状只有一处(`corner::format_line`),因为那里能在没有窗口的环境里被单测钉住。
// 「我们被 DWM 排除」的第一嫌疑是无边框(`WS_POPUP`)与整窗 α(`WS_EX_LAYERED`),
// 所以 `frame=` / `layered=` 必须和圆角结论在同一行——否则以后只能猜是哪一条。
// 关键的三栏必须在同一行:`pref=`(请求发出去了)、`layered=`(分层位在不在,来自
// 整窗 α)、`frame=`(窗口形状 = `overlapped`,不是 `popup`)——否则以后只能猜是哪一条。
let (outcome, style) = match self.hwnd {
Some(hwnd) => (apply_corner(hwnd, &plan).describe(), style_summary(hwnd)),
None => (
@@ -65,7 +75,7 @@ impl App {
};
eprintln!(
"{}",
corner::format_line(context, &plan, policy, &outcome, &style)
corner::format_line(context, &plan, policy, &outcome, &style, suppressed_by)
);
}
}
@@ -68,9 +68,7 @@ pub fn section<'a>(ui: &'a Ui, state: &'a State) -> Element<'a, Message> {
),
));
// ② 窗口圆角(M116):**只有两档**,且各平台能力不同 ⇒ 一行控件 + 一句人话说明。
// 不做 8/12/16 档:系统不暴露圆角半径,自绘圆角在无边框 + 整窗 α 上要裁内容层
// (会把标题栏三键与缩放热区一起裁掉)。文案不许暗示所有平台一致。
// ② 窗口圆角(M116):只有两档 + 一句按平台说明;不做 8/12/16 档(系统不暴露半径)。
let corner = state.window_corner();
rows = rows.push(choice_row(
ui,
@@ -90,11 +88,14 @@ pub fn section<'a>(ui: &'a Ui, state: &'a State) -> Element<'a, Message> {
),
],
));
rows = rows.push(note_line(
ui,
t(Key::SettingsAppearanceCornerHint),
ui.color(Role::OnSurfaceVariant),
));
// 档位照常显示用户选的值(不偷偷改成「直角」);α<1 时只补一句状态(裁定 A)。
let mut notes = vec![t(Key::SettingsAppearanceCornerHint)];
if corner == WindowCorner::System && state.window_opacity < 1.0 {
notes.push(t(Key::SettingsAppearanceCornerSuspended));
}
for note in notes {
rows = rows.push(note_line(ui, note, ui.color(Role::OnSurfaceVariant)));
}
// ③ 面板透明度(CPU 档强制 1.0)
let forced = state.panel_is_forced();
+43 -8
View File
@@ -19,10 +19,21 @@
//! | Linux X11 / Wayland | 不承诺,直角即如实结果 | **推断**(未真机验) |
//!
//! ⚠️ **「Win11 上我们的窗口一定有圆角、边缘平滑」这句话本分支的证据不支持**,别写回去。
//! 本机(Server 2025 build 26100、RDP 会话断开)只测到三件事:
//! ① `DwmSetWindowAttribute(DWMWA_WINDOW_CORNER_PREFERENCE)` 被接受;
//! 本机(Server 2025 build 26100、RDP 会话断开)测到的是:
//! ① `DwmSetWindowAttribute(DWMWA_WINDOW_CORNER_PREFERENCE)` 被接受(三种窗口样式都接受);
//! ② `DwmGetWindowAttribute` 同一属性返回 `E_INVALIDARG`(本会话不支持回读,与窗口形状无关);
//! ③ 无边框窗口没有 DWM 调整带(`EXTENDED_FRAME_BOUNDS − GetWindowRect` = `+0,+0`,有边框是 `+0,−7`)。
//! ③ DWM 的调整带只给**有 `WS_CAPTION|WS_THICKFRAME` 的窗口**:`EXTENDED_FRAME_BOUNDS −
//! GetWindowRect` 在有边框窗口上是 `+0,−7`,在**裸 `WS_POPUP` 假形状**上是 `+0,+0`。
//!
//! ⚠️ **第 ③ 条不能拿裸 popup 的读数去解释产品窗口**(M116 r4 复审订正):产品窗口
//! **不是** `WS_POPUP`(winit 只在带 owner 的弹出窗才加它;无边框观感来自 `WM_NCCALCSIZE`),
//! 它带 `WS_CAPTION|WS_THICKFRAME` ⇒ 按同一张表应得 `+0,−7`,与 `decorated` 同族。
//! 探针里 `product` / `product+alpha` 两行量的就是这个真实形状。
//!
//! ④ **整窗 α 会关掉圆角**(tower 裁定 A / D47:整窗透明度优先于 DWM 圆角):α < 1 时我们补
//! `WS_EX_LAYERED`,此时系统会忽略圆角请求——请求照样发出(日志里 `pref=` 有值),
//! 只是不生效;α 回到 1.0 就恢复。日志用 `suppressed_by=window_alpha` 记这一条**观测事实**。
//!
//! 「圆角长什么样」归真机验收,证据目录见 `DevDocs/artifacts/m116/`。
//!
//! 后两行在本 crate 里落到同一格:探测不到 Windows 版本 ⇒ `UnknownOs` ⇒ 直角。
@@ -183,7 +194,9 @@ pub fn plan_corner(
/// 产品日志行的**唯一格式化落点**(纯函数:把四段文本拼成一个固定形状)。
///
/// ```text
/// [corner] <context> corner=policy=system source=dwm os=10.0.26100 excluded_reason=none /// radius=system pref=2 readback=? read_hr=0x80070057 frame=popup layered=on
/// [corner] <context> corner=policy=system source=dwm os=10.0.26100 excluded_reason=none \
/// radius=system pref=2 readback=? read_hr=0x80070057 frame=overlapped layered=on \
/// suppressed_by=window_alpha
/// ```
///
/// 为什么单独拎出来:`App::apply_window_corner` 要真句柄、真 DWM 才能跑,而**这一行的形状**
@@ -192,6 +205,10 @@ pub fn plan_corner(
///
/// 参数里 `outcome` / `style` 由调用方给(`CornerOutcome::describe()` / `style_summary(hwnd)`),
/// 本文件不依赖 FFI 层,因此可以在非 Windows 目标上编、也能被单测覆盖。
///
/// `suppressed_by` 是**观测事实**(tower 裁定 A / D47 要求的那一栏):整窗 α < 1 时我们补了
/// `WS_EX_LAYERED`,此时系统忽略圆角请求。配合同一行里的 `pref=`(请求确实发出去了)与
/// `layered=`(分层位确实在),读者能自己推出「不是我们没发请求」——本函数不写结论性措辞。
#[must_use]
pub fn format_line(
context: &str,
@@ -199,11 +216,18 @@ pub fn format_line(
policy: WindowCorner,
outcome: &str,
style: &str,
suppressed_by: Option<&'static str>,
) -> String {
format!(
let mut line = format!(
"[corner] {context} {} {outcome} {style}",
plan.log_line(policy)
)
);
if let Some(reason) = suppressed_by {
line.push(' ');
line.push_str("suppressed_by=");
line.push_str(reason);
}
line
}
/// 一次「已经下发过」的**指纹**:只由判定本身与「当时有没有窗口」组成,不含读数。
@@ -224,17 +248,28 @@ pub struct CornerApplied {
pub excluded: Option<CornerExclusion>,
/// 下发时**有没有窗口句柄**:`false → true` 必须重下(首次拿到 HWND 时才真能设属性)。
pub has_window: bool,
/// 整窗 α 是否 < 1(= 我们补了 `WS_EX_LAYERED`,系统会忽略圆角请求)。
///
/// 进指纹是为了让「α 从 1.0 拉低 / 拉回 1.0」各触发一次重下与一行日志——那两次的
/// 系统行为确实不同,不重记就看不出来。
pub suppressed_by_window_alpha: bool,
}
impl CornerApplied {
/// 由判定与「有没有句柄」构造指纹。
/// 由判定 + 「有没有句柄」+ 「整窗 α 是否补了分层」构造指纹。
#[must_use]
pub const fn new(policy: WindowCorner, plan: &CornerPlan, has_window: bool) -> Self {
pub const fn new(
policy: WindowCorner,
plan: &CornerPlan,
has_window: bool,
suppressed_by_window_alpha: bool,
) -> Self {
Self {
policy,
source: plan.source,
excluded: plan.excluded,
has_window,
suppressed_by_window_alpha,
}
}
}
@@ -162,7 +162,8 @@ fn the_log_line_carries_every_field() {
/// 抓不住:产品进程里那一行到底长什么样(本 mission 没构建 bin;合并态由 tower 编 `--bins` 补)。
#[test]
fn the_log_line_shape_is_pinned_with_the_measured_readings() {
// 本机实测:Server 2025 / build 26100;读回 `E_INVALIDARG`;popup 窗口 layered=on。
// 本机实测:Server 2025 / build 26100;读回 `E_INVALIDARG`;产品形状是 `overlapped`,
// 而 `layered=on` 只可能来自整窗 α(我们的 `WS_EX_LAYERED`) ⇒ 带 `suppressed_by=`。
let os = WindowsVersion::new(10, 0, 26_100);
let plan: CornerPlan = plan_corner(WindowCorner::System, Some(os), Backend::Gpu);
let line = format_line(
@@ -170,14 +171,16 @@ fn the_log_line_shape_is_pinned_with_the_measured_readings() {
&plan,
WindowCorner::System,
"pref=2 readback=? read_hr=0x80070057",
"frame=popup layered=on",
"frame=overlapped layered=on",
Some("window_alpha"),
);
assert_eq!(
line,
concat!(
"[corner] boot corner=policy=system source=dwm os=10.0.26100 ",
"excluded_reason=none radius=system pref=2 readback=? ",
"read_hr=0x80070057 frame=popup layered=on"
"read_hr=0x80070057 frame=overlapped layered=on ",
"suppressed_by=window_alpha"
)
);
@@ -187,14 +190,17 @@ fn the_log_line_shape_is_pinned_with_the_measured_readings() {
&plan_corner(WindowCorner::Square, Some(os), Backend::Gpu),
WindowCorner::Square,
"pref=1 readback=? read_hr=0x80070057",
"frame=popup layered=off",
"frame=overlapped layered=off",
None,
);
assert!(
square.contains("corner=policy=square source=square"),
"{square}"
);
assert!(square.contains("excluded_reason=user"), "{square}");
assert!(square.contains("frame=popup layered=off"), "{square}");
assert!(square.contains("frame=overlapped layered=off"), "{square}");
// α = 1(不补分层)时**不许**出现 `suppressed_by=`:那一栏只在整窗 α 打开时才有意义。
assert!(!square.contains("suppressed_by"), "{square}");
// Win10:原因码是 `os-build`(GPU 档)/ `cpu-tier`(CPU 档),来源仍是 square。
let win10 = WindowsVersion::new(10, 0, 19_045);
@@ -239,7 +245,7 @@ fn the_same_policy_is_dispatched_only_once() {
let mut last: Option<CornerApplied> = None;
let mut dispatches = 0;
for _ in 0..20 {
let next = CornerApplied::new(WindowCorner::System, &plan, true);
let next = CornerApplied::new(WindowCorner::System, &plan, true, false);
if needs_redispatch(last.as_ref(), &next) {
dispatches += 1;
last = Some(next);
@@ -251,16 +257,32 @@ fn the_same_policy_is_dispatched_only_once() {
let square = plan_corner(WindowCorner::Square, Some(os), Backend::Gpu);
assert!(needs_redispatch(
last.as_ref(),
&CornerApplied::new(WindowCorner::Square, &square, true)
&CornerApplied::new(WindowCorner::Square, &square, true, false)
));
// 句柄从无到有必须重下(首次拿到 HWND 时才真能设属性)。
let mut no_window: Option<CornerApplied> = None;
let first = CornerApplied::new(WindowCorner::System, &plan, false);
let first = CornerApplied::new(WindowCorner::System, &plan, false, false);
assert!(needs_redispatch(no_window.as_ref(), &first));
no_window = Some(first);
assert!(needs_redispatch(
no_window.as_ref(),
&CornerApplied::new(WindowCorner::System, &plan, true)
&CornerApplied::new(WindowCorner::System, &plan, true, false)
));
// 整窗 α 拉低(补分层)与拉回 1.0 都必须各重下/各记一行:系统行为在这两次确实不同。
let opaque = CornerApplied::new(WindowCorner::System, &plan, true, false);
let translucent = CornerApplied::new(WindowCorner::System, &plan, true, true);
assert!(
needs_redispatch(Some(&opaque), &translucent),
"α 从 1.0 拉低必须重下(那一行才会出现 suppressed_by=window_alpha)"
);
assert!(
needs_redispatch(Some(&translucent), &opaque),
"α 拉回 1.0 也必须重下(圆角请求重新生效)"
);
assert!(
!needs_redispatch(Some(&translucent), &translucent),
"α 一直在 <1 时重复触发不该刷屏"
);
}
@@ -353,7 +353,8 @@ pub(super) fn destroy_probe_window(hwnd: isize) {
}
}
/// 读窗口样式位(`GWL_STYLE` = `-16`)。探针用它证明「无边框 = `WS_POPUP`」这条前提。
/// 读窗口样式位(`GWL_STYLE` = `-16`)。探针用它取 `frame=` 那一栏(产品的真实样式见
/// `probe::PRODUCT_STYLE`,**不是** `WS_POPUP`)。
#[cfg(target_os = "windows")]
pub(super) fn window_style(hwnd: isize) -> isize {
const GWL_STYLE: i32 = -16;
@@ -206,7 +206,11 @@ pub fn window_rect(hwnd: isize) -> Option<(i32, i32, i32, i32)> {
}
}
/// 读窗口样式位(`GWL_STYLE` = `-16`):证明「无边框 = `WS_POPUP`」这条前提。
/// 读窗口样式位(`GWL_STYLE` = `-16`):日志里 `frame=` 那一栏的取值来源。
///
/// ⚠️ 产品窗口**不是** `WS_POPUP`(M116 r4 复审订正):winit 在 `decorations = false` 下给的是
/// `WS_CAPTION|WS_BORDER|...`(见 `probe::PRODUCT_STYLE`),真进程的 `frame=` 应为 `overlapped`
/// (关掉 min/max 则 `other`)。「无边框」是 `WM_NCCALCSIZE` 的效果,不是样式位。
#[must_use]
pub fn window_style(hwnd: isize) -> Option<isize> {
#[cfg(target_os = "windows")]
@@ -234,14 +238,14 @@ pub fn window_ex_style(hwnd: isize) -> Option<isize> {
}
}
/// `WS_POPUP` 位:无边框窗口(winit 在 `decorations = false` 下给的形状)。
/// `WS_POPUP` 位(`0x8000_0000`):**带 owner 的弹出窗**才置它;产品主窗口不用这一位。
pub const WS_POPUP: isize = 0x8000_0000_u32 as isize;
/// 帧样式的一句话标签(纯函数,日志用):`popup` / `overlapped` / `other`。
///
/// 为什么要它:用户的现象是「别的窗口有圆角,我们的没有」,而**我们被排除的第一嫌疑**就是
/// `WS_POPUP`(无边框)。把它打进日志,事后才能把「这台机器的 DWM 对 popup 窗口不套圆角」
/// 与「属性没设上」分开。
/// 为什么要它:用户的现象是「别的窗口有圆角,我们的没有」。把它打进日志,才能一眼看出
/// 窗口形状这一项**不是**变量(产品恒为 `overlapped`)——真正让系统忽略圆角请求的是整窗 α
/// 引入的 `WS_EX_LAYERED`(见 `layered=` 与 `suppressed_by=window_alpha`),不是 `popup` 形状。
#[must_use]
pub const fn frame_label(style: isize) -> &'static str {
const WS_OVERLAPPEDWINDOW_MASK: isize = 0x00CF_0000;
@@ -1,24 +1,37 @@
//! M116 的**受控对照实验**:同进程里造三种窗口样式,逐个读 DWM 属性。
//! M116 的**受控对照实验**:同进程里造几种窗口样式,逐个读 DWM 属性。
//!
//! ## 为什么需要它
//!
//! 用户的观察是「同一台机器上别的窗口都有圆角,只有我们的没有」。于是「没圆角」至少有三种
//! 可能:①这是 DWM 的默认行为(我们没申请);②我们用什么样式把它 opt-out 了
//! (`decorations=false` ⇒ `WS_POPUP`,以及 D36 的整窗 `WS_EX_LAYERED`);③属性根本设不上。
//! 可能:①这是 DWM 的默认行为(我们没申请);②某个**样式位**把它 opt-out 了;③属性根本设不上。
//!
//! 只在自己的窗口上读一次,分不清 ①②③。所以这里**同一进程**里造三个窗口,除「样式」外
//! 只在自己的窗口上读一次,分不清 ①②③。所以这里**同一进程**里造几个窗口,除「样式」外
//! 条件全相同:
//!
//! | 标签 | 样式 | 对应现实 |
//! | 标签 | 样式 | 它代表什么 |
//! |---|---|---|
//! | `decorated` | `WS_OVERLAPPEDWINDOW` | 用户说的「别的窗口」——Win11 上就是圆的 |
//! | `popup` | `WS_POPUP` | winit 在 `decorations = false` 下给的样式 |
//! | `popup+layered` | `WS_POPUP` + `WS_EX_LAYERED` + `LWA_ALPHA 205` | 本产品窗口的完整形状(D36) |
//! | `decorated` | `WS_OVERLAPPEDWINDOW` | 用户说的「别的窗口」(对照) |
//! | `probe:popup` | 裸 `WS_POPUP` | **对照,不是产品形状**:带 owner 的弹出窗才用这一位 |
//! | `probe:popup+layered` | `WS_POPUP` + `WS_EX_LAYERED`(α) | **对照**:把两个嫌疑同时打开 |
//! | `product` | [`PRODUCT_STYLE`](α = 1,不补分层) | 产品的**真实**样式(出厂态) |
//! | `product+alpha` | [`PRODUCT_STYLE`] + `WS_EX_LAYERED`(α) | 产品真实样式 + 用户把整窗 α 拉低 |
//!
//! ## ⚠️ 产品窗口**不是** `WS_POPUP`(M116 r4 复审订正)
//!
//! iced/winit 在 `decorations = false` 下**不会**用 `WS_POPUP`:winit 的
//! `window_state.rs::to_window_styles` 起点是 `WS_CAPTION|WS_BORDER|WS_CLIPSIBLINGS|WS_SYSMENU`,
//! 可缩放/可最大化时再加 `WS_THICKFRAME|WS_MINIMIZEBOX|WS_MAXIMIZEBOX`;`WS_POPUP` 只在
//! `WindowFlags::POPUP`(**带 owner** 的弹出窗)时才加。产品的「无边框观感」来自
//! `WM_NCCALCSIZE` 把客户区铺满,**不是**样式位。
//!
//! 所以本文件早期那句「`popup+layered` = 本产品窗口的完整形状」是**错的**:那一行的读数
//! (尤其 `Δframe = +0,+0`)只属于假形状,不能拿去解释产品窗口。要解释产品,看 `product`
//! 与 `product+alpha` 两行。
//!
//! ## 这个实验能证明什么、不能证明什么
//!
//! **能**:属性层能不能开(`DwmSetWindowAttribute` 的 `HRESULT`)、开完 DWM 侧回读是多少、
//! 三种样式之间 `DWMWA_VISIBLE_FRAME_BORDER_THICKNESS` 与
//! 各样式之间 `DWMWA_VISIBLE_FRAME_BORDER_THICKNESS` 与
//! `DWMWA_EXTENDED_FRAME_BOUNDS - GetWindowRect` 的数值差是否随样式变化。
//!
//! **不能**:角上到底是不是透明的(那要合成输出像素,本机 RDP 断连拿不到)。所以本文件
@@ -31,10 +44,43 @@ use super::dwm::{
window_rect, window_style, CornerOutcome, DWMWCP_DONOTROUND, DWMWCP_ROUND,
};
/// `WS_POPUP`:无边框弹出窗口(winit 在 `decorations = false` 下用它)。
/// `WS_POPUP`(`0x8000_0000`):**带 owner 的弹出窗**才用它;产品主窗口没有 owner,不用。
pub const WS_POPUP: u32 = 0x8000_0000;
/// `WS_OVERLAPPEDWINDOW`:普通有边框窗口(对照组的「别的窗口」)。
pub const WS_OVERLAPPEDWINDOW: u32 = 0x00CF_0000;
/// `WS_CAPTION`(`WS_BORDER | WS_DLGFRAME`)。
const WS_CAPTION: u32 = 0x00C0_0000;
/// `WS_BORDER`(= `WS_THICKFRAME` 的低位别名,`0x0080_0000`)。
const WS_BORDER: u32 = 0x0080_0000;
/// `WS_CLIPSIBLINGS`:子窗口层叠顺序位(DWM 不看它,winit 的起点里有)。
const WS_CLIPSIBLINGS: u32 = 0x0400_0000;
/// `WS_SYSMENU`。
const WS_SYSMENU: u32 = 0x0008_0000;
/// `WS_THICKFRAME`(可缩放边框)。
const WS_THICKFRAME: u32 = 0x0004_0000;
/// `WS_MINIMIZEBOX`。
const WS_MINIMIZEBOX: u32 = 0x0002_0000;
/// `WS_MAXIMIZEBOX`。
const WS_MAXIMIZEBOX: u32 = 0x0001_0000;
/// **产品窗口的真实样式位**(iced/winit,`decorations = false`,可缩放、可最大化、可最小化)。
///
/// 出处(可复核):winit `window_state.rs::to_window_styles` 从
/// `WS_CAPTION|WS_BORDER|WS_CLIPSIBLINGS|WS_SYSMENU` 起手,`RESIZABLE` / `MAXIMIZABLE` /
/// `MINIMIZABLE` 分别加 `WS_THICKFRAME` / `WS_MAXIMIZEBOX` / `WS_MINIMIZEBOX`;`WS_POPUP`
/// 只在 `WindowFlags::POPUP`(带 owner)时才加——产品主窗口不带。
///
/// 这些**位**是输入(照抄 winit 的产物,带出处);表里那一行 `product` 的**读数**是跑出来的,
/// 不是我写死的。
pub const PRODUCT_STYLE: u32 = WS_CAPTION
| WS_BORDER
| WS_CLIPSIBLINGS
| WS_SYSMENU
| WS_THICKFRAME
| WS_MINIMIZEBOX
| WS_MAXIMIZEBOX;
/// `WS_EX_LAYERED` 的 `u32` 形态(`create_probe_window` 的参数类型)。
const WS_EX_LAYERED_U32: u32 = 0x0008_0000;
@@ -112,14 +158,19 @@ pub const TABLE_HEADER: &str = "| style | requested | exstyle | readback | pref
/// 跑一遍受控对照实验(非 Windows 返回空表)。
///
/// 前两行是对照(**不是产品形状**),后两行才是产品:`product` = 出厂态(α = 1,不补分层),
/// `product+alpha` = 用户把整窗 α 拉低之后的真实形状。
///
/// 每个窗口用完立刻销毁:这是诊断路径,不许留 HWND。
#[must_use]
pub fn controlled_experiment() -> Vec<ProbeRow> {
let mut rows = Vec::new();
for (label, style, ex_style) in [
("decorated", WS_OVERLAPPEDWINDOW, 0u32),
("popup", WS_POPUP, 0u32),
("popup+layered", WS_POPUP, WS_EX_LAYERED_U32),
("probe:popup", WS_POPUP, 0u32),
("probe:popup+layered", WS_POPUP, WS_EX_LAYERED_U32),
("product", PRODUCT_STYLE, 0u32),
("product+alpha", PRODUCT_STYLE, WS_EX_LAYERED_U32),
] {
rows.push(run_one(label, style, ex_style));
}
@@ -204,7 +255,10 @@ fn empty_row(label: &'static str, style: u32, ex_style: u32) -> ProbeRow {
#[cfg(test)]
mod tests {
use super::{controlled_experiment, ProbeRow, TABLE_HEADER, WS_OVERLAPPEDWINDOW, WS_POPUP};
use super::super::dwm::frame_label;
use super::{
controlled_experiment, ProbeRow, PRODUCT_STYLE, TABLE_HEADER, WS_OVERLAPPEDWINDOW, WS_POPUP,
};
/// **抓得住**:样式常量写错(`WS_POPUP` 少一个 0 就变成别的位,对照实验的前提就没了)。
#[test]
@@ -213,19 +267,57 @@ mod tests {
assert_eq!(WS_OVERLAPPEDWINDOW, 0x00CF_0000);
}
/// 实验永远给出三行,顺序固定(`decorated` → `popup` → `popup+layered`)。
/// **产品窗口不是 popup**(M116 r4 复审的核心事实,钉住它,别再写回去)。
///
/// 抓得住:有人把分层那一行删了/调换——那样「layered 会不会被 DWM 排除」这条对照就没了。
/// 抓得住:有人把 `PRODUCT_STYLE` 改回 `WS_POPUP`(那会让「产品形状」整条证据链回到错的假设上);
/// 也抓得住 `PRODUCT_STYLE` 丢掉 `WS_CAPTION`/`WS_THICKFRAME`(那会让 `Δframe` 从 `+0,−7`
/// 掉成 `+0,+0`,把「产品没有边框带」这个错结论复活)。
/// 抓不住:真进程上 winit 到底置了哪些位(那看 `[corner] … frame=` 的运行时读数)。
#[test]
fn the_product_window_is_overlapped_not_popup() {
assert_eq!(
PRODUCT_STYLE & WS_POPUP,
0,
"产品主窗口没有 owner ⇒ 不带 WS_POPUP"
);
assert_ne!(
PRODUCT_STYLE & 0x00C0_0000,
0,
"WS_CAPTION 在(winit 起点)"
);
assert_ne!(PRODUCT_STYLE & 0x0004_0000, 0, "WS_THICKFRAME 在(可缩放)");
assert_eq!(frame_label(PRODUCT_STYLE as isize), "overlapped");
assert_eq!(frame_label(WS_POPUP as isize), "popup");
}
/// 实验固定五行,顺序固定:两行对照 + 两行产品 + `decorated` 参照。
///
/// 抓得住:有人把分层那一行或产品行删了/调换——那样「layered 会不会让 DWM 忽略圆角请求」
/// 与「产品真实形状有没有边框带」这两条对照就都没了。
/// 抓不住:读数本身(那要么真机跑,要么看报告里的表)。
#[test]
fn the_experiment_always_covers_three_styles_in_order() {
fn the_experiment_always_covers_the_same_five_rows_in_order() {
let rows = controlled_experiment();
assert_eq!(rows.len(), 3);
assert_eq!(rows.len(), 5);
let labels: Vec<_> = rows.iter().map(|row| row.label).collect();
assert_eq!(labels, ["decorated", "popup", "popup+layered"]);
assert_eq!(
labels,
[
"decorated",
"probe:popup",
"probe:popup+layered",
"product",
"product+alpha"
]
);
assert_eq!(rows[0].style, WS_OVERLAPPEDWINDOW);
assert_eq!(rows[1].style, WS_POPUP);
assert_eq!(rows[2].ex_style & 0x0008_0000, 0x0008_0000);
// 产品两行:样式位相同(都是真实样式),只有 layered 一位不同。
assert_eq!(rows[3].style, PRODUCT_STYLE);
assert_eq!(rows[4].style, PRODUCT_STYLE);
assert_eq!(rows[3].ex_style & 0x0008_0000, 0, "出厂态 α=1 ⇒ 不补分层");
assert_eq!(rows[4].ex_style & 0x0008_0000, 0x0008_0000, "α<1 ⇒ 补分层");
}
/// 表头与每行的竖线数一致(`format!` 里多/少一列会让表格在报告里错位)。
@@ -243,7 +335,7 @@ mod tests {
// 没造出窗口的行必须**没有**读数(不许用 0 冒充)。
let failed = ProbeRow {
created: false,
..controlled_experiment().into_iter().next().expect("三行")
..controlled_experiment().into_iter().next().expect("五行")
};
assert!(failed.describe().contains("FAILED"));
}
@@ -199,22 +199,27 @@ fn m116_corner_log_line_on_real_windows() {
let os = crate::platform::current_windows_version();
let plan = plan_corner(WindowCorner::System, os, crate::Backend::Gpu);
for (label, style, ex_style) in [
("decorated", super::probe::WS_OVERLAPPEDWINDOW, 0u32),
("popup", super::probe::WS_POPUP, 0u32),
("popup+layered", super::probe::WS_POPUP, 0x0008_0000),
] {
let hwnd = super::create_probe_window(style, ex_style);
// 直接遍历 `controlled_experiment` 的**同一份**行定义(含 `product` / `product+alpha`),
// 免得这里另抄一份样式表、跟探针悄悄分叉。
for row in super::controlled_experiment() {
let hwnd = super::create_probe_window(row.style, row.ex_style);
if hwnd == 0 {
println!("[corner] {label} window=none");
println!("[corner] {} window=none", row.label);
continue;
}
super::show_probe_window(hwnd);
// layered=on 的那几行(产品的 α<1 档与裸 popup 对照)按裁定 A 带 `suppressed_by=`。
let layered = row.ex_style & 0x0008_0000 != 0;
println!(
"[corner] {label} {} {} {}",
plan.log_line(WindowCorner::System),
super::apply_corner(hwnd, &plan).describe(),
super::dwm::style_summary(hwnd),
"{}",
crate::platform::win32::corner::format_line(
row.label,
&plan,
WindowCorner::System,
&super::apply_corner(hwnd, &plan).describe(),
&super::dwm::style_summary(hwnd),
layered.then_some("window_alpha"),
)
);
super::destroy_probe_window(hwnd);
}