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
@@ -8,6 +8,12 @@
|
||||
/// 背景图填充模式(09 §101)。直接复用 core 的枚举:取值与落盘词表只有一处真相。
|
||||
pub use unlucky_core::setting::appearance::BgFill;
|
||||
|
||||
/// 外观下层的三张表/常量(D36·D37·D38):复用 core 的定义,一处真相。
|
||||
///
|
||||
/// [`WINDOW_ALPHA_MIN`] 还要给 `unlucky-app-iced` 的 win32 α 层用(它只依赖本 crate,不直接依赖
|
||||
/// core):窗口 α 的硬底必须与设置侧同源,两边各写一个 `0.08` 迟早会漂。
|
||||
pub use unlucky_core::setting::appearance::{PaletteSource, TitlebarMode, WINDOW_ALPHA_MIN};
|
||||
|
||||
use crate::dto::VersionCategory;
|
||||
|
||||
/// 预设强调色板(09 §104)。**必须**与 UI 侧设置页「外观」分组的色板控件同序同值:
|
||||
@@ -162,6 +168,51 @@ impl AppearancePrefs {
|
||||
}
|
||||
}
|
||||
|
||||
/// 外观下层的**只读**快照:整窗 α / 标题栏三态 / 配色来源(D36·D37·D38)。
|
||||
///
|
||||
/// 不并进 [`AppearancePrefs`] 的两个理由:那个结构体由 `settings_bridge` 逐字段字面量
|
||||
/// 构造(加字段就得改那边),且这三个键只读不写——落盘仍走 core 的 `Appearance::write`。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct AppearanceBase {
|
||||
/// 整窗透明度 [`WINDOW_ALPHA_MIN`]~1.0(1.0 = 完全不透明)
|
||||
pub window_opacity: f32,
|
||||
/// 标题栏三态(`auto` 的具体判定属于 UI 侧,本 crate 只给值)
|
||||
pub titlebar_mode: TitlebarMode,
|
||||
/// 配色来源档(三级编排在 `unlucky-design`,本 crate 只给值)
|
||||
pub palette_source: PaletteSource,
|
||||
}
|
||||
|
||||
impl AppearanceBase {
|
||||
/// 出厂态:不透明 / `auto` / 内置色卡。
|
||||
#[must_use]
|
||||
pub fn factory() -> Self {
|
||||
Self {
|
||||
window_opacity: 1.0,
|
||||
titlebar_mode: TitlebarMode::Auto,
|
||||
palette_source: PaletteSource::Preset,
|
||||
}
|
||||
}
|
||||
|
||||
/// 从配置文档读一份快照:缺键 → 出厂默认、越界 → 钳位、非法枚举 → 回落。
|
||||
///
|
||||
/// 三条语义都在 core 里落地(本函数只搬运,不重新判一遍)。
|
||||
#[must_use]
|
||||
pub fn from_document(doc: &unlucky_core::setting::document::SettingDocument) -> Self {
|
||||
let appearance = unlucky_core::setting::appearance::Appearance::read(doc);
|
||||
Self {
|
||||
window_opacity: appearance.window_opacity as f32,
|
||||
titlebar_mode: appearance.titlebar_mode,
|
||||
palette_source: appearance.palette_source,
|
||||
}
|
||||
}
|
||||
|
||||
/// 整窗 α 是否需要真的落到窗口上(1.0 = 不透明,调 FFI 纯属多余)。
|
||||
#[must_use]
|
||||
pub fn is_transparent(&self) -> bool {
|
||||
self.window_opacity < 1.0
|
||||
}
|
||||
}
|
||||
|
||||
/// 需要跨会话记忆的 UI 偏好。
|
||||
///
|
||||
/// 注意:`Eq` 不成立([`AppearancePrefs`] 带 `f32` 滑条值),用到的地方按 `PartialEq` 比。
|
||||
@@ -226,9 +277,15 @@ pub mod keys {
|
||||
pub const ACCENT_COLOR: &str = "appearance.accent_color";
|
||||
/// 下载页分类
|
||||
pub const DOWNLOAD_CATEGORY: &str = "ui.download.category";
|
||||
/// 整窗透明度(D36;下限见 [`super::WINDOW_ALPHA_MIN`])
|
||||
pub const WINDOW_OPACITY: &str = "appearance.window_opacity";
|
||||
/// 标题栏三态(D37)
|
||||
pub const TITLEBAR_MODE: &str = "appearance.titlebar_mode";
|
||||
/// 配色来源档(D38)
|
||||
pub const PALETTE_SOURCE: &str = "appearance.palette_source";
|
||||
|
||||
/// 本模块登记的全部键(自检/迁移用)。
|
||||
pub const ALL: [&str; 13] = [
|
||||
pub const ALL: [&str; 16] = [
|
||||
NAV_COLLAPSED,
|
||||
AI_COLLAPSED,
|
||||
TILE_SIZE,
|
||||
@@ -242,12 +299,18 @@ pub mod keys {
|
||||
GLASS,
|
||||
ACCENT_COLOR,
|
||||
DOWNLOAD_CATEGORY,
|
||||
WINDOW_OPACITY,
|
||||
TITLEBAR_MODE,
|
||||
PALETTE_SOURCE,
|
||||
];
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{keys, InstanceView, ThemeMode, TileSize, UiPrefs};
|
||||
use super::{
|
||||
keys, AppearanceBase, InstanceView, PaletteSource, ThemeMode, TileSize, TitlebarMode,
|
||||
UiPrefs, WINDOW_ALPHA_MIN,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn defaults_match_d16() {
|
||||
@@ -285,4 +348,51 @@ mod tests {
|
||||
keys.dedup();
|
||||
assert_eq!(keys.len(), total);
|
||||
}
|
||||
|
||||
fn base_of(raw: serde_json::Value) -> AppearanceBase {
|
||||
let schema = unlucky_core::setting::keys::launcher_schema();
|
||||
let (doc, _) = unlucky_core::setting::document::SettingDocument::load(&schema, &raw);
|
||||
AppearanceBase::from_document(&doc)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appearance_base_missing_keys_use_factory() {
|
||||
let base = base_of(serde_json::json!({}));
|
||||
assert_eq!(
|
||||
base,
|
||||
AppearanceBase::factory(),
|
||||
"缺键 → 出厂态(不透明 / auto / preset)"
|
||||
);
|
||||
assert!(!base.is_transparent(), "出厂不透明 → 不下发窗口 α");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appearance_base_clamps_out_of_range_opacity() {
|
||||
for raw in [0.0, 0.02, -1.0] {
|
||||
let base = base_of(serde_json::json!({ "appearance": { "window_opacity": raw } }));
|
||||
assert_eq!(
|
||||
base.window_opacity, WINDOW_ALPHA_MIN as f32,
|
||||
"raw={raw} 应夹到下限"
|
||||
);
|
||||
}
|
||||
for (raw, want) in [(0.6, 0.6), (3.0, 1.0)] {
|
||||
let base = base_of(serde_json::json!({ "appearance": { "window_opacity": raw } }));
|
||||
assert_eq!(base.window_opacity, want, "raw={raw}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appearance_base_illegal_enums_fall_back_and_legal_ones_read_through() {
|
||||
let bad = base_of(serde_json::json!({
|
||||
"appearance": { "titlebar_mode": "allways", "palette_source": "desktop" }
|
||||
}));
|
||||
assert_eq!(bad.titlebar_mode, TitlebarMode::Auto, "非法 → auto");
|
||||
assert_eq!(bad.palette_source, PaletteSource::Preset, "非法 → preset");
|
||||
// 合法值必须照读:否则「回落」测试会因为实现恒回默认而假绿。
|
||||
let good = base_of(serde_json::json!({
|
||||
"appearance": { "titlebar_mode": "never", "palette_source": "wallpaper" }
|
||||
}));
|
||||
assert_eq!(good.titlebar_mode, TitlebarMode::Never);
|
||||
assert_eq!(good.palette_source, PaletteSource::Wallpaper);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user