Files
Unlucky-Launcher/crates/app-iced/src/pages/appearance/state.rs
T
AskaEth 1d240ff1f2 fix(i18n): 复审 r1 —— 两句近义软件渲染说明合一,生效层级码翻成译文(M180)
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}` 原文串、专名)
2026-11-08 21:42:12 +08:00

383 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 外观的**运行期状态**:配置值 → 生效值(钳位 / 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())
}