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
@@ -66,7 +66,12 @@ impl App {
|
||||
|
||||
match change.key() {
|
||||
// 两个只读下层键:走 schema 指令(`SettingsBridge` 的结构里没有它们)。
|
||||
Some(_) if matches!(change, Change::WindowOpacity(_) | Change::PaletteSource(_)) => {
|
||||
Some(_)
|
||||
if matches!(
|
||||
change,
|
||||
Change::WindowOpacity(_) | Change::PaletteSource(_) | Change::Corner(_)
|
||||
) =>
|
||||
{
|
||||
if let (Some(key), Some(value)) = (change.key(), change.value()) {
|
||||
self.dispatch(AppCommand::SetSetting {
|
||||
key: key.to_owned(),
|
||||
@@ -84,6 +89,8 @@ impl App {
|
||||
} else {
|
||||
self.log_appearance("change", None);
|
||||
}
|
||||
// M116:圆角档位变了要**重新判定 + 重新下发**(与整窗 α 是两条独立的管线)。
|
||||
self.apply_window_corner("change");
|
||||
}
|
||||
|
||||
/// 重读配置后同步外观(热切:外部 CLI / AI 改完 1 秒内生效)。
|
||||
@@ -95,6 +102,8 @@ impl App {
|
||||
self.rebuild_ui();
|
||||
self.view_dirty = true;
|
||||
self.apply_window_alpha("reload");
|
||||
// M116:外部(CLI / AI)改了 `appearance.window_corner` 也要热生效。
|
||||
self.apply_window_corner("reload");
|
||||
}
|
||||
|
||||
/// 把外观写回 `launcher.json`(只改本 mission 管的字段,强调色/玻璃原样保留)。
|
||||
|
||||
@@ -44,6 +44,7 @@ use super::CAPTURE_AT_FRAME;
|
||||
|
||||
mod account;
|
||||
mod agent;
|
||||
mod appearance;
|
||||
mod boot_env;
|
||||
mod content;
|
||||
mod download;
|
||||
@@ -75,6 +76,9 @@ impl App {
|
||||
self.log_appearance("boot", None);
|
||||
}
|
||||
}
|
||||
// M116:圆角放在 α **之后**——α < 1 时那一步才补上 `WS_EX_LAYERED`,
|
||||
// 先打圆角日志会把 `layered=` 记成 `off`(那正是我们要分辨的那一位)。
|
||||
self.apply_window_corner("boot");
|
||||
Task::none()
|
||||
}
|
||||
Message::SystemTheme(mode) => {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
//! M116:窗口圆角的下发落点(`impl App::apply_window_corner`)。
|
||||
//!
|
||||
//! ## 为什么单独成文件
|
||||
//!
|
||||
//! 圆角与整窗 α 是**两条独立的管线**,只是都必须拿到 HWND、都在同一个时点下发:
|
||||
//!
|
||||
//! | 管线 | 判据 | 落点 | 键 |
|
||||
//! |---|---|---|---|
|
||||
//! | 整窗 α(D36) | `platform::effective_window_alpha` | `SetLayeredWindowAttributes` | `appearance.window_opacity` |
|
||||
//! | 圆角(M116) | `platform::plan_corner` | `DwmSetWindowAttribute` | `appearance.window_corner` |
|
||||
//!
|
||||
//! 合在一个函数里会让「α 变了要不要重设圆角」这种问题变成实现细节;分开之后两个判据各自可测。
|
||||
//!
|
||||
//! ## 日志为什么是**一行、六栏**
|
||||
//!
|
||||
//! `corner=policy=… source=dwm|square os=… excluded_reason=… radius=system pref=… readback=… frame=… layered=…`
|
||||
//!
|
||||
//! 用户报的现象是「别的窗口都有圆角,只有我们的没有」。没有这一行,事后无法区分
|
||||
//! 「没生效」「生效了但看不见」「这台系统根本没有这个能力」;`frame` / `layered` 两栏则是
|
||||
//! 「我们被 DWM 排除」的第一嫌疑(`WS_POPUP` 无边框、`WS_EX_LAYERED` 整窗 α)的现场读数。
|
||||
//!
|
||||
//! `radius=system` 是刻意的:系统不暴露 DWM 的圆角半径,我们也不自己定一个数字
|
||||
//! (M116 取消了 8/12/16 固定档)。
|
||||
|
||||
use crate::platform;
|
||||
use crate::platform::win32::transparency::{apply_corner, style_summary, CornerOutcome};
|
||||
|
||||
use super::super::state::App;
|
||||
|
||||
impl App {
|
||||
/// 把当前圆角策略下发到窗口,并打一行可分辨的日志。
|
||||
///
|
||||
/// 没有句柄时**也打日志**(`window=none`):不能让「没生效」只体现为屏幕上没变化。
|
||||
pub(crate) fn apply_window_corner(&self, context: &str) {
|
||||
let policy = self.appearance.window_corner();
|
||||
let plan = platform::plan_corner(
|
||||
policy,
|
||||
platform::current_windows_version(),
|
||||
self.appearance.backend,
|
||||
);
|
||||
// 行的形状只有一处(`corner::format_line`),因为那里能在没有窗口的环境里被单测钉住。
|
||||
// 「我们被 DWM 排除」的第一嫌疑是无边框(`WS_POPUP`)与整窗 α(`WS_EX_LAYERED`),
|
||||
// 所以 `frame=` / `layered=` 必须和圆角结论在同一行——否则以后只能猜是哪一条。
|
||||
let (outcome, style) = match self.hwnd {
|
||||
Some(hwnd) => (apply_corner(hwnd, &plan).describe(), style_summary(hwnd)),
|
||||
None => (
|
||||
CornerOutcome::NoWindow.describe(),
|
||||
"frame=? layered=?".to_owned(),
|
||||
),
|
||||
};
|
||||
eprintln!(
|
||||
"{}",
|
||||
platform::win32::corner::format_line(context, &plan, policy, &outcome, &style)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user