Unlucky Launcher v0.2.1
Release / 选槽位(默认两个 Windows) (push) Canceled after 0s
Release / build (${{ matrix.slot }}) (push) Canceled after 0s
Release / GitHub Release (push) Canceled after 0s

Single-commit source snapshot of the v0.2.1 release.
This commit is contained in:
AskaEth committed 2026-11-06 12:35:56 +08:00
commit d517f28140
1092 files changed
+211068

No files matched your search

+219
View File
@@ -0,0 +1,219 @@
//! # 外观接线(M93 / APPEARWIRE):**键 → 像素**
//!
//! 本模块把 `appearance.*` 那批键接到真正的窗口与真正的像素上,并把「哪里是判据、哪里是观感」
//! 分开写清楚。底座(M91)已经给了键、钳位判据与唯一的不安全落点;这里补的是**调用方**。
//!
//! ## 两层透明度(D34 / D36)
//!
//! | 层 | 作用范围 | 实现 | CPU 档 | 下限 |
//! |---|---|---|---|---|
//! | 整窗 α | 整扇窗口(标题栏 + 导航轨 + 内容 + 卡片) | `SetLayeredWindowAttributes(LWA_ALPHA)`,DWM 常数乘法 | **允许** | 0.08(α=0 = 整窗穿透,拒绝) |
//! | 面板 α | 只有面板/卡片表面这一层(背景图能透出来) | 容器色的 alpha([`section`] / `shell`) | **强制 1.0** | 0.4 |
//!
//! 真值表在 [`alpha`] 的模块文档里(三层 × 三个后端,含「钳位还是拒绝」)。
//!
//! ## 模块分工
//!
//! | 文件 | 是什么 | 有没有 IO |
//! |---|---|---|
//! | [`alpha`] | 两层 α 的钳位链、CPU 档降级、滑块映射、下发字节 | 无(纯函数) |
//! | [`background`] | 背景图解码(png/jpeg/bmp)、平铺网格、暗化系数、可读诊断 | 读盘一次(`load`) |
//! | `palette` | D38 三级优先序的调用侧 + 提示文案 | 无(像素由 `background` 给) |
//! | `state` | 运行期状态:把上面三件的输入输出连起来 | 只在 `refresh_background` |
//! | `section` | 设置页「外观」分组(真控件) | 无 |
//! | `picture` | 背景层 Element:真图直出(Cover/Contain/Tile)+ 暗化叠层 | 无 |
//!
//! ## 偏离登记(D30 要求逐条写清)
//!
//! | # | 偏离 | 为什么 | 什么时候收口 |
//! |---|---|---|---|
//! | 1 | ~~背景图按 36dp 马赛克画~~ → **r2 已销**:iced 的 `image-without-codecs` 打开,真图直出(`ContentFit` + 平铺网格) | r1 时 `Cargo.toml` 不在 scope;M93 r2 由 tower 扩 scope 解决 | ✅ |
//! | 2 | ~~面板 α 只覆盖外壳层~~ → **r2 已销**:α 收敛到 `Ui::layer`(唯一施加点),`frame::card` / 页头带 / 外壳 / 外观卡片自动同源 | M93 r2 扩了 `pages/frame.rs` + `theme.rs` 的 scope | ✅ |
//! | 3 | 桌面壁纸取色(第二级)永远 `Absent` | 壁纸探测(M73)还没落地;优先序本身仍按 design 的三级走,测试里用合成像素证明第二级会被用到 | M73 |
//! | 4 | 色卡档**不换** `Ui` 的 scheme | `resolve_source` 的色卡档返回 `DEFAULT_CARD`(天蓝卡)的方案,与令牌 baseline 逐色不同;应用它会把全部既有验收图的像素改掉。取色档(①②)才换 | 色卡选择 UI 落地时一并决议 |
//! | 5 | `Backend::Auto` 不触发 CPU 档降级 | 「强制 1.0」是 CPU 渲染档的规则,而 `Auto` 是「探测前的请求值」;探测结果没有回灌 `Backend` | 让 `probe` 把结果回灌 `Options::backend` |
//! | 6 | ~~背景图只支持 png~~ → **r2 已销**:`image` 补了 `jpeg`/`bmp`,单测与截图都用 `.jpg` 举证 | 同上 | ✅ |
//! | 7 | `bg_dim` 是「往**面板底色**退」,不是「叠黑」 | 浅色主题下叠黑会把亮图压成暗块,深色正文反而更读不清;向底色退在两种主题下都单调变好(见 [`background::dim_alpha`]) | 若将来为了「夜间压暗」需要真叠黑,加一档独立键(不要复用这个) |
//! | 8 | ~~页头文字压在画布上~~ → **r2 已销**:`frame::header_bar` 加了面板底色 | M93 r2 扩了 `pages/frame.rs` 的 scope | ✅ |
//!
//! ## 取证
//!
//! 每一处改动都打一行 [`state::State::log_line`]:`[appearance] window_alpha=… panel_alpha=…
//! byte=… corner=…`;圆角另有一条独立管线,行形如
//! `[corner] <ctx> corner=policy=system source=dwm os=10.0.26100 excluded_reason=none
//! radius=system pref=2 readback=? read_hr=0x80070057 frame=overlapped layered=on
//! suppressed_by=window_alpha`(真进程实跑的两条见 `DevDocs/artifacts/m116/README.md`;
//! `frame=` 恒为 `overlapped`/`other`,**不会是 `popup`**)
//! (见 `app::update::appearance` 与 `platform::win32::corner::format_line`)。数值是**下发前**的
//! 字节/属性值(不是观感),因为在 RDP + 虚拟显示适配器上「看上去多透明 / 圆不圆」不可信。
//! 截图矩阵用 `ULL_APPEARANCE_*` 环境变量灌值(见 [`state`] 的常量)。
use iced::widget::container;
use iced::{Background, Border, Element, Length, Padding};
use unlucky_design::color::Role;
use unlucky_design::shape::ShapeScale;
use unlucky_design::spacing::Space;
use unlucky_design::typography::TypeRole;
use crate::pages::frame;
use crate::pages::Message;
use crate::theme::Ui;
pub mod alpha;
pub mod background;
mod palette;
mod picture;
mod section;
mod state;
/// 状态提示的 **tonal notice**(`section.rs` 的 400 行硬顶把它放在模块根)。
///
/// `SurfaceContainerHighest` 底 + `ShapeScale::Small` 圆角 + 两侧 `X2` 内边距,文字 `OnSurfaceVariant`。
/// 用它而不是「换色的正文」:容器才是 M3 表达「当前状态」的形态,也和说明段在**形态**上分开
/// (不靠色相、不新增令牌)。调用点见 [`section::section`] 的圆角一节。
fn notice<'a>(ui: &'a Ui, text: &str) -> Element<'a, Message> {
let background = ui.color(Role::SurfaceContainerHighest);
let tone = ui.color(Role::OnSurfaceVariant);
let body = frame::text_role_owned(ui, TypeRole::BodySmall, true, text.to_owned(), tone);
let pad = ui.space(Space::X2);
let radius = ui.radius_dp(ShapeScale::Small);
container(body)
.width(Length::Fill)
.padding(Padding::from([ui.space(Space::X1), pad]))
.style(move |_theme| container::Style {
background: Some(Background::Color(background)),
border: Border {
radius,
..Border::default()
},
..container::Style::default()
})
.into()
}
pub use picture::layer;
pub use section::section;
pub use state::State;
/// 设置页「外观」分组在左列里的下标(0 = General,1 = Appearance …)。
pub const APPEARANCE_GROUP: usize = 1;
/// 设置页「外观」分组的显示名。
pub const GROUP_LABEL: &str = "Appearance";
/// 一次外观改动(设置页 → `App`)。
///
/// 值都是**用户请求值**(未钳位):钳位与降级只发生在 [`alpha`],这样「用户设了 0.5、CPU 档实际
/// 1.0」这种差别在状态里能看出来,日志也就不会把请求值写成生效值。
#[derive(Debug, Clone, PartialEq)]
pub enum Change {
/// 整窗透明度(请求值,0.08~1.0)。
WindowOpacity(f32),
/// 面板透明度(请求值,0.4~1.0)。
PanelAlpha(f32),
/// 背景图开关。
BgEnabled(bool),
/// 背景图路径(空串 = 清掉)。
BgImage(String),
/// 填充档。
BgFill(unlucky_presenter::prefs::BgFill),
/// 暗化强度(0~0.95)。
BgDim(f32),
/// 配色来源档。
PaletteSource(unlucky_presenter::prefs::PaletteSource),
/// 窗口圆角档位(M116:跟随系统 / 直角)。
Corner(unlucky_presenter::prefs::WindowCorner),
/// 设置页切分组(左列点选)。
SelectGroup(usize),
}
impl Change {
/// 这条改动对应哪个 schema 键(写回配置用;`SelectGroup` 不落盘)。
#[must_use]
pub fn key(&self) -> Option<&'static str> {
Some(match self {
Self::WindowOpacity(_) => unlucky_presenter::prefs::keys::WINDOW_OPACITY,
Self::PanelAlpha(_) => unlucky_presenter::prefs::keys::PANEL_ALPHA,
Self::BgEnabled(_) => unlucky_presenter::prefs::keys::BG_IMAGE_ENABLED,
Self::BgImage(_) => unlucky_presenter::prefs::keys::BG_IMAGE,
Self::BgFill(_) => unlucky_presenter::prefs::keys::BG_FILL,
Self::BgDim(_) => unlucky_presenter::prefs::keys::BG_DIM,
Self::PaletteSource(_) => unlucky_presenter::prefs::keys::PALETTE_SOURCE,
Self::Corner(_) => unlucky_presenter::prefs::keys::WINDOW_CORNER,
Self::SelectGroup(_) => return None,
})
}
/// 这条改动的落盘字符串形态(与 `schema` 的取值词表逐字一致)。
///
/// `None` = 不落盘(分组选择)。数字一律给 `f64` 的十进制字面量(`store.set` 按 schema 类型转)。
#[must_use]
pub fn value(&self) -> Option<String> {
Some(match self {
Self::WindowOpacity(alpha) => format!("{alpha}"),
Self::PanelAlpha(alpha) => format!("{alpha}"),
Self::BgEnabled(enabled) => enabled.to_string(),
Self::BgImage(path) => path.clone(),
Self::BgFill(fill) => fill.as_str().to_owned(),
Self::BgDim(dim) => format!("{dim}"),
Self::PaletteSource(source) => source.as_str().to_owned(),
Self::Corner(corner) => corner.as_str().to_owned(),
Self::SelectGroup(_) => return None,
})
}
}
#[cfg(test)]
mod tests {
use super::{Change, APPEARANCE_GROUP};
use unlucky_presenter::prefs::{keys, BgFill, PaletteSource};
/// 抓得住:每条改动都点名到自己那个 schema 键(写错键 = 静默写到别的配置项上)。
/// 抓不住:这些键是否真的在 core 的 schema 白名单里(那由 presenter/core 的测试守)。
#[test]
fn every_change_names_its_own_schema_key() {
assert_eq!(APPEARANCE_GROUP, 1, "左列第 1 项是 Appearance");
let cases = [
(Change::WindowOpacity(0.8), keys::WINDOW_OPACITY),
(Change::PanelAlpha(0.6), keys::PANEL_ALPHA),
(Change::BgEnabled(true), keys::BG_IMAGE_ENABLED),
(Change::BgImage("D:/x.png".to_owned()), keys::BG_IMAGE),
(Change::BgFill(BgFill::Tile), keys::BG_FILL),
(Change::BgDim(0.5), keys::BG_DIM),
(
Change::PaletteSource(PaletteSource::Wallpaper),
keys::PALETTE_SOURCE,
),
(
Change::Corner(unlucky_presenter::prefs::WindowCorner::Square),
keys::WINDOW_CORNER,
),
];
for (change, key) in cases {
assert_eq!(change.key(), Some(key), "{change:?}");
assert!(change.value().is_some());
}
assert_eq!(Change::SelectGroup(2).key(), None, "切分组不落盘");
assert_eq!(Change::SelectGroup(2).value(), None);
}
/// 抓得住:落盘字符串必须是 schema 认识的词表(枚举档写错就整条配置读回默认值)。
/// 抓不住:数值是否在范围内(钳位在 core 侧,越界写入会被 schema 拒绝)。
#[test]
fn persisted_values_use_the_schema_vocabulary() {
assert_eq!(
Change::BgFill(BgFill::Contain).value().as_deref(),
Some("contain")
);
assert_eq!(
Change::PaletteSource(PaletteSource::BackgroundImage)
.value()
.as_deref(),
Some("background-image")
);
assert_eq!(Change::BgEnabled(false).value().as_deref(), Some("false"));
assert_eq!(Change::WindowOpacity(0.8).value().as_deref(), Some("0.8"));
}
}