P2-1(一屏两句重复):
- 留面板那一句(就在被禁用的滑块旁边,正好回答「为什么拖不动」),措辞同时点名面板与玻璃
- 删 `SettingsAppearanceSoftwareRendererNote`(catalog 一条 + 金表一行,两边同时删)
- 删 `bg_image.rs` 的 `backend_note` 渲染块与 `State::backend_note()`;state.rs 391 → 382 行
P2-2(配色:preset 的机器码):
- `palette::level_key(code)` 把 `preset`/`wallpaper`/其余 映射到已有的三条档位 key,不新增平行词表
- `bg_image.rs` 用 `tr(PaletteLevel, {level})` 传译文,中文界面显示「配色:内置色卡」
- 补 `every_level_code_maps_to_its_own_sentence`:三个码值一一对应 + 三条译文互不相同
顺手(复审 L-1/L-2/L-4,全在本 scope 内):
- `appearance_texts` 加「渲染后不许残留 `{`」断言(参数名写错当场红)
- `section/mod.rs` 文档:取词口 t/tr、填充措辞、清掉孤立 `//!`、把「没有一句英文」改成
写明两处例外(`{reason}` 原文串、专名)
383 lines
14 KiB
Rust
383 lines
14 KiB
Rust
//! 外观的**运行期状态**:配置值 → 生效值(钳位 / 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_i18n::catalog::Key;
|
||
use unlucky_presenter::prefs::{
|
||
AppearanceBase, AppearancePrefs, BgFill, PaletteSource, WindowCorner,
|
||
};
|
||
|
||
use crate::Backend;
|
||
|
||
use super::alpha::{self, effective_glass, effective_panel_alpha, panel_is_forced_opaque};
|
||
use super::background::{self, Image, Note};
|
||
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,
|
||
/// 窗口圆角策略(M116:`system` / `square`;真能不能圆由平台判,见 `platform::plan_corner`)。
|
||
pub window_corner: WindowCorner,
|
||
/// 渲染后端(决定 CPU 档的两条降级)。
|
||
pub backend: Backend,
|
||
/// 设置页当前分组(0 = General … 1 = Appearance;见 [`super::APPEARANCE_GROUP`])。
|
||
pub selected_group: usize,
|
||
/// 已解码的背景图(`None` = 没设 / 读失败)。
|
||
image: Option<Image>,
|
||
/// 读盘/解码失败的**可读诊断**(按发生顺序追加;`Note` = key + 参数,句子在词表里)。
|
||
diagnostics: Vec<Note>,
|
||
/// 上一次的配色解析结果。
|
||
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,
|
||
window_corner: base.window_corner,
|
||
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)
|
||
}
|
||
|
||
/// 当前圆角策略(设置页显示选中项、日志与下发都用它)。
|
||
#[must_use]
|
||
pub fn window_corner(&self) -> WindowCorner {
|
||
self.window_corner
|
||
}
|
||
|
||
/// 改圆角策略(消息落点见 `app::update::appearance`;这里只管状态,不碰窗口)。
|
||
pub fn set_window_corner(&mut self, policy: WindowCorner) {
|
||
self.window_corner = policy;
|
||
}
|
||
|
||
/// 玻璃是否生效(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()
|
||
}
|
||
|
||
/// 取色得到的方案(`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)
|
||
}
|
||
|
||
/// 读盘/解码失败的诊断(设置页逐条显示;一条 = 一条 key + 它的参数)。
|
||
#[must_use]
|
||
pub fn diagnostics(&self) -> &[Note] {
|
||
&self.diagnostics
|
||
}
|
||
|
||
/// 配色来源的提示 key(`None` = 一级成功)。
|
||
#[must_use]
|
||
pub fn palette_note(&self) -> Option<Key> {
|
||
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::Corner(policy) => {
|
||
self.window_corner = policy;
|
||
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} corner={} backend={} bg={} fill={} dim={:.2} palette={} group={}",
|
||
self.window_opacity,
|
||
self.effective_panel_alpha(),
|
||
self.panel_alpha,
|
||
self.window_corner.as_str(),
|
||
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.window_corner = base.window_corner;
|
||
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.note()),
|
||
}
|
||
}
|
||
|
||
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())
|
||
}
|