fix(app-iced): M116 r1 复审 —— 口径降级为「不承诺」、修掉伪造读数与两处错注释、圆角下发去重

四条 P1(全是「口径与证据不符」,行为未变):

1. 把「Win11 一定有系统圆角、边缘平滑」从事实降级:`corner.rs` 的平台矩阵改成
   「实测 / 解释 / 推断 / 未验」四列,并明写本机只测到「写入被接受 / 回读 E_INVALIDARG /
   无边框窗口没有 DWM 调整带」;设置页与 golden 的说明句改成「我们**请求**系统圆角,
   是否圆、边缘什么样由合成器决定」,不再承诺结果。
2. 删掉证据目录里那条伪造读数 `readback=2`:本机三种样式回读全是 `E_INVALIDARG`,
   该值物理上产不出来,也不可能由 `format_line` 产出。改成显式的「合成示例(非实测)」,
   并把每一栏标注成真实/示例来源。probe-table 的「没有一条是推断」标题一并拆开,
   结论 4/5 明确标为**解释**(并写明 Δframe 之差是样式位的同义反复,不能反推 DWM 决策)。
3. `keys.rs` 的「旧读新」语义按实际链路改写:1.4.0 读 1.5.0 文件走
   `SchemaCompat::check_url` → `ReadOnlyPreserveSchema` → `SchemaCompat::access` →
   `FileAccess::ReadOnly`,**整份配置只读**(值照读、`store.set` 报 `SettingError::ReadOnly`、
   `save` 跳过),不是「只是少一档」。
4. 两处错注释订正:
   - `hresult_ok` 的理由改成 `S_FALSE` 的通用语义,并明写 `0x7FF8FFA9` **不是** DWM 的返回值
     (本机实测是 `E_INVALIDARG 0x80070057`),那是我早期用 `unsigned_abs()` 打印 HRESULT 的产物;
     对应单测也把该常量标为「合成值,只用来钉规则」。
   - `DWMWA_VISIBLE_FRAME_BORDER_THICKNESS` 的文档改成「本机三样式三态实测全 1,不能当判据,
     仅作诊断读数」。

两条裁定:
- CPU 档在 Win11 仍走 DWM(tower 裁定保留):理由写进 `plan_corner` 文档——D36 约束的是我们自己的
  渲染路径,DWM 圆角是合成器侧的,不增加我方负担;设置页说明同步加了「软件渲染也一样」。
- 「圆角窗口上拖边缩放仍命中」实测未做(RDP 断开、无指针注入手段):mission 里以 note 记为 dropped
  (store 的 task_drop 对已完成任务不生效,工具层面无法取消勾选,已在 note 里写清)。

六条 P2:
1. probe-table 拆「实测 / 解释 / 未验」并删掉绝对化标题。
2. CPU 档裁定见上(任务书由 tower 侧调整)。
3. 拖边缩放任务记账更正见上。
4. **圆角下发去重**:新增 `corner::{CornerApplied, needs_redispatch}`(指纹 = 策略 / 来源 /
   原因码 / 有没有句柄,不含只有真下发才拿得到的读数),`App::apply_window_corner` 改成
   `&mut self` 并在判定未变时**直接返回**(不动窗口、不打日志);`App` 新增 `corner_applied` 字段。
   新增测试 `the_same_policy_is_dispatched_only_once`:连续 20 次同策略只下发/记 1 次,
   换档与「首次拿到句柄」必须重下。
5. `dwm_get_rect` 的 `hr == 0` 改成 `hresult_ok(hr)`,「判据只有一处」名副其实。
6. 报告口径修正(诊断已产出、UI 接线待做——`SettingsBridge` 未暴露 doc,接线要改 presenter 的
   `settings_bridge.rs`,不在本 mission scope),本轮不动代码。

门禁:fmt --check exit 0;clippy(5 个 crate)--all-targets -D warnings 零 warning;
lib 测试 app-iced 419(+1 去重测试)/ core 266 / design 163 / i18n 24 / presenter 129 全绿;
改动文件仍全部 ≤400 行(最大 keys.rs 398)。
This commit is contained in:
AskaEth committed 2026-11-05 06:35:17 +08:00
1 parent 25d860b2b3
commit 41746d1612
10 files changed
+184 -40

No files matched your search

