feat(appearance): M91 —— 外观下层三键 + win32 整窗 α FFI + unsafe 落点门禁
core(D36/D37/D38 的键与范围)
- appearance.window_opacity(0.08..=1.0,默认 1.0,越界钳位且不报错;下限 D36 定案)
- appearance.titlebar_mode(auto|always|never,默认 auto;判定逻辑不在本 mission)
- appearance.palette_source(background-image|wallpaper|preset,与 design 的 Level::code() 同名)
- schema 1.3.0 → 1.4.0:纯增量、不加迁移步骤(新键出厂值 = 老文件缺键)
- appearance 拆成目录 mod/modes/tests(单文件 ≤400 行),非法枚举由 diagnostics() 给可读回落说明
presenter(只读语义,不破 M89 零 UI 门禁)
- AppearanceBase { window_opacity, titlebar_mode, palette_source } + from_document()
- 单测:缺键→出厂值、越界→钳位、非法枚举→回落且合法值照读
app-iced(D36 唯一 unsafe 落点)
- platform::effective_window_alpha(backend, requested) 纯函数:≤0/非有限 → None(拒绝整窗穿透),
其余夹到 0.08..=1.0;CPU 档同样允许窗口级 α(D36 对 D27 的修订),4 组真值表测试
- platform/win32/transparency.rs:SetLayeredWindowAttributes(LWA_ALPHA) 的 FFI +
alpha_byte 量化 + apply_window_alpha;非 Windows 同签名空实现返回 Unsupported
CI
- guards.yml 新增 unsafe-scope job:全仓 allow(unsafe_code) 文件集合必须恰好等于
crates/app-iced/src/platform/win32/transparency.rs(集合相等,不是黑名单命中)
版本号绊线
- isolation_keys / java_install_keys:字面量 1.3.0 → 1.4.0(绊线保留字面量)
- store / document 的两个关系夹具改成相对 launcher_version() 构造(「更高 minor 只读」/
「同 minor 更高 patch 保留 $schema」),下次 bump 不再假绿或假红
This commit is contained in:
1 parent
006eb6869a
commit
7406785325
15 files changed
+902
-18
No files matched your search
@@ -0,0 +1,114 @@
|
||||
//! 平台相关的窗口能力(P4 / D36):整窗透明度的**判据**与唯一 `unsafe` 落点分居两处。
|
||||
//!
|
||||
//! - 本文件 = 纯判据 [`effective_window_alpha`]:不读环境变量、不调 FFI、不碰窗口句柄,
|
||||
//! 只回答「按这个渲染后端和用户要的 α,最终该给窗口什么值」;
|
||||
//! - [`win32::transparency`] = 唯一允许 `unsafe` 的文件(`SetLayeredWindowAttributes`)。
|
||||
//!
|
||||
//! 为什么判据要单独拎出来:D36 的降级规则(α 下限 0.08 / `≤0` 一律拒绝 / CPU 档也允许
|
||||
//! 窗口级 α)**可以**被断言证明;而 FFI 的**视觉**结果在本仓库的验证环境(RDP + 虚拟显示
|
||||
//! 适配器)上没有参考价值。把可测的那一半独立出来,才有「有测试的判据 + 没测试的观感」这条
|
||||
//! 诚实的分界。
|
||||
|
||||
use unlucky_presenter::prefs::WINDOW_ALPHA_MIN;
|
||||
|
||||
use crate::Backend;
|
||||
|
||||
pub mod win32;
|
||||
|
||||
/// 用户请求的整窗 α → 真正写进窗口的值(纯函数)。
|
||||
///
|
||||
/// - `requested <= 0.0`(含 `-0.0`)→ `None`:**拒绝整窗穿透**。α = 0 表示窗口内容全透明,
|
||||
/// 用户会以为程序消失/卡死,所以这不是「合法的最小值」而是「不给设」(D36)。
|
||||
/// 非有限值(`NaN` / `±∞`)同样 `None`——它们没有合法落点,也不该被 `clamp` 悄悄吞掉。
|
||||
/// - 其余一律夹到 [`WINDOW_ALPHA_MIN`]`..=1.0`。
|
||||
/// - 后端**不**参与「能不能用窗口级 α」的判断:D36 **修订**了 D27 的旧结论——
|
||||
/// CPU 渲染档同样允许整窗 α(CPU 档被限制的是面板 α / 玻璃 / 模糊,不是窗口 α)。
|
||||
/// 因此三个后端分支给出同一个值;写成 `match` 是为了让「将来真分档」只改这一处,
|
||||
/// 而不是让读代码的人以为这里漏了一个分支。
|
||||
#[must_use]
|
||||
pub fn effective_window_alpha(backend: Backend, requested: f64) -> Option<f64> {
|
||||
if !requested.is_finite() || requested <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
match backend {
|
||||
Backend::Gpu | Backend::Cpu | Backend::Auto => Some(requested.clamp(WINDOW_ALPHA_MIN, 1.0)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::effective_window_alpha;
|
||||
use crate::Backend;
|
||||
use unlucky_presenter::prefs::WINDOW_ALPHA_MIN;
|
||||
|
||||
const BACKENDS: [Backend; 3] = [Backend::Gpu, Backend::Cpu, Backend::Auto];
|
||||
|
||||
#[test]
|
||||
fn truth_table_valid_alpha_passes_through_every_backend() {
|
||||
// GPU / CPU × 合法值:四个格子都要 Some(CPU 档不再强制不透明)。
|
||||
for backend in BACKENDS {
|
||||
assert_eq!(
|
||||
effective_window_alpha(backend, 0.5),
|
||||
Some(0.5),
|
||||
"{backend:?}:合法 α 原样返回"
|
||||
);
|
||||
assert_eq!(effective_window_alpha(backend, 1.0), Some(1.0));
|
||||
assert_eq!(
|
||||
effective_window_alpha(backend, WINDOW_ALPHA_MIN),
|
||||
Some(WINDOW_ALPHA_MIN)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truth_table_out_of_range_is_clamped_not_refused() {
|
||||
for backend in BACKENDS {
|
||||
assert_eq!(
|
||||
effective_window_alpha(backend, 0.001),
|
||||
Some(WINDOW_ALPHA_MIN),
|
||||
"{backend:?}:正数低于下限 → 夹到 0.08(钳位,不拒绝)"
|
||||
);
|
||||
assert_eq!(
|
||||
effective_window_alpha(backend, 9.0),
|
||||
Some(1.0),
|
||||
"{backend:?}:高于 1.0 → 夹到 1.0"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truth_table_non_positive_is_refused_on_every_backend() {
|
||||
for backend in BACKENDS {
|
||||
for requested in [0.0, -0.0, -0.01, -1.0] {
|
||||
assert_eq!(
|
||||
effective_window_alpha(backend, requested),
|
||||
None,
|
||||
"{backend:?}:requested={requested} → 拒绝整窗穿透"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truth_table_non_finite_is_refused() {
|
||||
// NaN 用 `<= 0.0` 判不出来(比较恒 false),所以必须先 `is_finite()`;
|
||||
// 否则 NaN 会穿到 `clamp` 里再原样返回,变成「给窗口一个 NaN」。
|
||||
for backend in BACKENDS {
|
||||
for requested in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
|
||||
assert_eq!(
|
||||
effective_window_alpha(backend, requested),
|
||||
None,
|
||||
"{backend:?}:requested={requested} 不是有限值"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpu_tier_is_not_forced_opaque() {
|
||||
// D36 对 D27 的**修订**:旧文档写「CPU 档一律不透明」,这条测试钉住修订后的行为,
|
||||
// 防止谁照着旧文档把 CPU 分支改回 `None`。
|
||||
assert_eq!(effective_window_alpha(Backend::Cpu, 0.5), Some(0.5));
|
||||
assert_eq!(effective_window_alpha(Backend::Gpu, 0.5), Some(0.5));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user