Merge branch 'feat/m182-maccorners-follow-compositor-rounde'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
This commit is contained in:
commit
dc3fa2be87
12 files changed
+901
-209
No files matched your search
@@ -35,6 +35,11 @@ pub(super) fn window_settings(size: (f32, f32)) -> window::Settings {
|
||||
size: iced::Size::new(size.0, size.1),
|
||||
min_size: Some(MIN_SIZE),
|
||||
// 自绘三键(M93):窗口**不许**带系统装饰,否则 macOS 上会出现原生标题栏 + 自绘三键两套。
|
||||
//
|
||||
// M182:这一行**同时**是「macOS 四角是直角」的原因——winit 见到 `decorations: false`
|
||||
// 会给出 `NSWindowStyleMask::Borderless`(没有 `Titled`),而窗口服务器只给 titled 窗
|
||||
// 做圆角裁剪。链条与「要不要迁移原生标题栏」的口径写在 `platform::macos` 的模块文档里,
|
||||
// 改动这一行之前先读那一页(迁移必须连同撤掉自绘三键一起做,否则两套控件打架)。
|
||||
decorations: false,
|
||||
resizable: true,
|
||||
// M93 / D36:整窗 α 需要 `WS_EX_LAYERED`,但这里给不了——`window::Settings` 没有扩展样式
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
//! 宿主平台族(M182):窗口外框判据的**输入值**,而不是散落各处的 `#[cfg]`。
|
||||
//!
|
||||
//! ## 为什么平台要是一个值
|
||||
//!
|
||||
//! 圆角判据([`super::win32::corner::plan_corner_for`])必须能表达「Windows / macOS / Linux
|
||||
//! 各自怎么办」三行。写成 `#[cfg(target_os = …)]` 的话,在 Windows 上编译时另外两行**根本不存在**,
|
||||
//! 判据就永远只能在一台机器上验三分之一——而 M182 要的恰恰是那两行。
|
||||
//!
|
||||
//! 把平台变成值以后:真值表可以在**本机**跑满三行,`cfg` 只留在真正需要 FFI 的地方
|
||||
//! (那边本来就有,且只影响「能不能调」,不影响「判据是什么」)。这与 AGENTS.md §7
|
||||
//! 「平台差异判断收敛到底层,不许在各模块里散写 `cfg`」是同一条纪律。
|
||||
//!
|
||||
//! ## 取值来源
|
||||
//!
|
||||
//! [`HostOs::current`] 读 `std::env::consts::OS`(编译期常量,运行期不会变)。
|
||||
//! 它**不**做任何环境探测:本仓库里「探测」是 [`super::win32::transparency`] 与
|
||||
//! `probe_windows_version` 的事,这里只回答「我编在哪个平台上」。
|
||||
|
||||
/// 宿主平台族(判据输入)。
|
||||
///
|
||||
/// 只分到判据真正会用到的粒度:Windows 要问 DWM、macOS 要交给窗口服务器、
|
||||
/// Linux 没有可问的对象、其余平台(FreeBSD / Android / wasm …)一律不承诺。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HostOs {
|
||||
/// Windows(10 / 11 / Server 同族)。
|
||||
Windows,
|
||||
/// macOS。
|
||||
MacOs,
|
||||
/// Linux(X11 与 Wayland 都是它:判据不看后端,看的是有没有可问的对象)。
|
||||
Linux,
|
||||
/// 其余目标(FreeBSD / Android / wasm32 …):不承诺圆角。
|
||||
Other,
|
||||
}
|
||||
|
||||
impl HostOs {
|
||||
/// 当前编译目标所属的平台族。
|
||||
#[must_use]
|
||||
pub fn current() -> Self {
|
||||
Self::from_os_str(std::env::consts::OS)
|
||||
}
|
||||
|
||||
/// [`std::env::consts::OS`] 的字符串 → 平台族(纯函数,测试直接钉它)。
|
||||
///
|
||||
/// 只认 `"windows"` / `"macos"` / `"linux"` 三个字面量,其余(含未来新平台)落 [`Self::Other`]:
|
||||
/// 猜错的代价是「多给它一个圆角承诺」,而不认识的平台本来就没有承诺可言。
|
||||
#[must_use]
|
||||
pub fn from_os_str(os: &str) -> Self {
|
||||
match os {
|
||||
"windows" => Self::Windows,
|
||||
"macos" => Self::MacOs,
|
||||
"linux" => Self::Linux,
|
||||
_ => Self::Other,
|
||||
}
|
||||
}
|
||||
|
||||
/// 日志取值(`host=windows|macos|linux|other`)。
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Windows => "windows",
|
||||
Self::MacOs => "macos",
|
||||
Self::Linux => "linux",
|
||||
Self::Other => "other",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::HostOs;
|
||||
|
||||
/// **抓得住**:三个字面量任一个拼错(`"macos"` 写成 `"mac-os"` / `"darwin"`)——
|
||||
/// 那会让 macOS 静默掉进 `Other` 从而「不承诺圆角」,正是 M182 要修的那类静默。
|
||||
/// **抓不住**:`std::env::consts::OS` 在真机上到底是哪个值(那是编译器给的常量,本测试只钉映射)。
|
||||
#[test]
|
||||
fn the_three_supported_families_are_recognized_verbatim() {
|
||||
assert_eq!(HostOs::from_os_str("windows"), HostOs::Windows);
|
||||
assert_eq!(HostOs::from_os_str("macos"), HostOs::MacOs);
|
||||
assert_eq!(HostOs::from_os_str("linux"), HostOs::Linux);
|
||||
assert_eq!(HostOs::Windows.as_str(), "windows");
|
||||
assert_eq!(HostOs::MacOs.as_str(), "macos");
|
||||
assert_eq!(HostOs::Linux.as_str(), "linux");
|
||||
}
|
||||
|
||||
/// 不认识的目标一律 `Other`(不承诺),且**不许**退化成 Windows/macOS 的承诺。
|
||||
#[test]
|
||||
fn anything_else_falls_to_other_instead_of_guessing() {
|
||||
for os in ["freebsd", "netbsd", "android", "ios", "emscripten", ""] {
|
||||
assert_eq!(HostOs::from_os_str(os), HostOs::Other, "{os}");
|
||||
}
|
||||
assert_eq!(HostOs::Other.as_str(), "other");
|
||||
}
|
||||
|
||||
/// `current()` 必须与编译目标的 `cfg` 一致——这是**本机唯一**能自查「值与 cfg 没跑偏」的地方。
|
||||
#[test]
|
||||
fn current_matches_the_compilation_target() {
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(HostOs::current(), HostOs::Windows);
|
||||
#[cfg(target_os = "macos")]
|
||||
assert_eq!(HostOs::current(), HostOs::MacOs);
|
||||
#[cfg(target_os = "linux")]
|
||||
assert_eq!(HostOs::current(), HostOs::Linux);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//! Linux(X11 / Wayland)的窗口圆角(M182):**谁负责、我们为什么不画**。
|
||||
//!
|
||||
//! 用户定的规则是「非 Windows 按窗口合成器的默认规则来」。在 Linux 上这句话要落到具体机制,
|
||||
//! 否则很容易变成「把 Windows 那套 DWM 判据搬过来假装能用」——**不许**那样写(M182 任务书)。
|
||||
//!
|
||||
//! ## 事实层:协议里没有「圆角」这个概念
|
||||
//!
|
||||
//! | 层 | 有什么 | 依据级别 |
|
||||
//! |---|---|---|
|
||||
//! | `xdg-shell`(Wayland) | 只有 `wl_surface.set_opaque_region` / `set_input_region`(**矩形集合**)、`set_buffer_transform`、圆角半径之类一概没有 | A 级(协议规范) |
|
||||
//! | X11 | 形状扩展(`XShape`)能按矩形并集切出非矩形窗口,但**只能切硬边**(无抗锯齿);要透明圆角得配 ARGB visual + 合成器 | A 级(X 扩展规范) |
|
||||
//! | 装饰归属 | 谁装饰谁负责:合成器画(SSD)由合成器圆,客户端画(CSD)由客户端圆。`zxdg_decoration_manager_v1` **很多合成器即使被请求也不画**(D35 已记) | A 级协议 + D35 实测/推断 |
|
||||
//!
|
||||
//! 我们是 **CSD**:`decorations: false`(`app/boot/diagnostics.rs`),自绘标题栏 + 自绘三键。
|
||||
//! 所以「圆角该由谁画」的答案是**我们**——而 D46 第 4 条明确要求不自绘裁切,
|
||||
//! 于是唯一的诚实结果是**直角**,并把这句结论写进日志:
|
||||
//! `source=compositor excluded_reason=client-side`(见 [`crate::platform::win32::corner`])。
|
||||
//!
|
||||
//! ## 为什么不自绘(不是「懒得做」)
|
||||
//!
|
||||
//! 1. 自绘圆角 = 把内容层裁成圆角。我们的内容层含**自绘三键 + 边缘缩放热区**,裁掉圆角就会
|
||||
//! 一起裁掉热区(这条在 M116 已被 tower 采纳)。
|
||||
//! 2. Wayland 上「表面画圆角 + ARGB 缓冲」在合成器侧确实能透出桌面,但需要整窗 `transparent`
|
||||
//! ——那是 D36 的整窗 α 管线,且会与 Linux 上「没有 `_NET_WM_CM_S0` 合成器时透明变黑」的
|
||||
//! 既有结论打架。
|
||||
//! 3. X11 无合成器时透明不可用;只剩 `XShape` 硬边圆角——像素级毛边,比直角更难看。
|
||||
//!
|
||||
//! ## 合成器差异(**推断级,本机无真机,未验**)
|
||||
//!
|
||||
//! - 浮动式合成器(KWin / Mutter)对自己的 SSD 窗做圆角,对 CSD 窗**通常不裁**(客户端的 surface
|
||||
//! 原样合成);但个别主题会给无边框窗额外加重绘制的阴影/边框——那种圆角是**合成器画的**,
|
||||
//! 不是我们画的。判别口径:日志 `source=compositor`,而像素上的圆角随主题/合成器变化。
|
||||
//! - 平铺式(sway / niri / Hyprland / river)连边框都按自己的配置画,更不会替 CSD 窗裁圆角;
|
||||
//! D35/D37 还记着「sway 默认下已经出现合成器 SSD + 我们自绘的双标题栏」这条现状缺陷。
|
||||
//!
|
||||
//! 这些行为**没有任何一条**能用协议查询,也没有一条能由应用侧读取——所以判据里**不写**
|
||||
//! 「Linux 会/不会圆角」,只写「决定权在合成器,而 CSD 窗它不裁」。
|
||||
//!
|
||||
//! ## 本轮明确未验
|
||||
//!
|
||||
//! - 真实合成器上的四角观感(无 Linux 真机;CI 的 Linux 冒烟只验「能起来 + 出图不是全黑」);
|
||||
//! - KWin / Mutter / Hyprland 是否会对我们的 CSD 窗做额外装饰;
|
||||
//! - 高 DPI 下若将来改为自绘,半径是否需要缩放(本轮不涉及:半径不由我们定)。
|
||||
@@ -0,0 +1,93 @@
|
||||
//! macOS 的窗口外框(M182):**本轮不实现**,这里只有结论、证据与后续配方。
|
||||
//!
|
||||
//! 用户第三轮 Mac 真机验收的现象:四角是直角,日志 `[corner] … source=square os=unknown`。
|
||||
//! 「macOS 不支持圆角」是**错的**——macOS 的窗口四角本来就有系统圆角,只是我们交上去的窗口
|
||||
//! 不在会被裁的那一类里。三环链条如下,每一环都能在本机复核(读锁文件里的依赖源码即可):
|
||||
//!
|
||||
//! ## 第 1 环:我们交的是什么窗
|
||||
//!
|
||||
//! `crates/app-iced/src/app/boot/diagnostics.rs` 的 `window_settings()` 给的是
|
||||
//! `decorations: false`(M93 自绘三键)。那是**唯一产地**,`[window]` / `[caps]` 两行
|
||||
//! 验收日志也从它取字段。
|
||||
//!
|
||||
//! ## 第 2 环:winit 把 `decorations: false` 翻成什么
|
||||
//!
|
||||
//! winit 0.30.13 `src/platform_impl/macos/window_delegate.rs:540-552`:
|
||||
//!
|
||||
//! ```text
|
||||
//! let mut masks = if (!attrs.decorations && screen.is_none()) || titlebar_hidden {
|
||||
//! // Resizable without a titlebar or borders
|
||||
//! NSWindowStyleMask::Borderless | NSWindowStyleMask::Resizable | NSWindowStyleMask::Miniaturizable
|
||||
//! } else {
|
||||
//! // default case, resizable window with titlebar and titlebar buttons
|
||||
//! NSWindowStyleMask::Closable | Miniaturizable | Resizable | Titled
|
||||
//! };
|
||||
//! ```
|
||||
//!
|
||||
//! ⇒ `decorations: false` 拿到的是 **`Borderless`**,**没有 `Titled`**。
|
||||
//!
|
||||
//! ## 第 3 环:窗口服务器只给 `Titled` 窗做圆角裁剪
|
||||
//!
|
||||
//! 这是 AppKit 的标准窗口行为(A 级外部事实:标准窗口的圆角由窗口服务器连同阴影一起画,
|
||||
//! 无边框窗口不参与)。本仓库**没有**任何一行代码能影响它:winit 的 macOS 后端里
|
||||
//! `cornerRadius` / `CornerRadius` 命中数为 **0**(`src/platform_impl/macos/` 内四命中全是
|
||||
//! 「屏幕/视图左上角」的注释),也就是说圆角从来没有一行业是我们调的。
|
||||
//!
|
||||
//! 反向印证:用户第三轮实测(本机窗口就是 borderless)拍到四角直角,与第 3 环一致。
|
||||
//!
|
||||
//! 判据一侧的落点:`plan_corner_for(MacOs, …)` 现在返回
|
||||
//! `source=compositor excluded_reason=borderless`(见 [`crate::platform::win32::corner`]),
|
||||
//! 而不是原先含糊的 `source=square os=unknown`。
|
||||
//!
|
||||
//! ## 为什么**不**用 objc 给 `contentView.layer` 设 `cornerRadius`(M182 任务书原路线,已否)
|
||||
//!
|
||||
//! 1. **违反 D46 第 4 条**「macOS / X11 / Wayland 按合成器默认、**不主动自绘**」:
|
||||
//! 把内容层裁成圆角就是自绘裁切,同一份决策日志里已经否决过这条(还附带
|
||||
//! 「会把标题栏三键与边缘缩放热区一起裁掉」的问题)。
|
||||
//! 2. **撞 `unsafe` 铁律**:根 `Cargo.toml` 的 `[workspace.lints.rust] unsafe_code = "deny"`,
|
||||
//! 且 `guards.yml` 的 `unsafe-scope` 作业要求「带 `allow(unsafe_code)` 的文件集合**恰好等于**
|
||||
//! `crates/app-iced/src/platform/win32/transparency.rs`」——新增一个 objc FFI 文件会让该作业
|
||||
//! 直接红,而那个工作流不在 M182 的改动范围内。
|
||||
//! 3. **拿不到 `NSWindow`**:`app/window_alpha.rs` 的 `probe_hwnd` 在非 Windows 恒返回 `None`,
|
||||
//! 于是 `App::apply_window_corner` 走 `NoWindow` 分支——macOS 上那条下发链**从来没被调用过**。
|
||||
//! 要接线得改 `app/window_alpha.rs` + `app/update.rs`(M182 范围之外,且 M168/M178 正在改
|
||||
//! `app/**`)。
|
||||
//! 4. **本机编不了**:macOS 分支要靠 macOS 工具链,Windows 上连类型检查都跑不到。
|
||||
//!
|
||||
//! ## 后续口径(**未实现**,需另立 mission + Mac 真机第 4 轮验收)
|
||||
//!
|
||||
//! 目标:把外框交还给窗口服务器当标准窗口,由它按自己的规则圆角(= 用户定的
|
||||
//! 「其他系统按照窗口合成器的默认规则来」):
|
||||
//!
|
||||
//! ```text
|
||||
//! decorations: true,
|
||||
//! platform_specific: { title_hidden: true, titlebar_transparent: true, fullsize_content_view: true }
|
||||
//! ```
|
||||
//!
|
||||
//! - 三个字段 iced 0.14 已暴露(`iced_core-0.14.0/src/window/settings/macos.rs`),
|
||||
//! iced_winit 1:1 转发(`iced_winit-0.14.1/src/conversion.rs:125-137` 的
|
||||
//! `#[cfg(target_os = "macos")]` 块);
|
||||
//! `FullSizeContentView` 让内容铺满整窗(不留原生标题栏高度),窗口服务器把整窗
|
||||
//! ——**连同我们的背景图**——裁成圆角,不会出现「外框圆了、里面还是方块」。
|
||||
//! - **必须同时隐藏我们自绘的三键**(`crates/app-iced/src/shell/mod.rs`),否则屏幕上会同时存在
|
||||
//! 左上角原生三色灯与右上角自绘三键两套控件——那不是「跟随合成器」,是两套控件打架。
|
||||
//! - **已知阻塞点(真做之前必须先解掉)**:winit 有 `with_titlebar_buttons_hidden`
|
||||
//! (`src/platform/macos.rs:290`,能一次藏掉全部三色灯),但 **iced 0.14 的
|
||||
//! `PlatformSpecific` 没有转发它**;而 `WindowExtMacOS` 也没有运行期隐藏按钮的接口
|
||||
//! (只有 `set_has_shadow` / `set_document_edited` 等)。三条出路:① 给 iced 上游提转发
|
||||
//! (最干净,但要等版本);② 仅用 `closeable: false, minimizable: false` 藏掉关闭/最小化
|
||||
//! 两键——**缩放键藏不掉**,会留一个孤零零的绿灯;③ 拿到 `NSWindow` 后手动 `setHidden:`
|
||||
//! (回到 objc + `unsafe`,见上面的第 2、3 条)。
|
||||
//! - **验收日志会一起变**:`renderer::acceptance::visual` 从 `decorations` 推
|
||||
//! `controls=self-drawn|native`,一旦改成 `true`,那行会写 `controls=native`,而我们当时
|
||||
//! 仍在自绘三键(除非同一次改动把三键撤掉)⇒ 那条改动必须**同一个 mission 内**一起做。
|
||||
//! - **验证路径**:本机(Windows)拿不到任何 macOS 像素;只能靠
|
||||
//! `ci.yml` 的 `runtime-smoke-macos` 作业(dispatch 触发、`continue-on-error`)看**编不编得过**,
|
||||
//! 视觉效果必须等**第 4 轮 Mac 真机验收图**或用户手动确认。
|
||||
//!
|
||||
//! ## 本轮明确未验
|
||||
//!
|
||||
//! - macOS 圆的半径、平滑度、与系统其它窗口是否一致(无真机);
|
||||
//! - 高 DPI(Retina)下系统圆角的视觉半径(由系统决定,我们读不到,也不需要缩放——这一档
|
||||
//! 本来就「不归我们管」,见 [`crate::platform::win32::corner`] 的 `radius=system`);
|
||||
//! - 原生三色灯的最终观感与「是否保留自绘三键」的产品取舍。
|
||||
@@ -1,30 +1,49 @@
|
||||
//! 平台相关的窗口能力(P4 / D36 / M116):整窗透明度与窗口圆角的**判据**、唯一 `unsafe` 落点,
|
||||
//! 以及系统语言真值源,三件事分居三处。
|
||||
//! 平台相关的窗口能力(P4 / D36 / M116 / M168 / M182):整窗透明度与窗口圆角的**判据**、唯一 `unsafe`
|
||||
//! 落点、系统语言真值源、以及数据根派生,五件事分居五处。
|
||||
//!
|
||||
//! - 本文件 = 整窗 α 的纯判据 [`effective_window_alpha`]:不读环境变量、不调 FFI、不碰窗口句柄,
|
||||
//! 只回答「按这个渲染后端和用户要的 α,最终该给窗口什么值」;
|
||||
//! - [`win32::corner`] = 圆角的纯判据 [`plan_corner`](M116):用户策略 × 系统版本 × 渲染档 →
|
||||
//! 来源与原因码。两个判据都**不碰** FFI,因此都能被断言证明;
|
||||
//! - [`win32::corner`] = 圆角的纯判据 [`plan_corner`] / [`plan_corner_for`](M116 / M182):
|
||||
//! 用户策略 × 宿主平台 × 渲染档 → 来源与原因码。两个判据都**不碰** FFI,因此都能被断言证明;
|
||||
//! - [`win32::transparency`] = 唯一允许 `unsafe` 的文件(补 `WS_EX_LAYERED` +
|
||||
//! `SetLayeredWindowAttributes`,以及 M116 的 `DwmSetWindowAttribute`);
|
||||
//! - [`locale`] = 系统语言真值源(D28):`reg.exe` 子进程读注册表,**零 `unsafe` / 零 FFI**。
|
||||
//!
|
||||
//! 为什么判据要单独拎出来:D36 的降级规则(α 下限 0.08 / `≤0` 一律拒绝 / CPU 档也允许
|
||||
//! 窗口级 α)**可以**被断言证明;而 FFI 的**视觉**结果在本仓库的验证环境(RDP + 虚拟显示
|
||||
//! 适配器)上没有参考价值。把可测的那一半独立出来,才有「有测试的判据 + 没测试的观感」这条
|
||||
//! 诚实的分界。
|
||||
//! 适配器、无 Mac 真机)上没有参考价值。把可测的那一半独立出来,才有「有测试的判据 +
|
||||
//! 没测试的观感」这条诚实的分界。
|
||||
//!
|
||||
//! ## 平台分册(M182:非 Windows 不再是「未知」一格)
|
||||
//!
|
||||
//! | 模块 | 里面有什么 | 有没有 FFI |
|
||||
//! |---|---|---|
|
||||
//! | [`data`] | 数据根派生(字体缓存分区 + 一条设置键的生效值,M168) | 没有 |
|
||||
//! | [`host`] | [`HostOs`]:平台族的**值**(判据的输入,不是散落的 `cfg`) | 没有 |
|
||||
//! | [`macos`] | macOS 为什么是直角的三环证据 + 「迁移原生标题栏」的未实现配方 | 没有(本轮只有文档) |
|
||||
//! | [`linux`] | X11 / Wayland 谁负责圆角、为什么我们不自绘 | 没有(本轮只有文档) |
|
||||
//! | [`win32`] | Windows 的实现与探针(DWM / 分层 / 只此一处的 `unsafe`) | **有**(唯一落点) |
|
||||
//!
|
||||
//! 两个新平台模块**只有文档、没有代码**:M182 的裁决是不实现(macOS 要动手就得先迁移原生标题栏,
|
||||
//! 那会引入两套窗口控件,必须连着撤掉自绘三键 + 真机验收一起做)。把结论写在这里而不是写进报告,
|
||||
//! 是因为报告会随会话消失,而下一轮动这块的人一定先读代码。
|
||||
|
||||
use unlucky_presenter::prefs::WINDOW_ALPHA_MIN;
|
||||
|
||||
use crate::Backend;
|
||||
|
||||
pub mod data;
|
||||
pub mod host;
|
||||
pub mod linux;
|
||||
pub mod locale;
|
||||
pub mod macos;
|
||||
pub mod win32;
|
||||
|
||||
/// 窗口圆角的**判定**(M116)住在 `win32::corner`:判据是 Windows 版本与 DWM 能力,
|
||||
/// 非 Windows 平台在这里恒得「直角」(平台没有可承诺的系统圆角)。
|
||||
pub use win32::corner::{plan_corner, CornerExclusion, CornerPlan, CornerSource};
|
||||
/// 窗口圆角的**判定**(M116 / M182)住在 `win32::corner`:判据是用户策略 × 宿主平台 × 渲染档。
|
||||
/// 非 Windows 的两档(macOS = 合成器不裁 borderless 窗、Linux = CSD 没人裁)在
|
||||
/// [`macos`] / [`linux`] 两个模块里逐条给了证据。
|
||||
pub use host::HostOs;
|
||||
pub use win32::corner::{plan_corner, plan_corner_for, CornerExclusion, CornerPlan, CornerSource};
|
||||
|
||||
/// 本机 Windows 版本(`None` = 非 Windows / 读不到)。**进程内只探测一次**。
|
||||
///
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
//! M116:窗口外框圆角的**判据**(纯函数,零 `unsafe`、零 FFI、零环境读取)。
|
||||
//! M116 / M182:窗口外框圆角的**判据**(纯函数,零 `unsafe`、零 FFI、零环境读取)。
|
||||
//!
|
||||
//! ## 判据为什么单独拎出来
|
||||
//!
|
||||
//! 「能不能圆」= 用户策略 × 系统版本 × 渲染档,这三件事**可以被断言证明**;而 DWM 的**视觉**
|
||||
//! 结果在本仓库的验证环境(RDP 断连、抓屏失效)上没有参考价值。把可测的那一半独立出来,
|
||||
//! 才有「有测试的判据 + 没测试的观感」这条诚实的分界(与 [`super::super::effective_window_alpha`]
|
||||
//! 「能不能圆」= 用户策略 × 宿主平台 × 渲染档,这几件事**可以被断言证明**;而合成器的**视觉**
|
||||
//! 结果在本仓库的验证环境(RDP 断连、抓屏失效、无 Mac 真机)上没有参考价值。把可测的那一半独立
|
||||
//! 出来,才有「有测试的判据 + 没测试的观感」这条诚实的分界(与 [`super::super::effective_window_alpha`]
|
||||
//! 同一套分法)。
|
||||
//!
|
||||
//! 真正下发给窗口的动作在 [`super::transparency`](全仓唯一 `unsafe` 落点)。
|
||||
//!
|
||||
//! ## 平台矩阵(哪些是实测、哪些是推断)
|
||||
//! ## 平台矩阵(M182 修订:非 Windows 不再是「未知」一格)
|
||||
//!
|
||||
//! | 平台 | 我们做什么 | 依据级别 |
|
||||
//! |---|---|---|
|
||||
//! | Win11 / Server 2025(build ≥ 22000) | 下发 `DWMWCP_ROUND`(**请求**系统圆角) | 写入被接受 = **本机 API 实测**;「圆没圆 / 边缘什么样」= **未验**(无像素、回读不可用) |
|
||||
//! | Win10 及更早(build < 22000) | 直角;不做 `SetWindowRgn` 毛边默认 | 「该属性不存在」= **外部文档证据**;本机没有 Win10 |
|
||||
//! | macOS | 不主动加,交给系统 | **推断**(未真机验) |
|
||||
//! | Linux X11 / Wayland | 不承诺,直角即如实结果 | **推断**(未真机验) |
|
||||
//! | 平台 | `source` | 谁画圆角 | 依据级别 |
|
||||
//! |---|---|---|---|
|
||||
//! | Win11 / Server 2025(build ≥ 22000) | `dwm` | DWM(我们只下发偏好) | 写入被接受 = **本机 API 实测**;「圆没圆」= **未验**(Server 2025 上实测无圆角,见下) |
|
||||
//! | Win10 及更早(build < 22000) | `square` + `os-build` / `cpu-tier` | 没人 | 「该属性不存在」= **外部文档证据**;本机没有 Win10 |
|
||||
//! | Windows 版本读不到 | `square` + `unknown-os` | 没人 | 探测失败,不猜 |
|
||||
//! | macOS | `compositor` + `borderless` | 本该窗口服务器,但**它只裁 titled 窗**,我们是 borderless ⇒ 没人 | **源码级证据**(见 [`crate::platform::macos`]) |
|
||||
//! | Linux X11 / Wayland | `compositor` + `client-side` | 本该合成器,但协议里没有圆角这回事,CSD 窗没人裁 | **规范级证据**(见 [`crate::platform::linux`]) |
|
||||
//! | 其余(FreeBSD / Android / wasm…) | `square` + `unsupported` | 没人 | 不承诺 |
|
||||
//!
|
||||
//! ⚠️ **「Win11 上我们的窗口一定有圆角、边缘平滑」这句话本分支的证据不支持**,别写回去。
|
||||
//! 本机(Server 2025 build 26100、RDP 会话断开)测到的是:
|
||||
@@ -28,63 +30,74 @@
|
||||
//! ⚠️ **第 ③ 条不能拿裸 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` ⇒ 直角。
|
||||
//! 这是「不承诺」,不是「做不到」——两者的区别写在日志的原因码里。
|
||||
|
||||
use unlucky_presenter::prefs::{WindowCorner, WindowsVersion, WIN11_MIN_BUILD};
|
||||
|
||||
use crate::platform::host::HostOs;
|
||||
use crate::Backend;
|
||||
|
||||
/// 一次「圆角该从哪来」的判定结果(M116)。
|
||||
/// 一次「圆角该从哪来」的判定结果(M116 / M182)。
|
||||
///
|
||||
/// 只有两种来源,且**互相排斥**:
|
||||
/// 三档**互斥**,回答的是同一个问题:「这个窗口的四角由谁负责」。
|
||||
///
|
||||
/// | 来源 | 谁画圆角 | 我们做什么 |
|
||||
/// | 来源 | 谁负责 | 我们做什么 |
|
||||
/// |---|---|---|
|
||||
/// | [`CornerSource::Dwm`] | 系统合成器(是否真画、半径多少都不由我们定,也读不到) | 只下发 `DWMWA_WINDOW_CORNER_PREFERENCE` |
|
||||
/// | [`CornerSource::Square`] | 没人 | 明确下发 `DWMWCP_DONOTROUND`,把「方」也变成可观测的选择 |
|
||||
/// | [`CornerSource::Dwm`] | Windows 的 DWM(半径不由我们定,也读不到) | 只下发 `DWMWA_WINDOW_CORNER_PREFERENCE` |
|
||||
/// | [`CornerSource::Compositor`] | 平台的窗口合成器 / 窗口服务器**按它自己的规则**(M182 新增) | **什么都不做**:不下发偏好、不自绘裁切(D46) |
|
||||
/// | [`CornerSource::Square`] | 没人 | Windows 上显式下发 `DWMWCP_DONOTROUND`,把「方」也变成可观测的选择 |
|
||||
///
|
||||
/// **没有 `Self`(自绘圆角)这一档**,这是刻意的(用户定案 2026-11-03 + tower 裁定):
|
||||
/// **「合成器」这一档不等于「一定有圆角」**:它只说「圆角的决定权不在我们手里」。落到这一档
|
||||
/// 之后到底圆不圆,由 `excluded` 那一栏如实交代(macOS = `borderless`、Linux = `client-side`)。
|
||||
/// 把它写成「跟随合成器 ⇒ 圆角」正是 M182 要修的误读。
|
||||
///
|
||||
/// **没有 `Self`(自绘圆角)这一档**,这是刻意的(用户定案 2026-11-03 + tower 裁定,D46 第 4 条):
|
||||
/// 窗口是 `decorations=false` + 自绘标题栏 + 整窗 `LWA_ALPHA`,自绘圆角意味着把内容层裁成圆角,
|
||||
/// 那会把标题栏三键与边缘缩放热区一起裁掉;系统也不暴露圆角半径(无 API 可读),
|
||||
/// 「固定 8/12/16」只是凭空维护一个数字。所以非 DWM 平台**就是直角**,并且日志里说明原因。
|
||||
/// 「固定 8/12/16」只是凭空维护一个数字。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CornerSource {
|
||||
/// DWM 系统圆角(Win11 / Server 2025 内核,build ≥ [`WIN11_MIN_BUILD`])。
|
||||
Dwm,
|
||||
/// 平台跟随合成器(M182):圆角归属交给窗口合成器 / 窗口服务器,我们既不下发也不自绘。
|
||||
Compositor,
|
||||
/// 直角(用户选择、系统不支持、或该平台不承诺)。
|
||||
Square,
|
||||
}
|
||||
|
||||
impl CornerSource {
|
||||
/// 日志取值(`corner=… source=dwm|square`)。
|
||||
/// 日志取值(`corner=… source=dwm|compositor|square`)。
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Dwm => "dwm",
|
||||
Self::Compositor => "compositor",
|
||||
Self::Square => "square",
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否要求 DWM 真去画圆角。
|
||||
/// 是否要求 DWM 真去画圆角(唯一需要下发 FFI 的那一档)。
|
||||
#[must_use]
|
||||
pub const fn is_dwm(self) -> bool {
|
||||
matches!(self, Self::Dwm)
|
||||
}
|
||||
|
||||
/// 是否把圆角归属交给合成器(M182:只在非 Windows 上可能为真)。
|
||||
#[must_use]
|
||||
pub const fn is_compositor(self) -> bool {
|
||||
matches!(self, Self::Compositor)
|
||||
}
|
||||
}
|
||||
|
||||
/// 为什么**不是** DWM 圆角(`None` = 就是 DWM 圆角)。日志里逐字打出来。
|
||||
/// 为什么**没有**圆角(`None` = 就是 DWM 圆角)。日志里逐字打出来。
|
||||
///
|
||||
/// 为什么要这个枚举而不是一个 `bool`:用户报的现象是「别的窗口都有圆角,只有我们的没有」,
|
||||
/// 于是「没圆角」至少有四种完全不同的原因,混成一个 `square` 以后没人能分辨
|
||||
/// 于是「没圆角」至少有六种完全不同的原因,混成一个 `square` 以后没人能分辨
|
||||
/// 「没生效」与「生效了但看不见」(M116 任务书原话)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CornerExclusion {
|
||||
@@ -92,11 +105,21 @@ pub enum CornerExclusion {
|
||||
UserSquare,
|
||||
/// 系统太老:Win10 及更早(build < [`WIN11_MIN_BUILD`])——DWM 没有这个偏好属性。
|
||||
OsBuild,
|
||||
/// 版本探测不出来(非 Windows / 注册表读不到 / 有东西在骗版本号)。
|
||||
/// Windows 版本探测不出来(注册表读不到 / 有东西在骗版本号)。**只在 Windows 上出现**。
|
||||
UnknownOs,
|
||||
/// 非 DWM 平台上的软件渲染档:圆角只能靠自绘/`SetWindowRgn` 毛边路,D36 的
|
||||
/// 「CPU 档不碰合成技巧」优先,所以退回直角。
|
||||
CpuTier,
|
||||
/// macOS(M182):窗口是 `decorations=false` ⇒ winit 给 `NSWindowStyleMask::Borderless`
|
||||
/// (**没有 `Titled`**)⇒ 窗口服务器不给这类窗口做圆角裁剪。不是「系统不支持圆角」,
|
||||
/// 是「我们交上去的形状不在会被裁的那一类里」。证据见 [`crate::platform::macos`]。
|
||||
BorderlessFrame,
|
||||
/// Linux(M182):`xdg-shell` / X11 协议里都没有「圆角半径」这种东西,CSD 无边框窗
|
||||
/// 合成器不会替我们裁(谁装饰谁负责,而我们自己声明了 CSD)。D46 又排除自绘 ⇒ 直角。
|
||||
/// 证据与各合成器差异见 [`crate::platform::linux`]。
|
||||
ClientSideFrame,
|
||||
/// 不认识的平台(FreeBSD / Android / wasm …):不承诺。
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
impl CornerExclusion {
|
||||
@@ -108,34 +131,49 @@ impl CornerExclusion {
|
||||
Self::OsBuild => "os-build",
|
||||
Self::UnknownOs => "unknown-os",
|
||||
Self::CpuTier => "cpu-tier",
|
||||
Self::BorderlessFrame => "borderless",
|
||||
Self::ClientSideFrame => "client-side",
|
||||
Self::Unsupported => "unsupported",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 判定结果:来源 + 排除原因 + 平台版本(日志与测试共用)。
|
||||
/// 判定结果:来源 + 排除原因 + 宿主平台 + Windows 版本(日志与测试共用)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct CornerPlan {
|
||||
/// 圆角从哪来。
|
||||
pub source: CornerSource,
|
||||
/// `None` = 走 DWM;`Some(..)` = 走直角,并说明为什么。
|
||||
/// `None` = 走合成器/DWM;`Some(..)` = 这一档没有圆角,并说明为什么。
|
||||
pub excluded: Option<CornerExclusion>,
|
||||
/// 探测到的 Windows 版本(`None` = 不是 Windows / 读不到)。
|
||||
/// 宿主平台族(M182:判据的输入,日志里也打出来)。
|
||||
pub host: HostOs,
|
||||
/// 探测到的 Windows 版本(`None` = 非 Windows / 读不到)。
|
||||
pub os: Option<WindowsVersion>,
|
||||
}
|
||||
|
||||
impl CornerPlan {
|
||||
/// 日志行:`corner=policy=system source=dwm os=10.0.26100 excluded_reason=none radius=system`。
|
||||
/// 日志行:`corner=policy=system host=macos source=compositor os=n/a excluded_reason=borderless radius=system`。
|
||||
///
|
||||
/// **`radius=system` 是刻意的**:系统不暴露 DWM 的圆角半径,我们也不自己定一个数字
|
||||
/// **`radius=system` 是刻意的**:系统不暴露圆角半径,我们也不自己定一个数字
|
||||
/// (用户定案取消了 8/12/16 档),所以这一栏永远只说「系统决定」。
|
||||
///
|
||||
/// **`host=` 是 M182 加的**:用户看到的那行 `source=square os=unknown` 之所以难判断,
|
||||
/// 就是因为「未知」同时被 Windows 探测失败和「这台根本不是 Windows」共用。有了 `host=`
|
||||
/// 与 `excluded_reason=borderless|client-side`,同一行就能自己解释完。
|
||||
///
|
||||
/// **非 Windows 的 `os=n/a`**:那一栏装的是 Windows 版本,在 macOS / Linux 上不是「没读到」,
|
||||
/// 而是「跟本机无关」——写 `unknown` 会让人去找一个根本不存在的注册表项。
|
||||
#[must_use]
|
||||
pub fn log_line(&self, policy: WindowCorner) -> String {
|
||||
let os = self
|
||||
.os
|
||||
.map_or_else(|| "unknown".to_owned(), |os| os.version_string());
|
||||
let os = match (self.host, self.os) {
|
||||
(HostOs::Windows, Some(os)) => os.version_string(),
|
||||
(HostOs::Windows, None) => "unknown".to_owned(),
|
||||
_ => "n/a".to_owned(),
|
||||
};
|
||||
format!(
|
||||
"corner=policy={} source={} os={os} excluded_reason={} radius=system",
|
||||
"corner=policy={} host={} source={} os={os} excluded_reason={} radius=system",
|
||||
policy.as_str(),
|
||||
self.host.as_str(),
|
||||
self.source.as_str(),
|
||||
self.excluded.map_or("none", CornerExclusion::as_str),
|
||||
)
|
||||
@@ -144,15 +182,29 @@ impl CornerPlan {
|
||||
|
||||
/// 判定「这一次窗口该不该有系统圆角」(纯函数,不调 FFI、不读环境)。
|
||||
///
|
||||
/// 真值表(`backend` 只影响**非 DWM** 平台,见下):
|
||||
/// 生产入口:平台取 [`HostOs::current`](编译期常量),Windows 版本由调用方探测后传进来。
|
||||
/// 想验另外两行真值表时用 [`plan_corner_for`]——那是同一个判据,只是平台可以由测试喂。
|
||||
#[must_use]
|
||||
pub fn plan_corner(
|
||||
policy: WindowCorner,
|
||||
os: Option<WindowsVersion>,
|
||||
backend: Backend,
|
||||
) -> CornerPlan {
|
||||
plan_corner_for(policy, HostOs::current(), os, backend)
|
||||
}
|
||||
|
||||
/// 真值表(`os` / `backend` 只对 Windows 行有意义):
|
||||
///
|
||||
/// | `policy` | `os.build` | backend | `source` | `excluded` |
|
||||
/// |---|---|---|---|---|
|
||||
/// | `Square` | 任意 | 任意 | `Square` | `UserSquare` |
|
||||
/// | `System` | `None` | 任意 | `Square` | `UnknownOs` |
|
||||
/// | `System` | `< 22000` | GPU/Auto | `Square` | `OsBuild` |
|
||||
/// | `System` | `< 22000` | CPU | `Square` | `CpuTier` |
|
||||
/// | `System` | `>= 22000` | 任意 | `Dwm` | `None` |
|
||||
/// | `policy` | `host` | `os.build` | backend | `source` | `excluded` |
|
||||
/// |---|---|---|---|---|---|
|
||||
/// | `Square` | 任意 | 任意 | 任意 | `Square` | `UserSquare` |
|
||||
/// | `System` | Windows | `None` | 任意 | `Square` | `UnknownOs` |
|
||||
/// | `System` | Windows | `< 22000` | GPU/Auto | `Square` | `OsBuild` |
|
||||
/// | `System` | Windows | `< 22000` | CPU | `Square` | `CpuTier` |
|
||||
/// | `System` | Windows | `>= 22000` | 任意 | `Dwm` | `None` |
|
||||
/// | `System` | MacOs | 忽略 | 任意 | `Compositor` | `BorderlessFrame` |
|
||||
/// | `System` | Linux | 忽略 | 任意 | `Compositor` | `ClientSideFrame` |
|
||||
/// | `System` | Other | 忽略 | 任意 | `Square` | `Unsupported` |
|
||||
///
|
||||
/// 为什么 CPU 档在 Win11 上**照样**用 DWM 圆角(**tower 2026-11-03 裁定,按此执行**):
|
||||
/// D36 的 CPU 档约束针对的是**我们自己的渲染路径**(面板 α / 玻璃 / 模糊要关、不许拿透明叠加
|
||||
@@ -162,39 +214,64 @@ impl CornerPlan {
|
||||
/// 反过来在非 DWM 平台上,圆角只剩「自绘裁切 / `SetWindowRgn` 毛边」这两条**我们自己的**
|
||||
/// 合成技巧,那才是 D36 要拦的东西 ⇒ 那一格落 `cpu-tier` 而不是 `os-build`
|
||||
/// (同一个直角,**原因不同**,日志里可分辨)。
|
||||
///
|
||||
/// macOS / Linux 两行**不看 `backend`**(M182):我们根本不下发任何东西,也就没有「渲染档
|
||||
/// 要不要让步」这回事——CPU 档与 GPU 档在这两个平台上得到同一份判定,这是事实而不是遗漏。
|
||||
#[must_use]
|
||||
pub fn plan_corner(
|
||||
pub fn plan_corner_for(
|
||||
policy: WindowCorner,
|
||||
host: HostOs,
|
||||
os: Option<WindowsVersion>,
|
||||
backend: Backend,
|
||||
) -> CornerPlan {
|
||||
let square = |excluded| CornerPlan {
|
||||
source: CornerSource::Square,
|
||||
excluded: Some(excluded),
|
||||
host,
|
||||
os,
|
||||
};
|
||||
|
||||
if policy == WindowCorner::Square {
|
||||
return square(CornerExclusion::UserSquare);
|
||||
}
|
||||
match os {
|
||||
None => square(CornerExclusion::UnknownOs),
|
||||
Some(os) if os.build < WIN11_MIN_BUILD => match backend {
|
||||
Backend::Cpu => square(CornerExclusion::CpuTier),
|
||||
Backend::Gpu | Backend::Auto => square(CornerExclusion::OsBuild),
|
||||
match host {
|
||||
// Windows:能不能圆是 DWM 的属性与版本问题,得问。
|
||||
HostOs::Windows => match os {
|
||||
None => square(CornerExclusion::UnknownOs),
|
||||
Some(os) if os.build < WIN11_MIN_BUILD => match backend {
|
||||
Backend::Cpu => square(CornerExclusion::CpuTier),
|
||||
Backend::Gpu | Backend::Auto => square(CornerExclusion::OsBuild),
|
||||
},
|
||||
Some(_) => CornerPlan {
|
||||
source: CornerSource::Dwm,
|
||||
excluded: None,
|
||||
host,
|
||||
os,
|
||||
},
|
||||
},
|
||||
Some(_) => CornerPlan {
|
||||
source: CornerSource::Dwm,
|
||||
excluded: None,
|
||||
// macOS:圆角归窗口服务器,但我们交的是 borderless 窗(`decorations: false`)⇒ 它不裁。
|
||||
// 迁移成原生标题栏是另一件需要真机验收的事,配方见 `platform::macos`。
|
||||
HostOs::MacOs => CornerPlan {
|
||||
source: CornerSource::Compositor,
|
||||
excluded: Some(CornerExclusion::BorderlessFrame),
|
||||
host,
|
||||
os,
|
||||
},
|
||||
// Linux:协议里没有圆角,CSD 窗由客户端装饰 ⇒ 没人替我们裁,自绘被 D46 排除。
|
||||
HostOs::Linux => CornerPlan {
|
||||
source: CornerSource::Compositor,
|
||||
excluded: Some(CornerExclusion::ClientSideFrame),
|
||||
host,
|
||||
os,
|
||||
},
|
||||
HostOs::Other => square(CornerExclusion::Unsupported),
|
||||
}
|
||||
}
|
||||
|
||||
/// 产品日志行的**唯一格式化落点**(纯函数:把四段文本拼成一个固定形状)。
|
||||
///
|
||||
/// ```text
|
||||
/// [corner] <context> corner=policy=system source=dwm os=10.0.26100 excluded_reason=none \
|
||||
/// [corner] <context> corner=policy=system host=windows 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
|
||||
/// ```
|
||||
|
||||
@@ -2,11 +2,26 @@
|
||||
//! 而日志行的形状断言本身就有几十行)。
|
||||
//!
|
||||
//! 判据与真实读数的出处:`DevDocs/artifacts/m116/probe-table.md`。
|
||||
//!
|
||||
//! ## M182 的两条纪律(都来自复审 P1-1,别再退回原样)
|
||||
//!
|
||||
//! 1. **判据断言一律走 [`plan_corner_for`],平台显式传值**。`plan_corner` 是**产品入口**,
|
||||
//! 它读的是 `HostOs::current()`——同一个测试在 Windows 上绿、在 ubuntu 上就可能红。
|
||||
//! M182 首轮复审实测:本机(Windows)384+ 条全绿,而其中三条断言的是 Windows 的结果,
|
||||
//! 合进 main 后 `ubuntu-latest` 的 `test` job 会当场红。
|
||||
//! 2. **`plan_corner` 只留一条用例**,且那条必须**与宿主无关**:
|
||||
//! [`the_production_entry_forwards_to_the_pure_judgement`] 两边都用同一个 `HostOs::current()`,
|
||||
//! 所以它在任何平台上都成立,只证「产品入口确实转发到纯函数」。
|
||||
//!
|
||||
//! 「非 Windows」那一组断言住在 [`platforms`](本文件守 400 行上限)。
|
||||
|
||||
use super::{format_line, plan_corner, CornerExclusion, CornerPlan, CornerSource};
|
||||
use super::{format_line, plan_corner, plan_corner_for, CornerExclusion, CornerPlan, CornerSource};
|
||||
use crate::platform::host::HostOs;
|
||||
use crate::Backend;
|
||||
use unlucky_presenter::prefs::{WindowCorner, WindowsVersion, WIN11_MIN_BUILD};
|
||||
|
||||
mod platforms;
|
||||
|
||||
/// 本机实测版本(Server 2025 / build 26100)。
|
||||
fn win11() -> WindowsVersion {
|
||||
WindowsVersion::new(10, 0, 26_100)
|
||||
@@ -17,14 +32,23 @@ fn win10() -> WindowsVersion {
|
||||
WindowsVersion::new(10, 0, 19_045)
|
||||
}
|
||||
|
||||
/// **抓得住**:判定链的顺序与每一格的原因码(把 `policy`/`os`/`backend` 任一条判反都会红)。
|
||||
/// 判据断言用的固定宿主:**永远是 Windows**,与本机实际跑在哪个系统上无关。
|
||||
///
|
||||
/// 为什么要有这个包装而不是直接写 `HostOs::Windows`:写一次名字就够了,以后想给 Linux /
|
||||
/// macOS 也加 Windows 专属断言时,改这一处即可。
|
||||
fn on_windows(policy: WindowCorner, os: Option<WindowsVersion>, backend: Backend) -> CornerPlan {
|
||||
plan_corner_for(policy, HostOs::Windows, os, backend)
|
||||
}
|
||||
|
||||
/// **抓得住**:判定链的顺序与每一格的原因码(把 `policy`/`host`/`os`/`backend` 任一条判反都会红)。
|
||||
/// **抓不住**:DWM 到底有没有真画圆角(那要像素,见 `platform/win32` 的探针输出)。
|
||||
#[test]
|
||||
fn truth_table_policy_os_backend() {
|
||||
fn truth_table_policy_host_os_backend() {
|
||||
let cases = [
|
||||
// policy=Square:任何系统/后端都直角,原因是用户选的。
|
||||
// policy=Square:任何平台/系统/后端都直角,原因是用户选的。
|
||||
(
|
||||
WindowCorner::Square,
|
||||
HostOs::Windows,
|
||||
Some(win11()),
|
||||
Backend::Gpu,
|
||||
CornerSource::Square,
|
||||
@@ -32,21 +56,16 @@ fn truth_table_policy_os_backend() {
|
||||
),
|
||||
(
|
||||
WindowCorner::Square,
|
||||
Some(win10()),
|
||||
HostOs::Linux,
|
||||
None,
|
||||
Backend::Cpu,
|
||||
CornerSource::Square,
|
||||
Some(CornerExclusion::UserSquare),
|
||||
),
|
||||
(
|
||||
WindowCorner::Square,
|
||||
None,
|
||||
Backend::Auto,
|
||||
CornerSource::Square,
|
||||
Some(CornerExclusion::UserSquare),
|
||||
),
|
||||
// policy=System + 探测不出系统:不许猜「Win11」。
|
||||
// policy=System + Windows 上探测不出系统:不许猜「Win11」。
|
||||
(
|
||||
WindowCorner::System,
|
||||
HostOs::Windows,
|
||||
None,
|
||||
Backend::Gpu,
|
||||
CornerSource::Square,
|
||||
@@ -55,6 +74,7 @@ fn truth_table_policy_os_backend() {
|
||||
// Win10:直角,GPU 档原因是版本、CPU 档原因是渲染档(同一个直角,原因不同)。
|
||||
(
|
||||
WindowCorner::System,
|
||||
HostOs::Windows,
|
||||
Some(win10()),
|
||||
Backend::Gpu,
|
||||
CornerSource::Square,
|
||||
@@ -62,6 +82,7 @@ fn truth_table_policy_os_backend() {
|
||||
),
|
||||
(
|
||||
WindowCorner::System,
|
||||
HostOs::Windows,
|
||||
Some(win10()),
|
||||
Backend::Auto,
|
||||
CornerSource::Square,
|
||||
@@ -69,6 +90,7 @@ fn truth_table_policy_os_backend() {
|
||||
),
|
||||
(
|
||||
WindowCorner::System,
|
||||
HostOs::Windows,
|
||||
Some(win10()),
|
||||
Backend::Cpu,
|
||||
CornerSource::Square,
|
||||
@@ -77,6 +99,7 @@ fn truth_table_policy_os_backend() {
|
||||
// Win11 / Server 2025:三个后端都用 DWM(D36 只管我们自己的合成技巧)。
|
||||
(
|
||||
WindowCorner::System,
|
||||
HostOs::Windows,
|
||||
Some(win11()),
|
||||
Backend::Gpu,
|
||||
CornerSource::Dwm,
|
||||
@@ -84,29 +107,54 @@ fn truth_table_policy_os_backend() {
|
||||
),
|
||||
(
|
||||
WindowCorner::System,
|
||||
HostOs::Windows,
|
||||
Some(win11()),
|
||||
Backend::Cpu,
|
||||
CornerSource::Dwm,
|
||||
None,
|
||||
),
|
||||
// macOS(M182):圆角归窗口服务器,但我们交的是 borderless 窗 ⇒ 它不裁。
|
||||
(
|
||||
WindowCorner::System,
|
||||
Some(win11()),
|
||||
Backend::Auto,
|
||||
CornerSource::Dwm,
|
||||
HostOs::MacOs,
|
||||
None,
|
||||
Backend::Gpu,
|
||||
CornerSource::Compositor,
|
||||
Some(CornerExclusion::BorderlessFrame),
|
||||
),
|
||||
// Linux(M182):CSD 窗没人裁,自绘被 D46 排除。
|
||||
(
|
||||
WindowCorner::System,
|
||||
HostOs::Linux,
|
||||
None,
|
||||
Backend::Gpu,
|
||||
CornerSource::Compositor,
|
||||
Some(CornerExclusion::ClientSideFrame),
|
||||
),
|
||||
// 不认识的平台:不承诺。
|
||||
(
|
||||
WindowCorner::System,
|
||||
HostOs::Other,
|
||||
None,
|
||||
Backend::Gpu,
|
||||
CornerSource::Square,
|
||||
Some(CornerExclusion::Unsupported),
|
||||
),
|
||||
];
|
||||
|
||||
for (policy, os, backend, want_source, want_excluded) in cases {
|
||||
let plan = plan_corner(policy, os, backend);
|
||||
for (policy, host, os, backend, want_source, want_excluded) in cases {
|
||||
let plan = plan_corner_for(policy, host, os, backend);
|
||||
assert_eq!(
|
||||
plan.source, want_source,
|
||||
"policy={policy:?} os={os:?} backend={backend:?}"
|
||||
"policy={policy:?} host={host:?} os={os:?} backend={backend:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
plan.excluded, want_excluded,
|
||||
"policy={policy:?} os={os:?} backend={backend:?} 的原因码"
|
||||
"policy={policy:?} host={host:?} os={os:?} backend={backend:?} 的原因码"
|
||||
);
|
||||
assert_eq!(
|
||||
plan.host, host,
|
||||
"判定必须把输入的平台原样带回来(日志要用)"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -119,40 +167,43 @@ fn truth_table_policy_os_backend() {
|
||||
fn the_win11_boundary_is_inclusive() {
|
||||
let at = WindowsVersion::new(10, 0, WIN11_MIN_BUILD);
|
||||
assert_eq!(
|
||||
plan_corner(WindowCorner::System, Some(at), Backend::Gpu).source,
|
||||
on_windows(WindowCorner::System, Some(at), Backend::Gpu).source,
|
||||
CornerSource::Dwm
|
||||
);
|
||||
let below = WindowsVersion::new(10, 0, WIN11_MIN_BUILD - 1);
|
||||
assert_eq!(
|
||||
plan_corner(WindowCorner::System, Some(below), Backend::Gpu).source,
|
||||
on_windows(WindowCorner::System, Some(below), Backend::Gpu).source,
|
||||
CornerSource::Square,
|
||||
"build 21999 = Win10 22H2 家族末尾,不能算 Win11"
|
||||
);
|
||||
}
|
||||
|
||||
/// 日志行的形状:四个字段都在,且 `excluded_reason=none` 与 `reason=user` 不许混。
|
||||
/// 日志行的形状:五个字段都在,且 `excluded_reason=none` 与 `reason=user` 不许混。
|
||||
///
|
||||
/// 为什么要钉这一条:这行是「后来的人分辨没生效 / 生效了看不见」的唯一依据(任务书原话),
|
||||
/// 少一栏就等于把这条线索丢掉。
|
||||
/// 少一栏就等于把这条线索丢掉。M182 新增的 `host=` 与「非 Windows 的 `os=n/a`」在这里钉死。
|
||||
#[test]
|
||||
fn the_log_line_carries_every_field() {
|
||||
let dwm = plan_corner(WindowCorner::System, Some(win11()), Backend::Gpu)
|
||||
let dwm = on_windows(WindowCorner::System, Some(win11()), Backend::Gpu)
|
||||
.log_line(WindowCorner::System);
|
||||
assert!(dwm.starts_with("corner=policy=system"), "{dwm}");
|
||||
assert!(dwm.contains("host=windows"), "{dwm}");
|
||||
assert!(dwm.contains("source=dwm"), "{dwm}");
|
||||
assert!(dwm.contains("os=10.0.26100"), "{dwm}");
|
||||
assert!(dwm.contains("excluded_reason=none"), "{dwm}");
|
||||
assert!(dwm.contains("radius=system"), "{dwm}");
|
||||
|
||||
let square = plan_corner(WindowCorner::Square, Some(win11()), Backend::Gpu)
|
||||
let square = on_windows(WindowCorner::Square, Some(win11()), Backend::Gpu)
|
||||
.log_line(WindowCorner::Square);
|
||||
assert!(square.contains("source=square"), "{square}");
|
||||
assert!(square.contains("excluded_reason=user"), "{square}");
|
||||
|
||||
let unknown: CornerPlan = plan_corner(WindowCorner::System, None, Backend::Gpu);
|
||||
assert!(unknown
|
||||
.log_line(WindowCorner::System)
|
||||
.contains("os=unknown"));
|
||||
// Windows 上「版本读不到」:`os=unknown` **配上** `host=windows` 才是完整的真话。
|
||||
let unknown =
|
||||
on_windows(WindowCorner::System, None, Backend::Gpu).log_line(WindowCorner::System);
|
||||
assert!(unknown.contains("host=windows"), "{unknown}");
|
||||
assert!(unknown.contains("os=unknown"), "{unknown}");
|
||||
assert!(unknown.contains("excluded_reason=unknown-os"), "{unknown}");
|
||||
}
|
||||
|
||||
/// **日志行的形状**,用 M116 探针窗口的**真实读数**构造(`DevDocs/artifacts/m116/probe-table.md`)。
|
||||
@@ -166,7 +217,7 @@ fn the_log_line_shape_is_pinned_with_the_measured_readings() {
|
||||
// 本机实测: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 plan: CornerPlan = on_windows(WindowCorner::System, Some(os), Backend::Gpu);
|
||||
let line = format_line(
|
||||
"boot",
|
||||
&plan,
|
||||
@@ -178,7 +229,7 @@ fn the_log_line_shape_is_pinned_with_the_measured_readings() {
|
||||
assert_eq!(
|
||||
line,
|
||||
concat!(
|
||||
"[corner] boot corner=policy=system source=dwm os=10.0.26100 ",
|
||||
"[corner] boot corner=policy=system host=windows 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"
|
||||
@@ -188,14 +239,14 @@ fn the_log_line_shape_is_pinned_with_the_measured_readings() {
|
||||
// 设置里选「直角」:原因码必须从 `none` 变成 `user`,其余字段不动。
|
||||
let square = format_line(
|
||||
"change",
|
||||
&plan_corner(WindowCorner::Square, Some(os), Backend::Gpu),
|
||||
&on_windows(WindowCorner::Square, Some(os), Backend::Gpu),
|
||||
WindowCorner::Square,
|
||||
"pref=1 readback=? read_hr=0x80070057",
|
||||
"frame=overlapped layered=off",
|
||||
None,
|
||||
);
|
||||
assert!(
|
||||
square.contains("corner=policy=square source=square"),
|
||||
square.contains("corner=policy=square host=windows source=square"),
|
||||
"{square}"
|
||||
);
|
||||
assert!(square.contains("excluded_reason=user"), "{square}");
|
||||
@@ -206,28 +257,80 @@ fn the_log_line_shape_is_pinned_with_the_measured_readings() {
|
||||
// Win10:原因码是 `os-build`(GPU 档)/ `cpu-tier`(CPU 档),来源仍是 square。
|
||||
let win10 = WindowsVersion::new(10, 0, 19_045);
|
||||
let os_build =
|
||||
plan_corner(WindowCorner::System, Some(win10), Backend::Gpu).log_line(WindowCorner::System);
|
||||
on_windows(WindowCorner::System, Some(win10), Backend::Gpu).log_line(WindowCorner::System);
|
||||
assert!(
|
||||
os_build.contains("source=square") && os_build.contains("excluded_reason=os-build"),
|
||||
"{os_build}"
|
||||
);
|
||||
let cpu =
|
||||
plan_corner(WindowCorner::System, Some(win10), Backend::Cpu).log_line(WindowCorner::System);
|
||||
on_windows(WindowCorner::System, Some(win10), Backend::Cpu).log_line(WindowCorner::System);
|
||||
assert!(cpu.contains("excluded_reason=cpu-tier"), "{cpu}");
|
||||
}
|
||||
|
||||
// 探测不到版本(macOS / X11 / Wayland 落到同一格):`unknown-os`,不许猜 Win11。
|
||||
let unknown =
|
||||
plan_corner(WindowCorner::System, None, Backend::Gpu).log_line(WindowCorner::System);
|
||||
assert!(
|
||||
unknown.contains("os=unknown") && unknown.contains("excluded_reason=unknown-os"),
|
||||
"{unknown}"
|
||||
);
|
||||
/// **产品入口只有一条用例**,且它**与宿主无关**(复审 P1-1 的要求)。
|
||||
///
|
||||
/// 断言的是「`plan_corner` 就是 `plan_corner_for(HostOs::current(), …)`」:两边喂同一个
|
||||
/// `HostOs::current()`,所以在 Windows / ubuntu / macOS 上恒成立——它证明的是**转发**,
|
||||
/// 不是某个平台的结果。任何一个平台的结果都由 [`truth_table_policy_host_os_backend`]
|
||||
/// 显式传 `HostOs` 钉住。
|
||||
///
|
||||
/// **抓得住**:`plan_corner` 里偷偷改成写死的 `HostOs::Windows`(那样 ubuntu 上这条会红)、
|
||||
/// 或者干脆不再转发(判据分叉)。
|
||||
#[test]
|
||||
fn the_production_entry_forwards_to_the_pure_judgement() {
|
||||
for policy in [WindowCorner::System, WindowCorner::Square] {
|
||||
for os in [None, Some(win10()), Some(win11())] {
|
||||
for backend in [Backend::Gpu, Backend::Cpu, Backend::Auto] {
|
||||
let production = plan_corner(policy, os, backend);
|
||||
assert_eq!(
|
||||
production,
|
||||
plan_corner_for(policy, HostOs::current(), os, backend),
|
||||
"plan_corner 必须原样转发到纯函数(policy={policy:?} os={os:?} backend={backend:?})"
|
||||
);
|
||||
assert_eq!(production.host, HostOs::current());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 每个原因码都是一个**非空、无空格、互不重复**的稳定串(复审 P2-1:`unknown-os` 此前没有任何断言)。
|
||||
///
|
||||
/// 为什么值得一条表测试:这些串是产品日志里 `excluded_reason=` 的唯一取值,而它们只在
|
||||
/// **构建这样一张表**的时候才会被逐个过一遍——散在断言里时,`unknown-os` 这种「只在一台
|
||||
/// Windows 机器、且版本探测失败时才出现」的值最容易一直没有断言(复审就是这么抓到的)。
|
||||
///
|
||||
/// **抓得住**:两个原因码共用一个串(日志再也分不清原因)、串里混进空格(那一栏会被截断)、
|
||||
/// 或者某一条被改成空串。
|
||||
#[test]
|
||||
fn every_exclusion_code_is_a_stable_non_empty_string() {
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
let codes = [
|
||||
(CornerExclusion::UserSquare, "user"),
|
||||
(CornerExclusion::OsBuild, "os-build"),
|
||||
(CornerExclusion::UnknownOs, "unknown-os"),
|
||||
(CornerExclusion::CpuTier, "cpu-tier"),
|
||||
(CornerExclusion::BorderlessFrame, "borderless"),
|
||||
(CornerExclusion::ClientSideFrame, "client-side"),
|
||||
(CornerExclusion::Unsupported, "unsupported"),
|
||||
];
|
||||
|
||||
let mut seen = BTreeSet::new();
|
||||
for (reason, code) in codes {
|
||||
assert_eq!(reason.as_str(), code, "{reason:?} 的日志取值");
|
||||
assert!(
|
||||
!code.is_empty() && !code.contains(char::is_whitespace),
|
||||
"原因码必须是单个词(日志按空格分栏):{code:?}"
|
||||
);
|
||||
assert!(seen.insert(code), "两个原因码共用了同一个串:{code}");
|
||||
}
|
||||
assert_eq!(seen.len(), 7, "目前共七个原因码,少一个就是漏了断言");
|
||||
}
|
||||
|
||||
/// **同一策略 N 次只下发 / 只记 1 行**(M116 r1 复审 P2-4 的判据)。
|
||||
///
|
||||
/// 为什么要有这条:设置页的整窗 α / 面板 α / 暗化滑条每动一下都会触发一次外观变更,
|
||||
/// 而圆角与它们无关。没有去重时拖一次滑条会刷出一串 `[corner] change …`,
|
||||
/// 而圆角与它们无关。没有去重时拖一次滑条就会刷出一串 `[corner] change …`,
|
||||
/// 把「分辨有没有生效」的那唯一一行日志淹掉。
|
||||
///
|
||||
/// 抓得住:把去重删掉(`needs_redispatch` 恒 `true` 时这里会数到 N)、或指纹里漏掉某一维
|
||||
@@ -235,12 +338,10 @@ fn the_log_line_shape_is_pinned_with_the_measured_readings() {
|
||||
/// 抓不住:真进程里到底打了几行(`eprintln!` 在 `App` 里,本测试只钉判定)。
|
||||
#[test]
|
||||
fn the_same_policy_is_dispatched_only_once() {
|
||||
use super::{needs_redispatch, plan_corner, CornerApplied};
|
||||
use crate::Backend;
|
||||
use unlucky_presenter::prefs::{WindowCorner, WindowsVersion};
|
||||
use super::{needs_redispatch, CornerApplied};
|
||||
|
||||
let os = WindowsVersion::new(10, 0, 26_100);
|
||||
let plan = plan_corner(WindowCorner::System, Some(os), Backend::Gpu);
|
||||
let plan = on_windows(WindowCorner::System, Some(os), Backend::Gpu);
|
||||
|
||||
// 模拟「滑条被拖动 20 次」:每次都是同一个策略、同一个系统、句柄一直在。
|
||||
let mut last: Option<CornerApplied> = None;
|
||||
@@ -255,7 +356,7 @@ fn the_same_policy_is_dispatched_only_once() {
|
||||
assert_eq!(dispatches, 1, "同一策略连按 20 次只该下发/记 1 次");
|
||||
|
||||
// 换档必须重下。
|
||||
let square = plan_corner(WindowCorner::Square, Some(os), Backend::Gpu);
|
||||
let square = on_windows(WindowCorner::Square, Some(os), Backend::Gpu);
|
||||
assert!(needs_redispatch(
|
||||
last.as_ref(),
|
||||
&CornerApplied::new(WindowCorner::Square, &square, true, false)
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
//! 「非 Windows」那一组断言(M182):`HostOs` 是判据的**参数**,所以本机(Windows)也能验。
|
||||
//!
|
||||
//! 为什么单独成文件:`corner/tests.rs` 已经贴着 400 行上限(M182 加进来的三行平台断言
|
||||
//! 正好是压垮它的那部分)。
|
||||
|
||||
use super::{win10, win11};
|
||||
use crate::platform::host::HostOs;
|
||||
use crate::platform::win32::corner::{plan_corner_for, CornerExclusion, CornerSource};
|
||||
use crate::Backend;
|
||||
use unlucky_presenter::prefs::WindowCorner;
|
||||
|
||||
const ALL_HOSTS: [HostOs; 4] = [HostOs::Windows, HostOs::MacOs, HostOs::Linux, HostOs::Other];
|
||||
const ALL_BACKENDS: [Backend; 3] = [Backend::Gpu, Backend::Cpu, Backend::Auto];
|
||||
|
||||
/// **M182 的核心断言**:`os = None` 不再等于「未知平台」。
|
||||
///
|
||||
/// 同一个 `(System, None, Gpu)` 三元组,在 Windows 上是「版本读不出来」(`unknown-os`),
|
||||
/// 在 macOS / Linux 上是「这一栏跟本机无关」(`borderless` / `client-side`)。把三行写成
|
||||
/// 同一格就是用户看到的那句 `source=square os=unknown`——它同时糊掉了「探测失败」与
|
||||
/// 「这台根本不是 Windows」两件事。
|
||||
///
|
||||
/// **抓得住**:谁把非 Windows 两行改回 `Square`/`UnknownOs`(或把 `host` 从判据里摘掉)。
|
||||
#[test]
|
||||
fn a_missing_windows_version_does_not_mean_unknown_platform() {
|
||||
let rows: Vec<_> = [HostOs::Windows, HostOs::MacOs, HostOs::Linux]
|
||||
.into_iter()
|
||||
.map(|host| {
|
||||
let plan = plan_corner_for(WindowCorner::System, host, None, Backend::Gpu);
|
||||
(host, plan.source, plan.excluded)
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
rows,
|
||||
vec![
|
||||
(
|
||||
HostOs::Windows,
|
||||
CornerSource::Square,
|
||||
Some(CornerExclusion::UnknownOs)
|
||||
),
|
||||
(
|
||||
HostOs::MacOs,
|
||||
CornerSource::Compositor,
|
||||
Some(CornerExclusion::BorderlessFrame)
|
||||
),
|
||||
(
|
||||
HostOs::Linux,
|
||||
CornerSource::Compositor,
|
||||
Some(CornerExclusion::ClientSideFrame)
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// 非 Windows 的日志行必须自己解释完:有 `host=`、`os=n/a`(不是 `unknown`)、原因码说人话。
|
||||
///
|
||||
/// **抓得住**:把 `os=` 在非 Windows 上也写成 `unknown`——那会让人去找一个不存在的注册表项。
|
||||
#[test]
|
||||
fn the_non_windows_log_line_explains_itself() {
|
||||
let mac = plan_corner_for(WindowCorner::System, HostOs::MacOs, None, Backend::Gpu)
|
||||
.log_line(WindowCorner::System);
|
||||
assert!(mac.contains("host=macos"), "{mac}");
|
||||
assert!(mac.contains("os=n/a"), "{mac}");
|
||||
assert!(!mac.contains("os=unknown"), "{mac}");
|
||||
assert!(mac.contains("source=compositor"), "{mac}");
|
||||
assert!(mac.contains("excluded_reason=borderless"), "{mac}");
|
||||
|
||||
let linux = plan_corner_for(WindowCorner::System, HostOs::Linux, None, Backend::Cpu)
|
||||
.log_line(WindowCorner::System);
|
||||
assert!(linux.contains("host=linux"), "{linux}");
|
||||
assert!(linux.contains("os=n/a"), "{linux}");
|
||||
assert!(linux.contains("excluded_reason=client-side"), "{linux}");
|
||||
|
||||
// Windows 上 `os=unknown` 仍要保留它本来的意思:真的没探测到版本。
|
||||
let win = plan_corner_for(WindowCorner::System, HostOs::Windows, None, Backend::Gpu)
|
||||
.log_line(WindowCorner::System);
|
||||
assert!(
|
||||
win.contains("host=windows") && win.contains("os=unknown"),
|
||||
"{win}"
|
||||
);
|
||||
}
|
||||
|
||||
/// macOS / Linux 两行**不看渲染档**:我们什么都不下发,也就没有「CPU 档要不要让步」这回事。
|
||||
///
|
||||
/// **抓得住**:有人按 Windows 的形状给这两行加 `backend` 分支(那会变成凭空造的降级)。
|
||||
#[test]
|
||||
fn the_non_windows_rows_ignore_the_backend() {
|
||||
for host in [HostOs::MacOs, HostOs::Linux] {
|
||||
let plans: Vec<_> = ALL_BACKENDS
|
||||
.into_iter()
|
||||
.map(|backend| plan_corner_for(WindowCorner::System, host, None, backend))
|
||||
.collect();
|
||||
for plan in &plans {
|
||||
assert_eq!(
|
||||
(plan.source, plan.excluded),
|
||||
(plans[0].source, plans[0].excluded),
|
||||
"{host:?}:三个渲染档必须得到同一份判定(实际 {plan:?})"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// macOS 与 Linux **不许共用同一个原因码**:两边的机制不同(缺 `Titled` 位 ≠ 协议里没有圆角)。
|
||||
///
|
||||
/// **抓得住**:复制粘贴把 Linux 行也标成 `borderless`(那会让以后看日志的人去研究
|
||||
/// `NSWindowStyleMask`,而 Linux 上根本没有这个东西)。
|
||||
#[test]
|
||||
fn macos_and_linux_report_different_reasons() {
|
||||
let mac = plan_corner_for(WindowCorner::System, HostOs::MacOs, None, Backend::Gpu);
|
||||
let linux = plan_corner_for(WindowCorner::System, HostOs::Linux, None, Backend::Gpu);
|
||||
assert_ne!(mac.excluded, linux.excluded);
|
||||
assert_eq!(mac.excluded, Some(CornerExclusion::BorderlessFrame));
|
||||
assert_eq!(linux.excluded, Some(CornerExclusion::ClientSideFrame));
|
||||
// 两行都不是「我们要求 DWM 画」,而是「决定权在合成器」(`is_compositor` / `is_dwm`
|
||||
// 这两个小判据的用处就在这里:以后加平台时先问一句「这一档算不算合成器档」)。
|
||||
assert!(mac.source.is_compositor() && linux.source.is_compositor());
|
||||
assert!(!mac.source.is_dwm() && !linux.source.is_dwm());
|
||||
}
|
||||
|
||||
/// `Compositor` 这一档**不许**在 Windows 上出现;`Dwm` 也**不许**在非 Windows 上出现。
|
||||
///
|
||||
/// **抓得住**:「非 Windows 一律 compositor」写成「一律不 square」这类图省事的判定,
|
||||
/// 或者反过来把 Windows 的 DWM 判据搬去 Linux 假装能用(M182 任务书点名禁止)。
|
||||
#[test]
|
||||
fn each_source_stays_on_its_own_platforms() {
|
||||
for os in [None, Some(win10()), Some(win11())] {
|
||||
for backend in ALL_BACKENDS {
|
||||
let win = plan_corner_for(WindowCorner::System, HostOs::Windows, os, backend);
|
||||
assert_ne!(
|
||||
win.source,
|
||||
CornerSource::Compositor,
|
||||
"Windows 上不许出现 compositor 档:{win:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
for host in [HostOs::MacOs, HostOs::Linux, HostOs::Other] {
|
||||
for backend in ALL_BACKENDS {
|
||||
// 非 Windows 传一个「看起来像 Win11」的版本号进去:判据**不许**被它带偏。
|
||||
let plan = plan_corner_for(WindowCorner::System, host, Some(win11()), backend);
|
||||
assert_ne!(
|
||||
plan.source,
|
||||
CornerSource::Dwm,
|
||||
"{host:?} 上没有 DWM:{plan:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `source` 与 `excluded` 不许互相撒谎:说「从合成器来」就必须说清为什么没来。
|
||||
///
|
||||
/// **抓得住**:把 macOS/Linux 写成 `source=compositor excluded_reason=none`——那行的字面意思是
|
||||
/// 「已经按合成器圆了」,而用户屏幕上仍是直角(正是 M182 要修的那种误读)。
|
||||
#[test]
|
||||
fn the_source_never_claims_a_corner_that_the_exclusion_denies() {
|
||||
for policy in [WindowCorner::System, WindowCorner::Square] {
|
||||
for host in ALL_HOSTS {
|
||||
for os in [None, Some(win10()), Some(win11())] {
|
||||
for backend in ALL_BACKENDS {
|
||||
let plan = plan_corner_for(policy, host, os, backend);
|
||||
match plan.source {
|
||||
// 真下发了 DWM 偏好时不该有原因码。
|
||||
CornerSource::Dwm => assert_eq!(plan.excluded, None, "{plan:?}"),
|
||||
// 合成器档在当前实现里**永远**带原因码(没有任何平台真拿到系统圆角)。
|
||||
CornerSource::Compositor => assert!(
|
||||
plan.excluded.is_some(),
|
||||
"source=compositor 必须交代原因:{plan:?}"
|
||||
),
|
||||
CornerSource::Square => assert!(plan.excluded.is_some(), "{plan:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 用户选「直角」必须在**每一个**平台上都赢(包括将来真的迁移了标题栏的 macOS)。
|
||||
///
|
||||
/// **抓得住**:把 `policy == Square` 的短路写在 `match host` 之后或写进某个平台分支里。
|
||||
#[test]
|
||||
fn the_user_square_choice_wins_on_every_platform() {
|
||||
for host in ALL_HOSTS {
|
||||
for backend in ALL_BACKENDS {
|
||||
let plan = plan_corner_for(WindowCorner::Square, host, Some(win11()), backend);
|
||||
assert_eq!(plan.source, CornerSource::Square, "{host:?} {backend:?}");
|
||||
assert_eq!(plan.excluded, Some(CornerExclusion::UserSquare));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,11 +102,16 @@ const E_INVALIDARG: u32 = 0x8007_0057;
|
||||
/// `Dwm` → [`DWMWCP_ROUND`];`Square` → [`DWMWCP_DONOTROUND`]。
|
||||
/// 刻意**不用**默认值(`0`):那在 Win11 上等于「请 DWM 自己决定」,而无边框窗口
|
||||
/// 到底算不算「有框」由 DWM 的启发式决定——那正是我们要么拿到、要么明确放弃的东西。
|
||||
///
|
||||
/// M182 的 `Compositor` 档(跟随合成器)在 Windows 上**不可达**(`plan_corner_for` 不产出它,
|
||||
/// 单测钉着这一点);真要出现,正确的落点是默认值 `0`(= 让系统自己决定),
|
||||
/// 而不是被当成「强制直角」——否则 `source=compositor` 那行日志会与下发值自相矛盾。
|
||||
#[must_use]
|
||||
pub const fn preference_for(source: super::super::corner::CornerSource) -> u32 {
|
||||
use super::super::corner::CornerSource;
|
||||
match source {
|
||||
CornerSource::Dwm => DWMWCP_ROUND,
|
||||
CornerSource::Compositor => 0,
|
||||
CornerSource::Square => DWMWCP_DONOTROUND,
|
||||
}
|
||||
}
|
||||
@@ -282,94 +287,7 @@ pub fn style_summary(hwnd: isize) -> String {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
apply_corner, corner_preference_hr, preference_for, CornerOutcome,
|
||||
DWMWA_VISIBLE_FRAME_BORDER_THICKNESS, DWMWA_WINDOW_CORNER_PREFERENCE, DWMWCP_DONOTROUND,
|
||||
DWMWCP_ROUND,
|
||||
};
|
||||
use crate::platform::win32::corner::{CornerPlan, CornerSource};
|
||||
use unlucky_presenter::prefs::WindowsVersion;
|
||||
|
||||
/// **抓得住**:属性号写错(34 写成 33 / 37 写成 36 —— 这两个都在 Win11 上存在,改错不会崩,
|
||||
/// 只会静默读出别的东西)、两个偏好值互换。
|
||||
/// **抓不住**:这些值在真窗口上是否真的生效(那要探针读数,见 `probe` 与报告)。
|
||||
#[test]
|
||||
fn attribute_numbers_follow_the_win32_header() {
|
||||
assert_eq!(DWMWA_WINDOW_CORNER_PREFERENCE, 34);
|
||||
assert_eq!(DWMWA_VISIBLE_FRAME_BORDER_THICKNESS, 37);
|
||||
assert_eq!(DWMWCP_ROUND, 2);
|
||||
assert_eq!(DWMWCP_DONOTROUND, 1);
|
||||
}
|
||||
|
||||
/// **抓得住**:`preference_for` 把圆角/直角映射反了(这一格反了就是「设置里选直角反而更圆」)。
|
||||
#[test]
|
||||
fn the_preference_mapping_cannot_be_swapped() {
|
||||
assert_eq!(preference_for(CornerSource::Dwm), DWMWCP_ROUND);
|
||||
assert_eq!(preference_for(CornerSource::Square), DWMWCP_DONOTROUND);
|
||||
}
|
||||
|
||||
/// 无效句柄:**不下发**、也不 panic。句柄 0 走 `NoWindow`(还没拿到窗口 ≠ 平台不支持)。
|
||||
#[test]
|
||||
fn a_missing_window_is_reported_not_guessed() {
|
||||
let plan = CornerPlan {
|
||||
source: CornerSource::Dwm,
|
||||
excluded: None,
|
||||
os: Some(WindowsVersion::new(10, 0, 26_100)),
|
||||
};
|
||||
assert_eq!(apply_corner(0, &plan), CornerOutcome::NoWindow);
|
||||
assert_eq!(corner_preference_hr(0).0, None);
|
||||
// 1 号句柄永远无效:非 Windows 上是「平台不支持」,Windows 上是 DWM 的失败码,
|
||||
// 两种都不许表现成「成功」。
|
||||
let outcome = apply_corner(1, &plan);
|
||||
assert!(
|
||||
matches!(
|
||||
outcome,
|
||||
CornerOutcome::Failed { .. }
|
||||
| CornerOutcome::Unsupported { .. }
|
||||
| CornerOutcome::UnsupportedPlatform
|
||||
),
|
||||
"无效句柄应报失败,实际 {outcome:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// 日志尾巴三态可区分:设了 / 设了但读不回 / 属性不支持。
|
||||
#[test]
|
||||
fn the_log_tail_separates_applied_from_unsupported() {
|
||||
let applied = CornerOutcome::Applied {
|
||||
preference: DWMWCP_ROUND,
|
||||
readback: Some(DWMWCP_ROUND),
|
||||
read_hresult: 0,
|
||||
};
|
||||
assert!(
|
||||
applied.describe().starts_with("pref=2 readback=2"),
|
||||
"{applied:?}"
|
||||
);
|
||||
|
||||
let blind = CornerOutcome::Applied {
|
||||
preference: DWMWCP_ROUND,
|
||||
readback: None,
|
||||
read_hresult: 0x8000_4005,
|
||||
};
|
||||
assert!(
|
||||
blind.describe().contains("readback=?"),
|
||||
"{}",
|
||||
blind.describe()
|
||||
);
|
||||
|
||||
let old = CornerOutcome::Unsupported {
|
||||
hresult: 0x8007_0057,
|
||||
};
|
||||
assert!(
|
||||
old.describe().contains("attribute unsupported"),
|
||||
"{}",
|
||||
old.describe()
|
||||
);
|
||||
assert!(CornerOutcome::UnsupportedPlatform
|
||||
.describe()
|
||||
.contains("platform=unsupported"));
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
|
||||
/// **抓得住**:`frame_label` 把 `WS_POPUP` 与 `WS_OVERLAPPEDWINDOW` 判反(那会让日志把
|
||||
/// 「我们自己的窗口」写成「普通窗口」,这行日志的全部价值就是分辨这个)。
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
//! `transparency::dwm` 的单测(M182 从 `dwm.rs` 内联挪出来:那个文件贴着 400 行上限,
|
||||
//! 而 M182 要往 `preference_for` 里加第三档映射)。
|
||||
//!
|
||||
//! 挪动是**逐行照搬** + M182 的新断言,没有重写判据。
|
||||
|
||||
use super::{
|
||||
apply_corner, corner_preference_hr, preference_for, CornerOutcome,
|
||||
DWMWA_VISIBLE_FRAME_BORDER_THICKNESS, DWMWA_WINDOW_CORNER_PREFERENCE, DWMWCP_DONOTROUND,
|
||||
DWMWCP_ROUND,
|
||||
};
|
||||
use crate::platform::host::HostOs;
|
||||
use crate::platform::win32::corner::{CornerPlan, CornerSource};
|
||||
use unlucky_presenter::prefs::WindowsVersion;
|
||||
|
||||
/// **抓得住**:属性号写错(34 写成 33 / 37 写成 36 —— 这两个都在 Win11 上存在,改错不会崩,
|
||||
/// 只会静默读出别的东西)、两个偏好值互换。
|
||||
/// **抓不住**:这些值在真窗口上是否真的生效(那要探针读数,见 `probe` 与报告)。
|
||||
#[test]
|
||||
fn attribute_numbers_follow_the_win32_header() {
|
||||
assert_eq!(DWMWA_WINDOW_CORNER_PREFERENCE, 34);
|
||||
assert_eq!(DWMWA_VISIBLE_FRAME_BORDER_THICKNESS, 37);
|
||||
assert_eq!(DWMWCP_ROUND, 2);
|
||||
assert_eq!(DWMWCP_DONOTROUND, 1);
|
||||
}
|
||||
|
||||
/// **抓得住**:`preference_for` 把圆角/直角映射反了(这一格反了就是「设置里选直角反而更圆」)。
|
||||
#[test]
|
||||
fn the_preference_mapping_cannot_be_swapped() {
|
||||
assert_eq!(preference_for(CornerSource::Dwm), DWMWCP_ROUND);
|
||||
assert_eq!(preference_for(CornerSource::Square), DWMWCP_DONOTROUND);
|
||||
}
|
||||
|
||||
/// M182 的第三档:`Compositor`(跟随合成器)在 Windows 上**不会出现**(见
|
||||
/// `plan_corner_for` 的真值表),所以这里钉两件事——① match 必须写全、不许 panic;
|
||||
/// ② 真要出现时它落到 `DWMWCP_DEFAULT`(`0` = 让 DWM 自己决定),而不是被偷偷当成直角。
|
||||
///
|
||||
/// **抓得住**:有人把这一臂写成 `DWMWCP_DONOTROUND`(那就成了「说跟随合成器、实际强制直角」,
|
||||
/// 与 `source=compositor` 那行日志自相矛盾)。
|
||||
#[test]
|
||||
fn the_compositor_tier_maps_to_letting_the_system_decide() {
|
||||
assert_eq!(
|
||||
preference_for(CornerSource::Compositor),
|
||||
0,
|
||||
"Compositor 档必须落到 DWMWCP_DEFAULT(0),不是 DONOTROUND"
|
||||
);
|
||||
// 且它在 Windows 的判据里不可达(真值表不会产出它)。
|
||||
for os in [None, Some(WindowsVersion::new(10, 0, 26_100))] {
|
||||
for backend in [
|
||||
crate::Backend::Gpu,
|
||||
crate::Backend::Cpu,
|
||||
crate::Backend::Auto,
|
||||
] {
|
||||
let plan = crate::platform::win32::corner::plan_corner_for(
|
||||
unlucky_presenter::prefs::WindowCorner::System,
|
||||
HostOs::Windows,
|
||||
os,
|
||||
backend,
|
||||
);
|
||||
assert_ne!(plan.source, CornerSource::Compositor, "{plan:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 无效句柄:**不下发**、也不 panic。句柄 0 走 `NoWindow`(还没拿到窗口 ≠ 平台不支持)。
|
||||
#[test]
|
||||
fn a_missing_window_is_reported_not_guessed() {
|
||||
let plan = CornerPlan {
|
||||
source: CornerSource::Dwm,
|
||||
excluded: None,
|
||||
host: HostOs::Windows,
|
||||
os: Some(WindowsVersion::new(10, 0, 26_100)),
|
||||
};
|
||||
assert_eq!(apply_corner(0, &plan), CornerOutcome::NoWindow);
|
||||
assert_eq!(corner_preference_hr(0).0, None);
|
||||
// 1 号句柄永远无效:非 Windows 上是「平台不支持」,Windows 上是 DWM 的失败码,
|
||||
// 两种都不许表现成「成功」。
|
||||
let outcome = apply_corner(1, &plan);
|
||||
assert!(
|
||||
matches!(
|
||||
outcome,
|
||||
CornerOutcome::Failed { .. }
|
||||
| CornerOutcome::Unsupported { .. }
|
||||
| CornerOutcome::UnsupportedPlatform
|
||||
),
|
||||
"无效句柄应报失败,实际 {outcome:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// 日志尾巴三态可区分:设了 / 设了但读不回 / 属性不支持。
|
||||
#[test]
|
||||
fn the_log_tail_separates_applied_from_unsupported() {
|
||||
let applied = CornerOutcome::Applied {
|
||||
preference: DWMWCP_ROUND,
|
||||
readback: Some(DWMWCP_ROUND),
|
||||
read_hresult: 0,
|
||||
};
|
||||
assert!(
|
||||
applied.describe().starts_with("pref=2 readback=2"),
|
||||
"{applied:?}"
|
||||
);
|
||||
|
||||
let blind = CornerOutcome::Applied {
|
||||
preference: DWMWCP_ROUND,
|
||||
readback: None,
|
||||
read_hresult: 0x8000_4005,
|
||||
};
|
||||
assert!(
|
||||
blind.describe().contains("readback=?"),
|
||||
"{}",
|
||||
blind.describe()
|
||||
);
|
||||
|
||||
let old = CornerOutcome::Unsupported {
|
||||
hresult: 0x8007_0057,
|
||||
};
|
||||
assert!(
|
||||
old.describe().contains("attribute unsupported"),
|
||||
"{}",
|
||||
old.describe()
|
||||
);
|
||||
assert!(CornerOutcome::UnsupportedPlatform
|
||||
.describe()
|
||||
.contains("platform=unsupported"));
|
||||
}
|
||||
@@ -194,11 +194,20 @@ fn dword_error_codes_and_hresults_are_printed_differently() {
|
||||
#[test]
|
||||
#[ignore = "要真窗口:手动跑 cargo test -p unlucky-app-iced --lib m116 -- --ignored --nocapture"]
|
||||
fn m116_corner_log_line_on_real_windows() {
|
||||
use crate::platform::win32::corner::plan_corner;
|
||||
use crate::platform::host::HostOs;
|
||||
use crate::platform::win32::corner::plan_corner_for;
|
||||
use unlucky_presenter::prefs::WindowCorner;
|
||||
|
||||
// 平台**显式传 Windows**:这条探针本身就是 `#[cfg(target_os = "windows")]` 的真窗口实验,
|
||||
// 用宿主绑定的 `plan_corner` 只会让「这台机器是 Windows」变成一个隐式前提(复审 P1-1 的
|
||||
// 同类问题:本机绿、别的宿主红)。判据与产品完全同一份,产物那一行逐字不变。
|
||||
let os = crate::platform::current_windows_version();
|
||||
let plan = plan_corner(WindowCorner::System, os, crate::Backend::Gpu);
|
||||
let plan = plan_corner_for(
|
||||
WindowCorner::System,
|
||||
HostOs::Windows,
|
||||
os,
|
||||
crate::Backend::Gpu,
|
||||
);
|
||||
// 直接遍历 `controlled_experiment` 的**同一份**行定义(含 `product` / `product+alpha`),
|
||||
// 免得这里另抄一份样式表、跟探针悄悄分叉。
|
||||
for row in super::controlled_experiment() {
|
||||
@@ -225,7 +234,12 @@ fn m116_corner_log_line_on_real_windows() {
|
||||
}
|
||||
|
||||
// 设置里选「直角」时的取值(`excluded_reason` 必须变成 `user`)。
|
||||
let square = plan_corner(WindowCorner::Square, os, crate::Backend::Gpu);
|
||||
let square = plan_corner_for(
|
||||
WindowCorner::Square,
|
||||
HostOs::Windows,
|
||||
os,
|
||||
crate::Backend::Gpu,
|
||||
);
|
||||
println!(
|
||||
"[corner] policy=square {}",
|
||||
square.log_line(WindowCorner::Square)
|
||||
|
||||
@@ -14,13 +14,19 @@
|
||||
//! 一个元素该查哪一层由 [`radius_layer_for`] 判定(按尺寸,唯一入口);微控件层取值走
|
||||
//! [`ComponentRadius::radius_px`],**尺寸超界时返回 `None`**——这不是文档约定,是签名保证。
|
||||
//!
|
||||
//! ## 窗口**外框**圆角不在这里(M116)
|
||||
//! ## 窗口**外框**圆角不在这里(M116 / M182)
|
||||
//!
|
||||
//! `ShapeScale` 管的是**我们画的东西**:卡片、按钮、浮层、面板。窗口的最外层圆角由操作系统
|
||||
//! 决定(Win11 / Server 2025 走 DWM 合成器,macOS 走 `layer.cornerRadius`,X11/Wayland 看合成器),
|
||||
//! 系统**不暴露**那个半径值,我们也不自己定一个数字(用户定案:不做 8/12/16 档)。
|
||||
//! 所以窗口圆角没有令牌:判据在 `unlucky-app-iced` 的 `platform::plan_corner`,
|
||||
//! 落点在 `platform/win32/transparency`。
|
||||
//! `ShapeScale` 管的是**我们画的东西**:卡片、按钮、浮层、面板。窗口的最外层圆角由平台决定,
|
||||
//! 系统**不暴露**那个半径值,我们也不自己定一个数字(用户定案:不做 8/12/16 档;D46 也不许
|
||||
//! 自绘裁切)。三档平台的机制各不相同(M182 已逐条取证):
|
||||
//!
|
||||
//! - Win11 / Server 2025:下发给 DWM;(Win10 及更早没有这个属性)
|
||||
//! - macOS:窗口服务器只给 **titled** 窗裁圆角,我们是 `decorations: false` 的 borderless 窗
|
||||
//! ⇒ 不裁(`platform::macos` 里有三环证据与后续口径);
|
||||
//! - X11 / Wayland:协议里没有圆角这回事,CSD 窗由客户端装饰 ⇒ 没人裁(`platform::linux`)。
|
||||
//!
|
||||
//! 所以窗口圆角没有令牌:判据在 `unlucky-app-iced` 的 `platform::plan_corner` /
|
||||
//! `platform::plan_corner_for`,Windows 的落点在 `platform/win32/transparency`。
|
||||
//!
|
||||
//! 反过来说:**别**为了「让窗口看起来是圆的」去给根容器加 `ShapeScale::ExtraLarge`——那会把
|
||||
//! 四角裁掉内容(背景图/标题栏底色露出方角),而外框仍然是方的。
|
||||
|
||||
Reference in new issue
Block a user