feat(app-iced): M116 窗口外框圆角策略(跟随系统/直角)+ DWM 读写的受控对照实验
用户问「为什么窗口没有圆角」。核实:组件层圆角齐,但窗口外框**零处理**——代码里没有任何
Dwm* / SetWindowRgn。本 mission 先做实测,再按平台分开落策略。
判据(纯函数、可真值表):
- `platform::plan_corner(策略 × 系统版本 × 渲染档)` → `source=dwm|square` + `excluded_reason`。
Win11 / Server 2025(build ≥ 22000)→ DWM;Win10 及更早 → 直角(不做 SetWindowRgn 毛边默认);
探测不到版本(macOS / X11 / Wayland)→ 直角 + `unknown-os`(「不承诺」而不是「做不到」)。
CPU 档在 Win11 上照样用 DWM(D36 限制的是我们自己的合成技巧,不是系统合成器)。
落点:`platform/win32/transparency.rs` 仍是全仓唯一 unsafe 文件,新增 DWM 的 FFI 与安全包装;
`alpha.rs` / `dwm.rs` / `probe.rs` 三个新子模块零 unsafe、零 extern(沿 exstyle.rs 的既有形状)。
实测(本机 Server 2025 build 26100、RDP 会话断开、同进程三窗口对照):
- `DwmSetWindowAttribute(DWMWA_WINDOW_CORNER_PREFERENCE)` 在 WS_OVERLAPPEDWINDOW / WS_POPUP /
WS_POPUP|WS_EX_LAYERED(α=0.8) 上**全部成功** ⇒ 无边框与整窗 α 不会让属性设不上;
- `DwmGetWindowAttribute` 同一属性三种样式**全部 E_INVALIDARG** ⇒ 本机不支持回读,与样式无关;
- `DWMWA_EXTENDED_FRAME_BOUNDS − GetWindowRect`:有边框 = +0,−7,popup = +0,+0
⇒ DWM 只给有边框窗口加不可见调整带,我们的窗口没有可画的圆角区域(结构性解释)。
- 像素(角上透明/方块)**未验**:RDP 断连导致 CopyFromScreen / gdigrab 全废。
同时修掉一个自己踩到的判据错误:`i32::unsigned_abs()` 不能用来打印 HRESULT
(E_INVALIDARG 0x80070057 会被印成 0x7FF8FFA9)。现在 HRESULT 用 `hr as u32` + 严重性位,
GetLastError 的 DWORD 语义保持 `unsigned_abs` 并写死注释(两者不同源,别统一)。
配置:新键 `appearance.window_corner ∈ {system, square}`(出厂 system),schema 1.5.0 只推 minor、
不写迁移:老文件缺键 = 取默认 = 跟随系统;旧版本读新文件时未知键被登记并原样写回(`RenderMode::Full`),
只在显式 `store.set` 该键时才 `UnknownKey`。
设置 UI:外观组只留两档(跟随系统 / 直角)+ 一句按平台说清的说明,文案走 i18n 中英。
日志:`[corner] <ctx> corner=policy=… source=… os=… excluded_reason=… radius=system pref=… readback=… frame=… layered=…`
(形状由 `corner::format_line` 的格式化单测钉住,用探针窗口真实读数构造)。
证据:DevDocs/artifacts/m116/{README.md,probe-table.md}
This commit is contained in:
1 parent
aace0c5836
commit
88dbf6d955
29 files changed
+2047
-259
No files matched your search
@@ -17,7 +17,18 @@ pub use unlucky_core::setting::appearance::PALETTE_SOURCES;
|
||||
/// 同理 [`PANEL_ALPHA_MIN`](0.4)与 [`BG_DIM_MAX`](0.95)也在这里重导出:M93 的 UI 侧要按它们
|
||||
/// 钳位滑块与暗化,而 `app-iced` 不直接依赖 core——不重导出就只能各抄一份字面量。
|
||||
pub use unlucky_core::setting::appearance::{
|
||||
PaletteSource, TitlebarMode, BG_DIM_MAX, PANEL_ALPHA_MIN, WINDOW_ALPHA_MIN,
|
||||
PaletteSource, TitlebarMode, WindowCorner, BG_DIM_MAX, PANEL_ALPHA_MIN, WINDOW_ALPHA_MIN,
|
||||
WINDOW_CORNERS,
|
||||
};
|
||||
|
||||
/// 系统版本探测(M116):Windows 侧「能不能用 DWM 圆角」的判据要 build 号,而
|
||||
/// `unlucky-app-iced` 不直接依赖 `unlucky-core`(分层:UI → presenter → core),
|
||||
/// 所以在这里做**唯一一次**重导出,`platform` 只认 [`WindowsVersion`]。
|
||||
///
|
||||
/// [`WIN11_MIN_BUILD`] = 22000:Win11 与 Server 2025 共用同一内核判据(M116 实测本机
|
||||
/// Server 2025 build 26100 落在这条线之上)。
|
||||
pub use unlucky_core::platform::windows_version::{
|
||||
probe as probe_windows_version, WindowsVersion, WIN11_MIN_BUILD,
|
||||
};
|
||||
|
||||
use crate::dto::VersionCategory;
|
||||
@@ -186,16 +197,19 @@ pub struct AppearanceBase {
|
||||
pub titlebar_mode: TitlebarMode,
|
||||
/// 配色来源档(三级编排在 `unlucky-design`,本 crate 只给值)
|
||||
pub palette_source: PaletteSource,
|
||||
/// 窗口外框圆角策略(M116:`system` / `square`;能不能圆由平台决定)
|
||||
pub window_corner: WindowCorner,
|
||||
}
|
||||
|
||||
impl AppearanceBase {
|
||||
/// 出厂态:不透明 / `auto` / 内置色卡。
|
||||
/// 出厂态:不透明 / `auto` / 内置色卡 / 跟随系统圆角。
|
||||
#[must_use]
|
||||
pub fn factory() -> Self {
|
||||
Self {
|
||||
window_opacity: 1.0,
|
||||
titlebar_mode: TitlebarMode::Auto,
|
||||
palette_source: PaletteSource::Preset,
|
||||
window_corner: WindowCorner::System,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,6 +223,7 @@ impl AppearanceBase {
|
||||
window_opacity: appearance.window_opacity as f32,
|
||||
titlebar_mode: appearance.titlebar_mode,
|
||||
palette_source: appearance.palette_source,
|
||||
window_corner: appearance.window_corner,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,9 +304,11 @@ pub mod keys {
|
||||
pub const TITLEBAR_MODE: &str = "appearance.titlebar_mode";
|
||||
/// 配色来源档(D38)
|
||||
pub const PALETTE_SOURCE: &str = "appearance.palette_source";
|
||||
/// 窗口圆角策略(M116;`system` / `square`)
|
||||
pub const WINDOW_CORNER: &str = "appearance.window_corner";
|
||||
|
||||
/// 本模块登记的全部键(自检/迁移用)。
|
||||
pub const ALL: [&str; 16] = [
|
||||
pub const ALL: [&str; 17] = [
|
||||
NAV_COLLAPSED,
|
||||
AI_COLLAPSED,
|
||||
TILE_SIZE,
|
||||
@@ -308,6 +325,7 @@ pub mod keys {
|
||||
WINDOW_OPACITY,
|
||||
TITLEBAR_MODE,
|
||||
PALETTE_SOURCE,
|
||||
WINDOW_CORNER,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use super::{
|
||||
keys, AppearanceBase, InstanceView, PaletteSource, ThemeMode, TileSize, TitlebarMode, UiPrefs,
|
||||
WINDOW_ALPHA_MIN,
|
||||
WindowCorner, WINDOW_ALPHA_MIN,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -88,3 +88,22 @@ fn appearance_base_illegal_enums_fall_back_and_legal_ones_read_through() {
|
||||
assert_eq!(good.titlebar_mode, TitlebarMode::Never);
|
||||
assert_eq!(good.palette_source, PaletteSource::Wallpaper);
|
||||
}
|
||||
|
||||
/// **M116**:`appearance.window_corner` 的读侧——缺键 = 跟随系统、写 `square` 读回 `square`。
|
||||
///
|
||||
/// 抓得住:`AppearanceBase::from_document` 漏搬这一格(那会让设置页永远显示「跟随系统」,
|
||||
/// 用户点了直角重启后又变回去)。
|
||||
/// 抓不住:这台机器能不能真圆(`app-iced` 的 `platform::plan_corner` 管)。
|
||||
#[test]
|
||||
fn window_corner_round_trips_through_the_document() {
|
||||
assert_eq!(
|
||||
base_of(serde_json::json!({})).window_corner,
|
||||
WindowCorner::System,
|
||||
"缺键 → 出厂跟随系统"
|
||||
);
|
||||
let square = base_of(serde_json::json!({ "appearance": { "window_corner": "square" } }));
|
||||
assert_eq!(square.window_corner, WindowCorner::Square);
|
||||
// 坏值:core 侧落到 system(并留诊断),这里只钉 UI 侧看到的值。
|
||||
let broken = base_of(serde_json::json!({ "appearance": { "window_corner": "rounded" } }));
|
||||
assert_eq!(broken.window_corner, WindowCorner::System);
|
||||
}
|
||||
Reference in new issue
Block a user