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:
1 parent
bc3a2b56a4
commit
abc51a957b
11 files changed
+246
-71
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user