feat(app-iced): M93 —— 外观接线:整窗 α / 面板 α / 背景图 / 配色来源
把 appearance.* 那批键接到窗口与像素上(M91 只落了键与判据,零调用方)。
整窗 α(D34/D36)
- `ensure_layered(hwnd)` 落在唯一 unsafe 文件 `platform/win32/transparency.rs`
(`WS_EX_LAYERED` 只有 Get/SetWindowLongPtrW 能加,且 CI 的 unsafe-scope 是集合相等);
`app/boot.rs` 只负责开窗,句柄到手后由 `Message::WindowHandle` → `window_alpha::dispatch`
先补样式再下发。本机实测 `layered=ok outcome=Applied { byte: 205 }`(α=0.8)。
- 三条入口都会重算并下发:启动(句柄到达)、设置页改动、配置文件热切。
- `layered` 日志有三态:`n/a`(α≥1,压根不需要)/ `ok` / `FAILED`,避免把「没用到」写成假红。
面板 α 与 CPU 档(D36)
- 面板 α 独立于整窗 α:只作用到外壳层(根容器 / 标题栏 / 导航轨)与外观卡片;
CPU 档强制 1.0、玻璃强制关(`pages/appearance/alpha.rs` 的真值表 + 单测)。
- 色卡档继续走令牌 baseline,只有取色档(①②级)才换 `Ui` 的 scheme(`Ui::from_scheme`)。
背景图(D38)
- 读盘解码(png)→ cover/contain/tile 三种填充几何 → `bg_dim` 向底色退 → 36dp 网格马赛克;
iced 的 `image` widget 需要 `iced/image` feature(Cargo.toml 不在 scope),故为已登记降级。
- 读盘失败静默回落 + 一条可读诊断(设置页错误行 + 日志),配色经 `Provenance` 回落色卡。
设置页「外观」分组
- 整窗透明度滑块(下限 0.08)/ 面板透明度(CPU 档禁用并写明 55%→100% 的降级)/
背景图开关与路径 / 填充三档 / 暗化 / 配色来源三档;改完即时生效并写回:
bridge 覆盖的 5 个键走 `SettingsBridge::persist`,`window_opacity` / `palette_source`
走 `AppCommand::SetSetting`(每个键只有一个写者)。
- `SettingsBridge::load_base()`(presenter)让 app-iced 不需要直接依赖 core 就能读回
`window_opacity` / `palette_source`。
取证
- `ULL_APPEARANCE_*` / `ULL_START_ROUTE` 只影响本次进程(语义同 ULL_TICK_TRACE),
截图通道不读用户配置(D30)。
This commit is contained in:
1 parent
eae7ae0ff4
commit
b8ab46662c
24 files changed
+3119
-48
No files matched your search
@@ -0,0 +1,386 @@
|
||||
//! 外观的**运行期状态**:配置值 → 生效值(钳位 / CPU 档降级)→ 要下发的字节与要画的格子。
|
||||
//!
|
||||
//! 这一层是「键 → 像素」的中间站,App 持有一份(`crate::app::state::App::appearance`):
|
||||
//!
|
||||
//! ```text
|
||||
//! launcher.json ──SettingsBridge::load/load_base──┐
|
||||
//! ▼
|
||||
//! State(本文件)
|
||||
//! ┌────────────┴────────────┐
|
||||
//! 整窗 α → 字节 │ │ 面板 α / 背景图 → Element
|
||||
//! ▼ ▼
|
||||
//! app::window_alpha(FFI) shell / 设置页(像素)
|
||||
//! ```
|
||||
//!
|
||||
//! **纯的与不纯的**分界刻意写在这里:钳位链([`super::alpha`])、几何([`super::background`])、
|
||||
//! 优先序([`super::palette`])都是纯函数;本文件只把它们的输入输出连起来,并把「读盘失败」
|
||||
//! 变成一条**可读诊断**而不是一个 `unwrap`。
|
||||
|
||||
use unlucky_design::color::{Scheme, SchemeFlavor};
|
||||
use unlucky_presenter::prefs::{AppearanceBase, AppearancePrefs, BgFill, PaletteSource};
|
||||
|
||||
use crate::Backend;
|
||||
|
||||
use super::alpha::{self, effective_glass, effective_panel_alpha, panel_is_forced_opaque};
|
||||
use super::background::{self, Image, Mosaic};
|
||||
use super::palette::{self, Resolved, Source};
|
||||
|
||||
/// 取证开关的环境变量名(截图矩阵与真机日志用;生产默认全部不设)。
|
||||
pub const ENV_WINDOW_ALPHA: &str = "ULL_APPEARANCE_WINDOW_ALPHA";
|
||||
/// 面板 α。
|
||||
pub const ENV_PANEL_ALPHA: &str = "ULL_APPEARANCE_PANEL_ALPHA";
|
||||
/// 背景图路径。
|
||||
pub const ENV_BG_IMAGE: &str = "ULL_APPEARANCE_BG_IMAGE";
|
||||
/// 填充档(`cover` / `contain` / `tile`)。
|
||||
pub const ENV_BG_FILL: &str = "ULL_APPEARANCE_BG_FILL";
|
||||
/// 暗化强度。
|
||||
pub const ENV_BG_DIM: &str = "ULL_APPEARANCE_BG_DIM";
|
||||
/// 配色来源档(`background-image` / `wallpaper` / `preset`)。
|
||||
pub const ENV_PALETTE: &str = "ULL_APPEARANCE_PALETTE";
|
||||
|
||||
/// 外观的运行期状态。
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct State {
|
||||
/// 用户请求的整窗 α(未钳位原值;下发前过 [`super::alpha`])。
|
||||
pub window_opacity: f32,
|
||||
/// 用户请求的面板 α(CPU 档会被强制成 1.0,见 [`State::effective_panel_alpha`])。
|
||||
pub panel_alpha: f32,
|
||||
/// 背景图总开关。
|
||||
pub bg_enabled: bool,
|
||||
/// 背景图路径(空 = 未设置)。
|
||||
pub bg_path: String,
|
||||
/// 填充档。
|
||||
pub bg_fill: BgFill,
|
||||
/// 暗化强度。
|
||||
pub bg_dim: f32,
|
||||
/// 配色来源档。
|
||||
pub palette_source: PaletteSource,
|
||||
/// 玻璃拟态开关(CPU 档强制关)。
|
||||
pub glass: bool,
|
||||
/// 渲染后端(决定 CPU 档的两条降级)。
|
||||
pub backend: Backend,
|
||||
/// 设置页当前分组(0 = General … 1 = Appearance;见 [`super::APPEARANCE_GROUP`])。
|
||||
pub selected_group: usize,
|
||||
/// 已解码的背景图(`None` = 没设 / 读失败)。
|
||||
image: Option<Image>,
|
||||
/// 读盘/解码失败的**可读诊断**(按发生顺序追加)。
|
||||
diagnostics: Vec<String>,
|
||||
/// 上一次的配色解析结果。
|
||||
resolved: Option<Resolved>,
|
||||
/// 当前深浅(配色解析的输入)。
|
||||
flavor: SchemeFlavor,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// 从配置读一份(启动时调一次)。
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
prefs: &AppearancePrefs,
|
||||
base: &AppearanceBase,
|
||||
backend: Backend,
|
||||
flavor: SchemeFlavor,
|
||||
) -> Self {
|
||||
let mut state = Self {
|
||||
window_opacity: base.window_opacity,
|
||||
panel_alpha: prefs.panel_alpha,
|
||||
bg_enabled: prefs.bg_image_enabled,
|
||||
bg_path: prefs.bg_image.clone(),
|
||||
bg_fill: prefs.bg_fill,
|
||||
bg_dim: prefs.bg_dim,
|
||||
palette_source: base.palette_source,
|
||||
glass: prefs.glass,
|
||||
backend,
|
||||
selected_group: super::APPEARANCE_GROUP,
|
||||
image: None,
|
||||
diagnostics: Vec::new(),
|
||||
resolved: None,
|
||||
flavor,
|
||||
};
|
||||
state.refresh_background();
|
||||
state.refresh_palette();
|
||||
state
|
||||
}
|
||||
|
||||
/// 截图/真机取证用:把 `ULL_APPEARANCE_*` 灌进来(不设 = 不动)。
|
||||
///
|
||||
/// 为什么用环境变量而不是 CLI 参数:`crates/app-iced/src/cli.rs` 不在本 mission 的 scope。
|
||||
/// 语义与既有取证开关(`ULL_TICK_TRACE`)一致:**只影响这次进程**,不落盘。
|
||||
pub fn apply_env(&mut self) {
|
||||
if let Some(value) = env_f32(ENV_WINDOW_ALPHA) {
|
||||
self.window_opacity = value;
|
||||
}
|
||||
if let Some(value) = env_f32(ENV_PANEL_ALPHA) {
|
||||
self.panel_alpha = value;
|
||||
}
|
||||
if let Some(value) = std::env::var_os(ENV_BG_IMAGE) {
|
||||
let path = value.to_string_lossy().to_string();
|
||||
self.bg_enabled = !path.trim().is_empty();
|
||||
self.bg_path = path;
|
||||
}
|
||||
if let Some(value) = std::env::var_os(ENV_BG_FILL) {
|
||||
self.bg_fill = BgFill::from_key_value(&value.to_string_lossy());
|
||||
}
|
||||
if let Some(value) = env_f32(ENV_BG_DIM) {
|
||||
self.bg_dim = value;
|
||||
}
|
||||
if let Some(value) = std::env::var_os(ENV_PALETTE) {
|
||||
self.palette_source = PaletteSource::from_key_value(&value.to_string_lossy());
|
||||
}
|
||||
self.refresh_background();
|
||||
self.refresh_palette();
|
||||
}
|
||||
|
||||
/// 面板 α 的**生效值**(CPU 档恒 1.0)。
|
||||
#[must_use]
|
||||
pub fn effective_panel_alpha(&self) -> f32 {
|
||||
effective_panel_alpha(self.backend, self.panel_alpha)
|
||||
}
|
||||
|
||||
/// 面板 α 是否被后端强制(设置页据此禁用滑块并给说明)。
|
||||
#[must_use]
|
||||
pub fn panel_is_forced(&self) -> bool {
|
||||
panel_is_forced_opaque(self.backend)
|
||||
}
|
||||
|
||||
/// 玻璃是否生效(CPU 档恒关)。
|
||||
#[must_use]
|
||||
pub fn effective_glass(&self) -> bool {
|
||||
effective_glass(self.backend, self.glass)
|
||||
}
|
||||
|
||||
/// 整窗 α 要下发的字节(`None` = 不调 FFI)。
|
||||
#[must_use]
|
||||
pub fn window_alpha_byte(&self) -> Option<u8> {
|
||||
alpha::window_alpha_byte(self.backend, self.window_opacity)
|
||||
}
|
||||
|
||||
/// 背景图是否真的会画(开关开 + 路径非空 + 解码成功)。
|
||||
#[must_use]
|
||||
pub fn background_active(&self) -> bool {
|
||||
self.bg_enabled && self.image.is_some()
|
||||
}
|
||||
|
||||
/// 已解码的背景图。
|
||||
#[must_use]
|
||||
pub fn image(&self) -> Option<&Image> {
|
||||
self.image.as_ref()
|
||||
}
|
||||
|
||||
/// 算一次马赛克(渲染层每帧调一次;纯函数,成本 = 格数)。
|
||||
///
|
||||
/// `toward` 是当前主题的面板底色(暗化往它退,见 [`background::dimmed`])。
|
||||
#[must_use]
|
||||
pub fn mosaic(&self, viewport: (f32, f32), toward: [u8; 3]) -> Option<Mosaic> {
|
||||
let image = self.image.as_ref()?;
|
||||
if !self.bg_enabled {
|
||||
return None;
|
||||
}
|
||||
Some(background::mosaic(
|
||||
image,
|
||||
self.bg_fill,
|
||||
viewport,
|
||||
self.bg_dim,
|
||||
toward,
|
||||
))
|
||||
}
|
||||
|
||||
/// 取色得到的方案(`None` = 色卡档,继续用令牌 baseline)。
|
||||
///
|
||||
/// 为什么色卡档不换 scheme:`resolve_source` 的色卡档走的是 `DEFAULT_CARD`(天蓝卡),
|
||||
/// 与令牌 baseline 逐色不同;直接应用会把**全部既有验收图的像素**改掉。色卡档的视觉基线
|
||||
/// 归令牌层(`Scheme::baseline`),这里只接管「取色真的成功」的那两档。
|
||||
#[must_use]
|
||||
pub fn derived_scheme(&self) -> Option<&Scheme> {
|
||||
let resolved = self.resolved.as_ref()?;
|
||||
if resolved.level() == unlucky_design::palette::Level::Preset {
|
||||
return None;
|
||||
}
|
||||
Some(&resolved.scheme)
|
||||
}
|
||||
|
||||
/// 读盘/解码失败的诊断(设置页逐条显示)。
|
||||
#[must_use]
|
||||
pub fn diagnostics(&self) -> &[String] {
|
||||
&self.diagnostics
|
||||
}
|
||||
|
||||
/// CPU 档降级的说明(`None` = 没有降级)。
|
||||
#[must_use]
|
||||
pub fn backend_note(&self) -> Option<&'static str> {
|
||||
self.panel_is_forced().then_some(
|
||||
"Software renderer: panel transparency and glass stay fully opaque (window \
|
||||
transparency still works).",
|
||||
)
|
||||
}
|
||||
|
||||
/// 配色来源的提示(`None` = 一级成功)。
|
||||
#[must_use]
|
||||
pub fn palette_note(&self) -> Option<&'static str> {
|
||||
self.resolved.as_ref()?.note()
|
||||
}
|
||||
|
||||
/// 生效的配色层级码(日志用)。
|
||||
#[must_use]
|
||||
pub fn palette_level(&self) -> &'static str {
|
||||
match &self.resolved {
|
||||
Some(resolved) => resolved.level().code(),
|
||||
None => self.palette_source.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 深浅切换后重算配色(`Ui` 换主题时调)。
|
||||
pub fn set_flavor(&mut self, flavor: SchemeFlavor) {
|
||||
if self.flavor == flavor {
|
||||
return;
|
||||
}
|
||||
self.flavor = flavor;
|
||||
self.refresh_palette();
|
||||
}
|
||||
|
||||
/// 当前深浅。
|
||||
#[must_use]
|
||||
pub fn flavor(&self) -> SchemeFlavor {
|
||||
self.flavor
|
||||
}
|
||||
|
||||
/// 应用一条改动(设置页 → 状态)。返回**是否需要立刻下发整窗 α**。
|
||||
pub fn apply(&mut self, change: super::Change) -> bool {
|
||||
use super::Change;
|
||||
match change {
|
||||
Change::WindowOpacity(alpha) => {
|
||||
self.window_opacity = alpha;
|
||||
true
|
||||
}
|
||||
Change::PanelAlpha(alpha) => {
|
||||
self.panel_alpha = alpha;
|
||||
false
|
||||
}
|
||||
Change::BgEnabled(enabled) => {
|
||||
self.bg_enabled = enabled;
|
||||
self.refresh_background();
|
||||
self.refresh_palette();
|
||||
false
|
||||
}
|
||||
Change::BgImage(path) => {
|
||||
self.bg_path = path;
|
||||
self.bg_enabled = !self.bg_path.trim().is_empty();
|
||||
self.refresh_background();
|
||||
self.refresh_palette();
|
||||
false
|
||||
}
|
||||
Change::BgFill(fill) => {
|
||||
self.bg_fill = fill;
|
||||
false
|
||||
}
|
||||
Change::BgDim(dim) => {
|
||||
self.bg_dim = dim;
|
||||
false
|
||||
}
|
||||
Change::PaletteSource(source) => {
|
||||
self.palette_source = source;
|
||||
self.refresh_palette();
|
||||
false
|
||||
}
|
||||
Change::SelectGroup(index) => {
|
||||
self.selected_group = index;
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 写回配置用的快照(`SettingsBridge::persist` 覆盖的那 8 个字段)。
|
||||
///
|
||||
/// 注意:**强调色 / 玻璃不在本 mission 的管辖内**,调用方(`app::appearance`)会拿配置里
|
||||
/// 已有的快照做基底、只覆盖本 mission 真改了的字段,所以这里不产出完整快照。
|
||||
/// 这个函数只给「当前值是什么」一个只读视图(日志与测试用)。
|
||||
#[must_use]
|
||||
pub fn current_values(&self) -> AppearancePrefs {
|
||||
AppearancePrefs {
|
||||
bg_image_enabled: self.bg_enabled,
|
||||
bg_image: self.bg_path.clone(),
|
||||
bg_fill: self.bg_fill,
|
||||
bg_dim: self.bg_dim,
|
||||
panel_alpha: self.panel_alpha,
|
||||
glass: self.glass,
|
||||
accent_color: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 取证日志(**一行**,数值而不是观感):`[appearance] window_alpha=… panel_alpha=… byte=…`。
|
||||
#[must_use]
|
||||
pub fn log_line(&self, context: &str) -> String {
|
||||
let byte = self
|
||||
.window_alpha_byte()
|
||||
.map_or_else(|| "none".to_owned(), |byte| byte.to_string());
|
||||
format!(
|
||||
"[appearance] {context} window_alpha={:.3} panel_alpha={:.3} (requested {:.3}) \
|
||||
byte={byte} backend={} bg={} fill={} dim={:.2} palette={} group={}",
|
||||
self.window_opacity,
|
||||
self.effective_panel_alpha(),
|
||||
self.panel_alpha,
|
||||
self.backend.name(),
|
||||
if self.background_active() {
|
||||
self.bg_path.as_str()
|
||||
} else {
|
||||
"none"
|
||||
},
|
||||
self.bg_fill.as_str(),
|
||||
self.bg_dim,
|
||||
self.palette_level(),
|
||||
self.selected_group
|
||||
)
|
||||
}
|
||||
|
||||
/// 重读配置后同步(热切)。
|
||||
pub fn reload(&mut self, prefs: &AppearancePrefs, base: &AppearanceBase) {
|
||||
self.window_opacity = base.window_opacity;
|
||||
self.palette_source = base.palette_source;
|
||||
self.panel_alpha = prefs.panel_alpha;
|
||||
self.bg_enabled = prefs.bg_image_enabled;
|
||||
self.bg_fill = prefs.bg_fill;
|
||||
self.bg_dim = prefs.bg_dim;
|
||||
self.glass = prefs.glass;
|
||||
if self.bg_path != prefs.bg_image {
|
||||
self.bg_path = prefs.bg_image.clone();
|
||||
}
|
||||
self.refresh_background();
|
||||
self.refresh_palette();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 内部
|
||||
|
||||
fn refresh_background(&mut self) {
|
||||
self.image = None;
|
||||
self.diagnostics.clear();
|
||||
let trimmed = self.bg_path.trim();
|
||||
if !self.bg_enabled || trimmed.is_empty() {
|
||||
return;
|
||||
}
|
||||
match background::load(trimmed) {
|
||||
Ok(image) => self.image = Some(image),
|
||||
Err(error) => self.diagnostics.push(error.describe()),
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_palette(&mut self) {
|
||||
let background = match (&self.image, self.bg_enabled) {
|
||||
(Some(image), true) => Source::Pixels(image),
|
||||
(None, true) if !self.bg_path.trim().is_empty() => Source::Undecodable,
|
||||
_ => Source::Absent,
|
||||
};
|
||||
// 桌面壁纸探测(M73)尚未落地:这一级永远 `Absent`,回落链仍按 design 的规则走。
|
||||
self.resolved = Some(palette::resolve(
|
||||
self.palette_source,
|
||||
background,
|
||||
Source::Absent,
|
||||
self.flavor,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn env_f32(key: &str) -> Option<f32> {
|
||||
std::env::var(key)
|
||||
.ok()
|
||||
.and_then(|value| value.trim().parse::<f32>().ok())
|
||||
.filter(|value| value.is_finite())
|
||||
}
|
||||
Reference in new issue
Block a user