+6
View File
@@ -156,6 +156,11 @@ pub struct App {
pub(super) account: pages::account::Page, pub(super) account: pages::account::Page,
/// 皮肤/头像解码缓存(M114:同一张皮肤只解一次)。 /// 皮肤/头像解码缓存(M114:同一张皮肤只解一次)。
pub(super) avatars: pages::account::avatar::Cache, pub(super) avatars: pages::account::avatar::Cache,
/// 上一次**真下发过**的圆角判定指纹(M116 r1 复审 P2-4:判定没变就不重下、不重记日志)。
///
/// `None` = 还没下发过(首屏 `boot` 那一次)。指纹类型见
/// [`crate::platform::win32::corner::CornerApplied`]。
pub(super) corner_applied: Option<crate::platform::win32::corner::CornerApplied>,
} }
impl App { impl App {
@@ -266,6 +271,7 @@ impl App {
width, width,
), ),
avatars: pages::account::avatar::Cache::default(), avatars: pages::account::avatar::Cache::default(),
corner_applied: None,
}; };
// 配色来源档可能已经指向一张背景图:那时 `Ui` 必须由解析出的方案构造,而不是 baseline。 // 配色来源档可能已经指向一张背景图:那时 `Ui` 必须由解析出的方案构造,而不是 baseline。
app.rebuild_ui(); app.rebuild_ui();
+17 -2
View File
@@ -23,6 +23,7 @@
//! (M116 取消了 8/12/16 固定档)。 //! (M116 取消了 8/12/16 固定档)。
use crate::platform; use crate::platform;
use crate::platform::win32::corner::{self, CornerApplied};
use crate::platform::win32::transparency::{apply_corner, style_summary, CornerOutcome}; use crate::platform::win32::transparency::{apply_corner, style_summary, CornerOutcome};
use super::super::state::App; use super::super::state::App;
@@ -30,14 +31,28 @@ use super::super::state::App;
impl App { impl App {
/// 把当前圆角策略下发到窗口,并打一行可分辨的日志。 /// 把当前圆角策略下发到窗口,并打一行可分辨的日志。
/// ///
/// ## 去重(M116 r1 复审 P2-4)
///
/// 设置页的滑条(整窗 α / 面板 α / 暗化)每动一下都会走一次外观变更,而<b>圆角与那些键无关</b>。
/// 判定完全没变时这里**直接返回**:不动窗口、不打日志。判据是
/// [`corner::needs_redispatch`](纯函数,有「同一策略 N 次只记 1 行」的测试),
/// 指纹只含「策略 / 来源 / 原因码 / 有没有句柄」,不含只有真下发才拿得到的读数。
///
/// 没有句柄时**也打日志**(`window=none`):不能让「没生效」只体现为屏幕上没变化。 /// 没有句柄时**也打日志**(`window=none`):不能让「没生效」只体现为屏幕上没变化。
pub(crate) fn apply_window_corner(&self, context: &str) { /// 句柄从无到有会让指纹变化 ⇒ 首次拿到 HWND 时必然重下一次。
pub(crate) fn apply_window_corner(&mut self, context: &str) {
let policy = self.appearance.window_corner(); let policy = self.appearance.window_corner();
let plan = platform::plan_corner( let plan = platform::plan_corner(
policy, policy,
platform::current_windows_version(), platform::current_windows_version(),
self.appearance.backend, self.appearance.backend,
); );
let next = CornerApplied::new(policy, &plan, self.hwnd.is_some());
if !corner::needs_redispatch(self.corner_applied.as_ref(), &next) {
return;
}
self.corner_applied = Some(next);
// 行的形状只有一处(`corner::format_line`),因为那里能在没有窗口的环境里被单测钉住。 // 行的形状只有一处(`corner::format_line`),因为那里能在没有窗口的环境里被单测钉住。
// 「我们被 DWM 排除」的第一嫌疑是无边框(`WS_POPUP`)与整窗 α(`WS_EX_LAYERED`), // 「我们被 DWM 排除」的第一嫌疑是无边框(`WS_POPUP`)与整窗 α(`WS_EX_LAYERED`),
// 所以 `frame=` / `layered=` 必须和圆角结论在同一行——否则以后只能猜是哪一条。 // 所以 `frame=` / `layered=` 必须和圆角结论在同一行——否则以后只能猜是哪一条。
@@ -50,7 +65,7 @@ impl App {
}; };
eprintln!( eprintln!(
"{}", "{}",
platform::win32::corner::format_line(context, &plan, policy, &outcome, &style) corner::format_line(context, &plan, policy, &outcome, &style)
); );
} }
} }
+73 -15
View File
@@ -9,14 +9,21 @@
//! //!
//! 真正下发给窗口的动作在 [`super::transparency`](全仓唯一 `unsafe` 落点)。 //! 真正下发给窗口的动作在 [`super::transparency`](全仓唯一 `unsafe` 落点)。
//! //!
//! ## 平台矩阵(真值表的出处) //! ## 平台矩阵(哪些是实测、哪些是推断)
//! //!
//! | 平台 | 系统圆角 | 我们做什么 | //! | 平台 | 我们做什么 | 依据级别 |
//! |---|---|---| //! |---|---|---|
//! | Win11 / Server 2025(build ≥ 22000) | 有(DWM 合成器画,抗锯齿) | `DWMWCP_ROUND` | //! | Win11 / Server 2025(build ≥ 22000) | 下发 `DWMWCP_ROUND`(**请求**系统圆角) | 写入被接受 = **本机 API 实测**;「圆没圆 / 边缘什么样」= **未验**(无像素、回读不可用) |
//! | Win10 及更早(build < 22000) | 无(该属性不存在) | 直角;不做 `SetWindowRgn` 毛边默认 | //! | Win10 及更早(build < 22000) | 直角;不做 `SetWindowRgn` 毛边默认 | 「该属性不存在」= **外部文档证据**;本机没有 Win10 |
//! | macOS | 有(`layer.cornerRadius`,系统/合成器决定) | **不主动加**,交给系统 | //! | macOS | 不主动加,交给系统 | **推断**(未真机验) |
//! | Linux X11 / Wayland | 看合成器(客户端管不了外框) | **不承诺**,直角即如实结果 | //! | Linux X11 / Wayland | 不承诺,直角即如实结果 | **推断**(未真机验) |
//!
//! ⚠️ **「Win11 上我们的窗口一定有圆角、边缘平滑」这句话本分支的证据不支持**,别写回去。
//! 本机(Server 2025 build 26100、RDP 会话断开)只测到三件事:
//! ① `DwmSetWindowAttribute(DWMWA_WINDOW_CORNER_PREFERENCE)` 被接受;
//! ② `DwmGetWindowAttribute` 同一属性返回 `E_INVALIDARG`(本会话不支持回读,与窗口形状无关);
//! ③ 无边框窗口没有 DWM 调整带(`EXTENDED_FRAME_BOUNDS − GetWindowRect` = `+0,+0`,有边框是 `+0,−7`)。
//! 「圆角长什么样」归真机验收,证据目录见 `DevDocs/artifacts/m116/`。
//! //!
//! 后两行在本 crate 里落到同一格:探测不到 Windows 版本 ⇒ `UnknownOs` ⇒ 直角。 //! 后两行在本 crate 里落到同一格:探测不到 Windows 版本 ⇒ `UnknownOs` ⇒ 直角。
//! 这是「不承诺」,不是「做不到」——两者的区别写在日志的原因码里。 //! 这是「不承诺」,不是「做不到」——两者的区别写在日志的原因码里。
@@ -29,10 +36,10 @@ use crate::Backend;
/// ///
/// 只有两种来源,且**互相排斥**: /// 只有两种来源,且**互相排斥**:
/// ///
/// | 来源 | 谁画圆角 | 抗锯齿 | 我们做什么 | /// | 来源 | 谁画圆角 | 我们做什么 |
/// |---|---|---|---| /// |---|---|---|
/// | [`CornerSource::Dwm`] | DWM 合成器 | 有(系统画) | 只下发 `DWMWA_WINDOW_CORNER_PREFERENCE` | /// | [`CornerSource::Dwm`] | 系统合成器(是否真画、半径多少都不由我们定,也读不到) | 只下发 `DWMWA_WINDOW_CORNER_PREFERENCE` |
/// | [`CornerSource::Square`] | 没人 | — | 明确下发 `DWMWCP_DONOTROUND`,把「方」也变成可观测的选择 | /// | [`CornerSource::Square`] | 没人 | 明确下发 `DWMWCP_DONOTROUND`,把「方」也变成可观测的选择 |
/// ///
/// **没有 `Self`(自绘圆角)这一档**,这是刻意的(用户定案 2026-11-03 + tower 裁定): /// **没有 `Self`(自绘圆角)这一档**,这是刻意的(用户定案 2026-11-03 + tower 裁定):
/// 窗口是 `decorations=false` + 自绘标题栏 + 整窗 `LWA_ALPHA`,自绘圆角意味着把内容层裁成圆角, /// 窗口是 `decorations=false` + 自绘标题栏 + 整窗 `LWA_ALPHA`,自绘圆角意味着把内容层裁成圆角,
@@ -136,11 +143,14 @@ impl CornerPlan {
/// | `System` | `< 22000` | CPU | `Square` | `CpuTier` | /// | `System` | `< 22000` | CPU | `Square` | `CpuTier` |
/// | `System` | `>= 22000` | 任意 | `Dwm` | `None` | /// | `System` | `>= 22000` | 任意 | `Dwm` | `None` |
/// ///
/// 为什么 CPU 档在 Win11 上**照样**用 DWM 圆角:D36 限制的是**我们**做的合成技巧(面板 α / /// 为什么 CPU 档在 Win11 上**照样**用 DWM 圆角(**tower 2026-11-03 裁定,按此执行**):
/// 玻璃 / 模糊),而 DWM 圆角是系统合成器自己的行为,不消耗我们的渲染预算,也不违反 /// D36 的 CPU 档约束针对的是**我们自己的渲染路径**(面板 α / 玻璃 / 模糊要关、不许拿透明叠加
/// 「CPU 档不碰透明叠加」——把 Win11 的圆角在 CPU 档砍掉等于凭空造一个没人要的降级。 /// 去换效果),而 DWM 圆角是**合成器侧**的效果,不增加我们的 GPU/CPU 绘制负担。所以
/// 反过来在非 DWM 平台上,圆角只剩「自绘裁切 / `SetWindowRgn` 毛边」这两条路,那才是 D36 /// 「CPU 档关掉 Win11 圆角」不是 D36 的要求,而是凭空造一个没人要的降级。
/// 要拦的东西,所以那一格落 `cpu-tier` 而不是 `os-build`(同一个直角,**原因不同**)。 ///
/// 反过来在非 DWM 平台上,圆角只剩「自绘裁切 / `SetWindowRgn` 毛边」这两条**我们自己的**
/// 合成技巧,那才是 D36 要拦的东西 ⇒ 那一格落 `cpu-tier` 而不是 `os-build`
/// (同一个直角,**原因不同**,日志里可分辨)。
#[must_use] #[must_use]
pub fn plan_corner( pub fn plan_corner(
policy: WindowCorner, policy: WindowCorner,
@@ -196,5 +206,53 @@ pub fn format_line(
) )
} }
/// 一次「已经下发过」的**指纹**:只由判定本身与「当时有没有窗口」组成,不含读数。
///
/// 为什么需要它:设置页的滑条(整窗 α / 面板 α / 暗化)每动一下就触发一次外部外观变更,
/// 而圆角与那些键**无关**。没有去重时,拖一次滑条就会刷出一串
/// `[corner] change …`,把「分辨有没有生效」的那条日志淹掉(M116 r1 复审 P2-4)。
///
/// 指纹**刻意不含 `pref=` / `readback=` 这类读数**:那些只有真下发才知道,拿它们参与比较
/// 会变成「自己比较自己」。判定没变就不重下、也不重记——这也是「同一策略 N 次只记 1 行」的判据。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CornerApplied {
/// 当时的策略(`system` / `square`)。
pub policy: WindowCorner,
/// 判定出来的来源。
pub source: CornerSource,
/// 直角时的原因码(`None` = 走 DWM)。
pub excluded: Option<CornerExclusion>,
/// 下发时**有没有窗口句柄**:`false → true` 必须重下(首次拿到 HWND 时才真能设属性)。
pub has_window: bool,
}
impl CornerApplied {
/// 由判定与「有没有句柄」构造指纹。
#[must_use]
pub const fn new(policy: WindowCorner, plan: &CornerPlan, has_window: bool) -> Self {
Self {
policy,
source: plan.source,
excluded: plan.excluded,
has_window,
}
}
}
/// 这一次要不要真的下发 / 记日志(纯函数)。
///
/// `false` = 判定与上次完全一致(策略、来源、原因码、有没有句柄都没变)⇒ **不动窗口、不打日志**。
/// `true` 的四种情形:首次、换档、系统版本变化导致判定变化、首次拿到句柄。
///
/// 注意:判定不变而上次**下发失败**时这里也返回 `false`——失败原因(`pref=none hresult=…`)
/// 已经在那一行日志里了,重复重试只会刷屏;真要重试应该由「换档 / 重启」触发,不是每帧。
#[must_use]
pub fn needs_redispatch(previous: Option<&CornerApplied>, next: &CornerApplied) -> bool {
match previous {
None => true,
Some(previous) => previous != next,
}
}
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@@ -216,3 +216,51 @@ fn the_log_line_shape_is_pinned_with_the_measured_readings() {
"{unknown}" "{unknown}"
); );
} }
/// **同一策略 N 次只下发 / 只记 1 行**(M116 r1 复审 P2-4 的判据)。
///
/// 为什么要有这条:设置页的整窗 α / 面板 α / 暗化滑条每动一下都会触发一次外观变更,
/// 而圆角与它们无关。没有去重时拖一次滑条会刷出一串 `[corner] change …`,
/// 把「分辨有没有生效」的那唯一一行日志淹掉。
///
/// 抓得住:把去重删掉(`needs_redispatch` 恒 `true` 时这里会数到 N)、或指纹里漏掉某一维
/// (换档 / 首次拿到句柄就不重下了)。
/// 抓不住:真进程里到底打了几行(`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};
let os = WindowsVersion::new(10, 0, 26_100);
let plan = plan_corner(WindowCorner::System, Some(os), Backend::Gpu);
// 模拟「滑条被拖动 20 次」:每次都是同一个策略、同一个系统、句柄一直在。
let mut last: Option<CornerApplied> = None;
let mut dispatches = 0;
for _ in 0..20 {
let next = CornerApplied::new(WindowCorner::System, &plan, true);
if needs_redispatch(last.as_ref(), &next) {
dispatches += 1;
last = Some(next);
}
}
assert_eq!(dispatches, 1, "同一策略连按 20 次只该下发/记 1 次");
// 换档必须重下。
let square = plan_corner(WindowCorner::Square, Some(os), Backend::Gpu);
assert!(needs_redispatch(
last.as_ref(),
&CornerApplied::new(WindowCorner::Square, &square, true)
));
// 句柄从无到有必须重下(首次拿到 HWND 时才真能设属性)。
let mut no_window: Option<CornerApplied> = None;
let first = CornerApplied::new(WindowCorner::System, &plan, 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)
));
}
@@ -240,9 +240,13 @@ pub(super) fn dwm_set_u32(hwnd: isize, attribute: u32, value: u32) -> i32 {
/// `HRESULT` 的成功判据:**看严重性位**(bit 31),不是「等于 0」。 /// `HRESULT` 的成功判据:**看严重性位**(bit 31),不是「等于 0」。
/// ///
/// Win32 里成功码不只有 `S_OK`(0):`S_FALSE`(1) 与各种 `S_*` / 信息级码都是成功。 /// Win32 里成功码不只有 `S_OK`(0):`S_FALSE`(1) 与各种 `S_*` / 信息级码都是成功,所以判据必须
/// M116 实测:本机 DWM 回读偏好返回 `0x7FF8FFA9`(正数),`hr == 0` 判据会把它读成失败, /// 是 `hr >= 0`。这条判据与 M116 探针的具体读数无关——它只是 `HRESULT` 的通用语义。
/// 于是「属性接受了吗」变成谜。本函数把这条判据收成一处,测试钉住正负两侧。 ///
/// ⚠️ **别把 `0x7FF8FFA9` 当成 DWM 的返回值**(本分支一度这么写过,已订正):本机
/// `DwmGetWindowAttribute(DWMWA_WINDOW_CORNER_PREFERENCE)` 三种窗口样式实测**全部**返回
/// `E_INVALIDARG`(`0x8007_0057`,负数);`0x7FF8FFA9` 是我早期用 `i32::unsigned_abs()` 打印
/// HRESULT 时的产物(把负数的补码取了绝对值),它既不是 `S_OK` 也不是任何 DWM 信息码。
#[must_use] #[must_use]
pub const fn hresult_ok(hr: i32) -> bool { pub const fn hresult_ok(hr: i32) -> bool {
hr >= 0 hr >= 0
@@ -287,7 +291,8 @@ pub(super) fn dwm_get_rect(hwnd: isize, attribute: u32) -> Option<WinRect> {
u32::try_from(std::mem::size_of::<WinRect>()).unwrap_or(16), u32::try_from(std::mem::size_of::<WinRect>()).unwrap_or(16),
) )
}; };
(hr == 0).then_some(rect) // 与 `dwm_get_u32_hr` 同一条判据:`HRESULT` 看严重性位,不是「等于 0」(judgment 只留一处)。
hresult_ok(hr).then_some(rect)
} }
/// `GetWindowRect`:窗口矩形(屏幕坐标)。失败返回 `None`。 /// `GetWindowRect`:窗口矩形(屏幕坐标)。失败返回 `None`。
@@ -26,8 +26,10 @@ pub const DWMWCP_DONOTROUND: u32 = 1;
pub const DWMWCP_ROUND: u32 = 2; pub const DWMWCP_ROUND: u32 = 2;
/// `DWMWA_VISIBLE_FRAME_BORDER_THICKNESS`:可见边框厚度(像素,Win11 起)。 /// `DWMWA_VISIBLE_FRAME_BORDER_THICKNESS`:可见边框厚度(像素,Win11 起)。
/// ///
/// 这是本机**唯一**能拿到的「DWM 到底把这个窗口当不当有框窗口」的数值信号: /// ⚠️ **本机实测它不能当「圆不圆」的判据**(我最初以为能,被探针否掉了):
/// 圆角窗口带有一条 1px(按 DPI 缩放)的可见边框,直角窗口为 0。 /// 三种窗口样式 × 三种状态(默认 / `DWMWCP_ROUND` / `DWMWCP_DONOTROUND`)读出来**全是 `1`**,
/// 没有任何区分度。保留它只作诊断读数(万一将来在别的机器上有变化),不参与判定。
/// 证据:`DevDocs/artifacts/m116/probe-table.md` 的 `bdef` / `brnd` / `bsq` 三列。
pub const DWMWA_VISIBLE_FRAME_BORDER_THICKNESS: u32 = 37; pub const DWMWA_VISIBLE_FRAME_BORDER_THICKNESS: u32 = 37;
/// `DWMWA_EXTENDED_FRAME_BOUNDS`:DWM 认为的窗口边界(**含**不可见的调整带)。 /// `DWMWA_EXTENDED_FRAME_BOUNDS`:DWM 认为的窗口边界(**含**不可见的调整带)。
pub const DWMWA_EXTENDED_FRAME_BOUNDS: u32 = 9; pub const DWMWA_EXTENDED_FRAME_BOUNDS: u32 = 9;
@@ -161,7 +163,7 @@ pub fn corner_preference_hr(hwnd: isize) -> (Option<u32>, u32) {
} }
} }
/// 读可见边框厚度(像素)。这是本机唯一能拿到的「DWM 把窗口当不当有框窗口」的数值信号。 /// 读可见边框厚度(像素)。**诊断用,不可作判据**(本机三样式三态实测全是 1,见常量文档)。
#[must_use] #[must_use]
pub fn visible_border_thickness(hwnd: isize) -> Option<u32> { pub fn visible_border_thickness(hwnd: isize) -> Option<u32> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@@ -123,22 +123,29 @@ fn m116_corner_controlled_experiment() {
} }
} }
/// **M116 实测踩到的判据错误**:`DwmGetWindowAttribute` 在本机(RDP 断连)回读偏好返回 /// **成功判据的语义**:`HRESULT` 看严重性位(bit 31),不是「等于 0」——`S_FALSE`(1) 也算成功。
/// `0x7FF8FFA9`——**正数**(严重性位为 0 = 成功),但 `hr == 0` 的写法会把它读成失败,
/// 于是「属性到底设上没有」变成猜谜。
/// ///
/// 抓得住:把成功判据写回 `hr == 0`(那会让本机永远报「读不回来」)。 /// ⚠️ 这条测试**不是**「本机 DWM 会返回某个正数」的证据:本机
/// 抓不住:DWM 具体返回哪个信息码(那是运行期读数)。 /// `DwmGetWindowAttribute` 三种窗口样式实测全 `E_INVALIDARG (0x80070057)`(失败)。
/// 下面那个 `0x7FF8_FFA9` 是**合成值**,只用来钉住「非零但严重性位为 0 ⇒ 成功」这条规则,
/// 以及钉住「`unsigned_abs()` 不能用来打印 HRESULT」这个打印坑。
///
/// 抓得住:把成功判据写回 `hr == 0`;把打印改回 `unsigned_abs()`。
/// 抓不住:DWM 在别的机器上返回什么(那是运行期读数)。
#[test] #[test]
fn hresult_success_follows_the_severity_bit_not_equality_with_zero() { fn hresult_success_follows_the_severity_bit_not_equality_with_zero() {
use super::hresult_ok; use super::hresult_ok;
assert!(hresult_ok(0), "S_OK"); assert!(hresult_ok(0), "S_OK");
assert!(hresult_ok(1), "S_FALSE 也是成功"); assert!(hresult_ok(1), "S_FALSE 也是成功");
// 失败码:严重性位为 1(i32 负数)。 assert!(
hresult_ok(0x7FF8_FFA9),
"合成值:非零但严重性位为 0 ⇒ 按 HRESULT 语义是成功(不是 DWM 的实测返回值)"
);
// 失败码:严重性位为 1(i32 负数)。**本机探针实际拿到的就是这个**。
assert!(!hresult_ok(0x8007_0057_u32 as i32), "E_INVALIDARG"); assert!(!hresult_ok(0x8007_0057_u32 as i32), "E_INVALIDARG");
assert!(!hresult_ok(-1)); assert!(!hresult_ok(-1));
// **打印规则**:失败码必须印成 `hr as u32` 的原始位。 // **打印规则**:失败码必须印成 `hr as u32` 的原始位。
// `unsigned_abs()` 会把 `0x8007_0057` 印成 `0x7FF8_FFA9`(M116 实测踩过), // `unsigned_abs()` 会把 `0x8007_0057` 印成 `0x7FF8_FFA9`(M116 踩过,一次误读的源头),
// 于是「E_INVALIDARG」读起来像「一个信息级成功码」,整个证据链就毁了。 // 于是「E_INVALIDARG」读起来像「一个信息级成功码」,整个证据链就毁了。
let e_invalidarg = 0x8007_0057_u32 as i32; let e_invalidarg = 0x8007_0057_u32 as i32;
assert_eq!(e_invalidarg as u32, 0x8007_0057); assert_eq!(e_invalidarg as u32, 0x8007_0057);
+9 -6
View File
@@ -53,12 +53,15 @@ pub const JDK_VENDORS: &[&str] = &["temurin", "oracle", "microsoft", "zulu"];
/// `palette_source`,M91 · D36/D37/D38):仍然只推 minor、不加迁移步骤——三者的出厂值 /// `palette_source`,M91 · D36/D37/D38):仍然只推 minor、不加迁移步骤——三者的出厂值
/// (不透明 / auto / 内置色卡)正是「老文件没有这些键」时的等价物。 /// (不透明 / auto / 内置色卡)正是「老文件没有这些键」时的等价物。
/// `1.5.0` 登记 M116 的窗口圆角策略(`appearance.window_corner`,出厂 `system`): /// `1.5.0` 登记 M116 的窗口圆角策略(`appearance.window_corner`,出厂 `system`):
/// 同一套纯增量语义——老文件缺该键 ⇒ 读默认 `system` ⇒ 跟随系统,与写进文件后行为一致。 /// 同一套纯增量语义——**新读旧**:老文件缺该键 ⇒ 读默认 `system` ⇒ 跟随系统,与写进文件后行为一致。
/// **向前兼容(旧版本读新文件)**:本仓的 [`super::catalog`] 是白名单校验, ///
/// 但读路径 (`SettingDocument::load`) 只**登记**陌生键、不报错也不删除(`RenderMode::Full` /// **旧读新(方向相反,措辞必须精确)**:1.4.0 的客户端读 1.5.0 的文件时,
/// 会把它们原样写回),所以旧版本打开新文件不会崩,只是看不到「直角」这一档; /// [`super::schema::SchemaCompat::check_url`] 走 `found.minor > expected.minor` →
/// 只有**显式 `store.set("appearance.window_corner", …)`** 在旧版本上才会 /// `ReadOnlyPreserveSchema` → [`super::schema::SchemaCompat::access`] → `FileAccess::ReadOnly`。
/// 返回 `SettingError::UnknownKey`(新版本自己不会这么调)。 /// 也就是说**整份配置对它只读**(值照读、`store.set` 一律 `SettingError::ReadOnly`、
/// `save` 跳过),不是「只是少一档」。这是 minor 提升的既定代价——也正是这条键必须推 minor
/// 而不是偷偷塞进 1.4.0 的原因:回退到旧版本会出现「设置改不动」,而原因只在
/// `$schema` 那一行里,不看版本号无从排查。
#[must_use] #[must_use]
pub fn launcher_version() -> SchemaVersion { pub fn launcher_version() -> SchemaVersion {
SchemaVersion::new(1, 5, 0) SchemaVersion::new(1, 5, 0)
+1 -1
View File
@@ -60,7 +60,7 @@ pub(super) const GOLDEN: &[(&str, &str, &str)] = &[
("SettingsAppearanceCorner", "Window corners", "窗口圆角"), ("SettingsAppearanceCorner", "Window corners", "窗口圆角"),
("SettingsAppearanceCornerSystem", "Follow system", "跟随系统"), ("SettingsAppearanceCornerSystem", "Follow system", "跟随系统"),
("SettingsAppearanceCornerSquare", "Square", "直角"), ("SettingsAppearanceCornerSquare", "Square", "直角"),
("SettingsAppearanceCornerHint", "Windows 11 and Windows Server 2025 round the window through the compositor, so the edges stay smooth. Windows 10 keeps square corners. On macOS and Linux the desktop's own rules apply, and we do not draw our own corners.", "Windows 11 与 Windows Server 2025 由系统合成器画圆角,边缘平滑;Windows 10 保持直角;macOS 与 Linux 交给桌面自己的规则,我们不自己画圆角。"), ("SettingsAppearanceCornerHint", "On Windows 11 and Server 2025 we ask the system to round the window; whether it does, and how the edge looks, is up to the compositor. Software rendering does not change that - corners are drawn by the system, not by our renderer. Windows 10 keeps square corners. On macOS and Linux the desktop's own rules apply, and we never draw corners ourselves.", "Windows 11 与 Windows Server 2025 上我们请求系统处理窗口外框圆角:是否圆、边缘什么样由系统合成器决定(软件渲染也一样——圆角由系统画,不由我们的渲染器画);Windows 10 保持直角;macOS 与 Linux 交给桌面自己的规则,我们不自己画圆角。"),
("CoreTextUntranslated", "[system text not translated]", "[未翻译的系统文案]"), ("CoreTextUntranslated", "[system text not translated]", "[未翻译的系统文案]"),
("DownloadTitle", "Version management", "版本管理"), ("DownloadTitle", "Version management", "版本管理"),
("DownloadSubtitle", "Manage versions and loaders: install, upgrade, repair", "管理版本与加载器:安装、升级、修复"), ("DownloadSubtitle", "Manage versions and loaders: install, upgrade, repair", "管理版本与加载器:安装、升级、修复"),
@@ -21,8 +21,8 @@ macro_rules! settings_appearance_entries {
SettingsAppearanceCornerSystem => ("Follow system", "跟随系统"), SettingsAppearanceCornerSystem => ("Follow system", "跟随系统"),
SettingsAppearanceCornerSquare => ("Square", "直角"), SettingsAppearanceCornerSquare => ("Square", "直角"),
SettingsAppearanceCornerHint => ( SettingsAppearanceCornerHint => (
"Windows 11 and Windows Server 2025 round the window through the compositor, so the edges stay smooth. Windows 10 keeps square corners. On macOS and Linux the desktop's own rules apply, and we do not draw our own corners.", "On Windows 11 and Server 2025 we ask the system to round the window; whether it does, and how the edge looks, is up to the compositor. Software rendering does not change that - corners are drawn by the system, not by our renderer. Windows 10 keeps square corners. On macOS and Linux the desktop's own rules apply, and we never draw corners ourselves.",
"Windows 11 与 Windows Server 2025 由系统合成器画圆角,边缘平滑;Windows 10 保持直角;macOS 与 Linux 交给桌面自己的规则,我们不自己画圆角。" "Windows 11 与 Windows Server 2025 上我们请求系统处理窗口外框圆角:是否圆、边缘什么样由系统合成器决定(软件渲染也一样——圆角由系统画,不由我们的渲染器画);Windows 10 保持直角;macOS 与 Linux 交给桌面自己的规则,我们不自己画圆角。"
), ),
} } } }
}; };