Merge branch 'feat/m180-i18nappear-localize-appearance-page'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
This commit is contained in:
commit
29da7e2e47
14 files changed
+921
-191
No files matched your search
@@ -26,6 +26,8 @@
|
|||||||
//! 为什么向底色退而不是「叠黑」:浅色主题下叠黑会把亮图压成暗块,深色正文反而更读不清
|
//! 为什么向底色退而不是「叠黑」:浅色主题下叠黑会把亮图压成暗块,深色正文反而更读不清
|
||||||
//! (偏离登记在 [`super`] 的偏离表 #7)。
|
//! (偏离登记在 [`super`] 的偏离表 #7)。
|
||||||
|
|
||||||
|
use unlucky_i18n::catalog::Key;
|
||||||
|
use unlucky_i18n::Lang;
|
||||||
use unlucky_presenter::prefs::BgFill;
|
use unlucky_presenter::prefs::BgFill;
|
||||||
|
|
||||||
/// 暗化上限(core 的 `BG_DIM_MAX` = 0.95,经 `unlucky_presenter::prefs` 重导出;这里只给 f32 形态)。
|
/// 暗化上限(core 的 `BG_DIM_MAX` = 0.95,经 `unlucky_presenter::prefs` 重导出;这里只给 f32 形态)。
|
||||||
@@ -63,7 +65,7 @@ pub const TILE_SCALE_STEP: f32 = 1.15;
|
|||||||
/// 会退化成「留缝」,正是 r1 的 P2-1。真实图片不会走到这个上限。
|
/// 会退化成「留缝」,正是 r1 的 P2-1。真实图片不会走到这个上限。
|
||||||
pub const MAX_TILE_SCALE: f32 = 1024.0;
|
pub const MAX_TILE_SCALE: f32 = 1024.0;
|
||||||
|
|
||||||
/// 解码失败的原因(人话在 [`LoadError::describe`])。
|
/// 解码失败的原因(人话在 [`LoadError::note`])。
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum LoadError {
|
pub enum LoadError {
|
||||||
/// 读文件失败:不存在 / 没权限 / 被占用。
|
/// 读文件失败:不存在 / 没权限 / 被占用。
|
||||||
@@ -79,17 +81,76 @@ pub enum LoadError {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LoadError {
|
/// 一条**可翻译**诊断:`key` + 已经格式化好的参数(模板里只有 `{name}`,见 [`Key`])。
|
||||||
/// 可读诊断(设置页提示行与日志共用同一句)。
|
///
|
||||||
|
/// 为什么不是 `String`:文案表在金表里(M180 `I18NAPPEAR`),本模块只给「哪条 key + 哪些值」。
|
||||||
|
/// 数字在这一层就格式化好(`{:.2}` / `{:?}` 这些格式说明词表不支持)。
|
||||||
|
///
|
||||||
|
/// 为什么 `types` 是 `String` 而不是 `&'static str`:`Io(reason)` / `Undecodable(reason)` 的原因来自
|
||||||
|
/// 磁盘与解码库(运行时字符串),只能持有。
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Note {
|
||||||
|
key: Key,
|
||||||
|
args: Vec<(&'static str, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Note {
|
||||||
|
/// 组装一条(参数名必须与译文里的 `{name}` 逐字相同)。
|
||||||
|
fn new(key: Key, args: Vec<(&'static str, String)>) -> Self {
|
||||||
|
Self { key, args }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 这条诊断是哪条文案。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn describe(&self) -> String {
|
pub fn key(&self) -> Key {
|
||||||
|
self.key
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 参数(名 → 已成形的值)。
|
||||||
|
#[must_use]
|
||||||
|
pub fn args(&self) -> Vec<(&str, &str)> {
|
||||||
|
self.args
|
||||||
|
.iter()
|
||||||
|
.map(|(name, value)| (*name, value.as_str()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 按语言取整句(`{name}` 全替换;缺参时占位符原样留着,见 [`unlucky_i18n::catalog::Catalog::render`])。
|
||||||
|
#[must_use]
|
||||||
|
pub fn render(&self, lang: Lang) -> String {
|
||||||
|
lang.catalog().render(self.key, &self.args())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 英文基线里有没有这个子串。
|
||||||
|
///
|
||||||
|
/// 给**跨 mission 的既有断言**用(`app/tests/appearance.rs` 查「诊断里有没有说出原因」),
|
||||||
|
/// 那条断言不在本 mission 的 scope 里、不能改成按 key 断言。UI 一律走 [`Self::render`]。
|
||||||
|
#[must_use]
|
||||||
|
pub fn contains(&self, needle: &str) -> bool {
|
||||||
|
self.render(Lang::En).contains(needle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoadError {
|
||||||
|
/// 可翻译诊断(设置页提示行与日志共用同一条 key)。
|
||||||
|
#[must_use]
|
||||||
|
pub fn note(&self) -> Note {
|
||||||
match self {
|
match self {
|
||||||
Self::Io(reason) => format!("Background image could not be read: {reason}"),
|
Self::Io(reason) => Note::new(
|
||||||
Self::Undecodable(reason) => format!(
|
Key::SettingsAppearanceBgReadFailed,
|
||||||
"Background image could not be decoded ({reason}; this build decodes PNG, JPEG and BMP)"
|
vec![("reason", reason.clone())],
|
||||||
),
|
),
|
||||||
Self::TooLarge { width, height } => format!(
|
Self::Undecodable(reason) => Note::new(
|
||||||
"Background image is too large to decode: {width}×{height} (limit {MAX_SOURCE_EDGE} px per edge)"
|
Key::SettingsAppearanceBgDecodeFailed,
|
||||||
|
vec![("reason", reason.clone())],
|
||||||
|
),
|
||||||
|
Self::TooLarge { width, height } => Note::new(
|
||||||
|
Key::SettingsAppearanceBgTooLarge,
|
||||||
|
vec![
|
||||||
|
("width", width.to_string()),
|
||||||
|
("height", height.to_string()),
|
||||||
|
("limit", MAX_SOURCE_EDGE.to_string()),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,7 +254,7 @@ fn header_size(bytes: &[u8]) -> Result<(u32, u32), LoadError> {
|
|||||||
/// 判据只看图片本身(短边 < [`SMALL_TILE_EDGE`])——视口尺寸在渲染层才知道,而这里要的是
|
/// 判据只看图片本身(短边 < [`SMALL_TILE_EDGE`])——视口尺寸在渲染层才知道,而这里要的是
|
||||||
/// 「这张图小到要放大」这个与视口无关的事实。`scale` 条数上限由 [`tiles`] 保证覆盖 ≥ 视口。
|
/// 「这张图小到要放大」这个与视口无关的事实。`scale` 条数上限由 [`tiles`] 保证覆盖 ≥ 视口。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn small_tile_note(image: &Image, fill: BgFill) -> Option<String> {
|
pub fn small_tile_note(image: &Image, fill: BgFill) -> Option<Note> {
|
||||||
if !matches!(fill, BgFill::Tile) {
|
if !matches!(fill, BgFill::Tile) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -201,9 +262,13 @@ pub fn small_tile_note(image: &Image, fill: BgFill) -> Option<String> {
|
|||||||
if width.min(height) >= SMALL_TILE_EDGE {
|
if width.min(height) >= SMALL_TILE_EDGE {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(format!(
|
Some(Note::new(
|
||||||
"Small image ({}×{}): tiles are scaled up so the window stays covered (max {MAX_TILES} tiles, no gaps).",
|
Key::SettingsAppearanceBgSmallTile,
|
||||||
image.source.0, image.source.1
|
vec![
|
||||||
|
("width", image.source.0.to_string()),
|
||||||
|
("height", image.source.1.to_string()),
|
||||||
|
("max", MAX_TILES.to_string()),
|
||||||
|
],
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
use super::{dim_alpha, load, small_tile_note, tiles, LoadError, BG_DIM_MAX, MAX_TILES};
|
use super::{dim_alpha, load, small_tile_note, tiles, LoadError, BG_DIM_MAX, MAX_TILES};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use unlucky_i18n::catalog::Key;
|
||||||
|
use unlucky_i18n::Lang;
|
||||||
use unlucky_presenter::prefs::BgFill;
|
use unlucky_presenter::prefs::BgFill;
|
||||||
|
|
||||||
fn temp_file(label: &str, name: &str) -> PathBuf {
|
fn temp_file(label: &str, name: &str) -> PathBuf {
|
||||||
@@ -66,19 +68,28 @@ fn a_real_jpeg_decodes_too() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn a_missing_or_broken_file_reports_a_readable_reason() {
|
fn a_missing_or_broken_file_reports_a_readable_reason() {
|
||||||
// 抓得住:①路径不存在 → Io;②文件存在但不是图片 → Undecodable;③空路径 → Io。
|
// 抓得住:①路径不存在 → Io;②文件存在但不是图片 → Undecodable;③空路径 → Io。
|
||||||
// 都必须是 Err(不许静默回默认色),且 describe() 是人话。
|
// 都必须是 Err(不许静默回默认色),且 note() 给出**哪条 key + 哪些参数**(句子在词表里)。
|
||||||
// 抓不住:webp/tiff 这类没开编码器的格式具体报什么字符串(只钉分类,不钉解码库原文)。
|
// 抓不住:webp/tiff 这类没开编码器的格式具体报什么字符串(只钉分类,不钉解码库原文)。
|
||||||
let missing = temp_file("missing", "bg.png");
|
let missing = temp_file("missing", "bg.png");
|
||||||
let error = load(missing.to_str().unwrap()).expect_err("不存在的路径必须报错");
|
let error = load(missing.to_str().unwrap()).expect_err("不存在的路径必须报错");
|
||||||
assert!(matches!(error, LoadError::Io(_)), "实际 {error:?}");
|
assert!(matches!(error, LoadError::Io(_)), "实际 {error:?}");
|
||||||
assert!(error
|
let note = error.note();
|
||||||
.describe()
|
assert_eq!(note.key(), Key::SettingsAppearanceBgReadFailed);
|
||||||
.starts_with("Background image could not be read"));
|
assert!(
|
||||||
|
note.contains("could not be read"),
|
||||||
|
"英文基线要说出「读不出来」:{}",
|
||||||
|
note.render(Lang::En)
|
||||||
|
);
|
||||||
|
|
||||||
let broken = temp_file("broken", "bg.png");
|
let broken = temp_file("broken", "bg.png");
|
||||||
std::fs::write(&broken, b"this is not an image").expect("写坏文件");
|
std::fs::write(&broken, b"this is not an image").expect("写坏文件");
|
||||||
let error = load(broken.to_str().unwrap()).expect_err("非图片必须报错");
|
let error = load(broken.to_str().unwrap()).expect_err("非图片必须报错");
|
||||||
assert!(matches!(error, LoadError::Undecodable(_)), "实际 {error:?}");
|
assert!(matches!(error, LoadError::Undecodable(_)), "实际 {error:?}");
|
||||||
|
assert_eq!(
|
||||||
|
error.note().key(),
|
||||||
|
Key::SettingsAppearanceBgDecodeFailed,
|
||||||
|
"解不开与读不出来必须是两条 key(措辞与修法都不同)"
|
||||||
|
);
|
||||||
cleanup(&broken);
|
cleanup(&broken);
|
||||||
|
|
||||||
let empty = load(" ").expect_err("空路径必须报错");
|
let empty = load(" ").expect_err("空路径必须报错");
|
||||||
@@ -168,10 +179,21 @@ fn an_over_sized_image_reports_too_large_instead_of_trying_to_decode() {
|
|||||||
),
|
),
|
||||||
"实际 {error:?}"
|
"实际 {error:?}"
|
||||||
);
|
);
|
||||||
|
let note = error.note();
|
||||||
|
assert_eq!(note.key(), Key::SettingsAppearanceBgTooLarge);
|
||||||
assert!(
|
assert!(
|
||||||
error.describe().contains("too large"),
|
note.contains("too large"),
|
||||||
"提示要人话:{}",
|
"英文基线要说出「太大」:{}",
|
||||||
error.describe()
|
note.render(Lang::En)
|
||||||
|
);
|
||||||
|
let args = note.args();
|
||||||
|
assert!(
|
||||||
|
args.contains(&("width", "9000")) && args.contains(&("height", "4")),
|
||||||
|
"宽高要作为参数传进去(不是写进译文):{args:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
args.contains(&("limit", "8192")),
|
||||||
|
"上限走占位符而不是写进译文:{args:?}"
|
||||||
);
|
);
|
||||||
// 上限不许再放宽(r1 的 16384 等于允许 1 GB 的中间缓冲):常量级约束放在测试里钉。
|
// 上限不许再放宽(r1 的 16384 等于允许 1 GB 的中间缓冲):常量级约束放在测试里钉。
|
||||||
const _: () = assert!(super::MAX_SOURCE_EDGE <= 8_192);
|
const _: () = assert!(super::MAX_SOURCE_EDGE <= 8_192);
|
||||||
@@ -187,8 +209,21 @@ fn an_over_sized_image_reports_too_large_instead_of_trying_to_decode() {
|
|||||||
fn small_images_get_a_tile_diagnostic_instead_of_silently_degrading() {
|
fn small_images_get_a_tile_diagnostic_instead_of_silently_degrading() {
|
||||||
let small = super::Image::from_rgba((32, 32), 4, 4, vec![0; 64]);
|
let small = super::Image::from_rgba((32, 32), 4, 4, vec![0; 64]);
|
||||||
let note = small_tile_note(&small, BgFill::Tile).expect("小图 tile 必须有提示");
|
let note = small_tile_note(&small, BgFill::Tile).expect("小图 tile 必须有提示");
|
||||||
assert!(note.contains("32×32"), "{note}");
|
assert_eq!(note.key(), Key::SettingsAppearanceBgSmallTile);
|
||||||
assert!(note.contains("scaled up"), "{note}");
|
let args = note.args();
|
||||||
|
assert!(
|
||||||
|
args.contains(&("width", "32")) && args.contains(&("height", "32")),
|
||||||
|
"尺寸作为参数:{args:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
args.contains(&("max", "64")),
|
||||||
|
"块数上限走占位符(`MAX_TILES` 改了不该动译文):{args:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
note.render(Lang::En).contains("scaled up"),
|
||||||
|
"英文基线要说出「会被放大」:{}",
|
||||||
|
note.render(Lang::En)
|
||||||
|
);
|
||||||
|
|
||||||
// 非 tile 档不提示(cover/contain 会等比铺满,不存在「小图被放大铺」这件事)。
|
// 非 tile 档不提示(cover/contain 会等比铺满,不存在「小图被放大铺」这件事)。
|
||||||
assert!(small_tile_note(&small, BgFill::Cover).is_none());
|
assert!(small_tile_note(&small, BgFill::Cover).is_none());
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ mod picture;
|
|||||||
pub(crate) mod probe;
|
pub(crate) mod probe;
|
||||||
mod section;
|
mod section;
|
||||||
mod state;
|
mod state;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
/// 状态提示的 **tonal notice**(行数硬顶把 `section/` 撑开之后它仍放在模块根)。
|
/// 状态提示的 **tonal notice**(行数硬顶把 `section/` 撑开之后它仍放在模块根)。
|
||||||
///
|
///
|
||||||
@@ -166,56 +168,3 @@ impl Change {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,8 @@
|
|||||||
//!
|
//!
|
||||||
//! 1. 把「没设」与「设了但坏了」**分开传**([`Source::Absent`] vs [`Source::Undecodable`])——
|
//! 1. 把「没设」与「设了但坏了」**分开传**([`Source::Absent`] vs [`Source::Undecodable`])——
|
||||||
//! 混成一个 `None` 会让设置页说错话(「没设背景图」 vs 「你那张图读不出来」);
|
//! 混成一个 `None` 会让设置页说错话(「没设背景图」 vs 「你那张图读不出来」);
|
||||||
//! 2. 把 `Provenance::hint_key()` 翻成一句可读提示(键 → 文案是 UI 层的活,i18n 之后再统一)。
|
//! 2. 把 `Provenance::hint_key()` 翻成一条词表 key(M180:句子在 `catalog/settings/appearance.rs`,
|
||||||
|
//! 这里只给 key——中文界面下这句提示也是中文)。
|
||||||
//!
|
//!
|
||||||
//! ## 为什么不在这里做对比度
|
//! ## 为什么不在这里做对比度
|
||||||
//!
|
//!
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
|
|
||||||
use unlucky_design::color::{Scheme, SchemeFlavor};
|
use unlucky_design::color::{Scheme, SchemeFlavor};
|
||||||
use unlucky_design::palette::{resolve_source, ImageInput, Level, Provenance};
|
use unlucky_design::palette::{resolve_source, ImageInput, Level, Provenance};
|
||||||
|
use unlucky_i18n::catalog::Key;
|
||||||
use unlucky_presenter::prefs::PaletteSource;
|
use unlucky_presenter::prefs::PaletteSource;
|
||||||
|
|
||||||
use super::background::Image;
|
use super::background::Image;
|
||||||
@@ -62,9 +64,9 @@ impl Resolved {
|
|||||||
self.provenance.level
|
self.provenance.level
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 可读提示(`None` = 一级成功、没什么可解释的)。
|
/// 提示 key(`None` = 一级成功、没什么可解释的)。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn note(&self) -> Option<&'static str> {
|
pub fn note(&self) -> Option<Key> {
|
||||||
note(&self.provenance)
|
note(&self.provenance)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,47 +99,55 @@ pub fn resolve(
|
|||||||
Resolved { scheme, provenance }
|
Resolved { scheme, provenance }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `Provenance::hint_key()` → 一句可读提示(英文基线;i18n mission 之后走词表)。
|
/// `Provenance::hint_key()` → 一条词表 key(M180:句子在 `catalog/settings/appearance.rs`)。
|
||||||
///
|
///
|
||||||
/// 键的完整表在 `unlucky_design::palette::resolve::Provenance::hint_key` 的文档里;
|
/// 键的完整表在 `unlucky_design::palette::resolve::Provenance::hint_key` 的文档里;
|
||||||
/// 这里只翻「本 mission 真会产生的那几个」,未识别键返回 `None` 而不是编一句话。
|
/// 这里只翻「本 mission 真会产生的那几个」,未识别键返回 `None` 而不是编一句话。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn note(provenance: &Provenance) -> Option<&'static str> {
|
pub fn note(provenance: &Provenance) -> Option<Key> {
|
||||||
Some(match provenance.hint_key()? {
|
Some(match provenance.hint_key()? {
|
||||||
"palette.hint.background.undecodable" => {
|
"palette.hint.background.undecodable" => Key::SettingsAppearancePaletteBgUndecodable,
|
||||||
"Background image could not be read; colours fall back."
|
"palette.hint.background.too-small" => Key::SettingsAppearancePaletteBgTooSmall,
|
||||||
}
|
"palette.hint.background.bad-dimensions" => Key::SettingsAppearancePaletteBgBadDimensions,
|
||||||
"palette.hint.background.too-small" => {
|
|
||||||
"Background image is too small to sample; colours fall back."
|
|
||||||
}
|
|
||||||
"palette.hint.background.bad-dimensions" => {
|
|
||||||
"Background image data looks inconsistent; colours fall back."
|
|
||||||
}
|
|
||||||
"palette.hint.background.no-opaque-pixels" => {
|
"palette.hint.background.no-opaque-pixels" => {
|
||||||
"Background image is almost fully transparent; colours fall back."
|
Key::SettingsAppearancePaletteBgNoOpaquePixels
|
||||||
}
|
}
|
||||||
"palette.hint.background.low-chroma" => {
|
"palette.hint.background.low-chroma" => Key::SettingsAppearancePaletteBgLowChroma,
|
||||||
"Background image is too monochrome to tint from; colours fall back."
|
"palette.hint.background.contrast" => Key::SettingsAppearancePaletteBgContrast,
|
||||||
|
"palette.hint.wallpaper.undecodable" => Key::SettingsAppearancePaletteWallpaperUndecodable,
|
||||||
|
"palette.hint.wallpaper.low-chroma" => Key::SettingsAppearancePaletteWallpaperLowChroma,
|
||||||
|
"palette.hint.wallpaper.too-small" => Key::SettingsAppearancePaletteWallpaperTooSmall,
|
||||||
|
"palette.hint.wallpaper.bad-dimensions" => {
|
||||||
|
Key::SettingsAppearancePaletteWallpaperBadDimensions
|
||||||
}
|
}
|
||||||
"palette.hint.background.contrast" => {
|
|
||||||
"Background image colours would be unreadable; colours fall back."
|
|
||||||
}
|
|
||||||
"palette.hint.wallpaper.undecodable" => "Desktop wallpaper could not be read.",
|
|
||||||
"palette.hint.wallpaper.low-chroma" => "Desktop wallpaper is too monochrome to tint from.",
|
|
||||||
"palette.hint.wallpaper.too-small" => "Desktop wallpaper is too small to sample.",
|
|
||||||
"palette.hint.wallpaper.bad-dimensions" => "Desktop wallpaper data looks inconsistent.",
|
|
||||||
"palette.hint.wallpaper.no-opaque-pixels" => {
|
"palette.hint.wallpaper.no-opaque-pixels" => {
|
||||||
"Desktop wallpaper is almost fully transparent."
|
Key::SettingsAppearancePaletteWallpaperNoOpaquePixels
|
||||||
}
|
}
|
||||||
"palette.hint.wallpaper.contrast" => {
|
"palette.hint.wallpaper.contrast" => Key::SettingsAppearancePaletteWallpaperContrast,
|
||||||
"Desktop wallpaper colours would be unreadable; colours fall back."
|
"palette.hint.preset.contrast" => Key::SettingsAppearancePalettePresetContrast,
|
||||||
}
|
"palette.hint.adjusted" => Key::SettingsAppearancePaletteAdjusted,
|
||||||
"palette.hint.preset.contrast" => "The built-in card needed a small contrast correction.",
|
|
||||||
"palette.hint.adjusted" => "A few colours were nudged for readability.",
|
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 生效层级**码值** → 已有的那三条档位 key(M180 复审 P2-2:中文界面里不许留机器码)。
|
||||||
|
///
|
||||||
|
/// 码值的全部来源只有两处,取值集合相同:`Level::code()`(`background-image` / `wallpaper` /
|
||||||
|
/// `preset`)与 `PaletteSource::as_str()`(同三个词)。所以三条 key 就够,**不新增平行词表**——
|
||||||
|
/// 卡片底部「配色来源」用它们表示**用户选的**档,这一行用它们表示**回落之后真正生效**的档。
|
||||||
|
///
|
||||||
|
/// 未识别的码值落到 `background-image` 那条(宁可显示一个偏保守的档名,也不许把机器码漏进
|
||||||
|
/// 中文界面——这条兜底本身由 `tests` 里的 `every_level_code_maps_to_its_own_sentence` 钉住)。
|
||||||
|
/// `state.rs` 已贴 400 行线,所以这个映射住在本文件。
|
||||||
|
#[must_use]
|
||||||
|
pub fn level_key(code: &str) -> Key {
|
||||||
|
match code {
|
||||||
|
"preset" => Key::SettingsAppearancePalettePreset,
|
||||||
|
"wallpaper" => Key::SettingsAppearancePaletteWallpaper,
|
||||||
|
_ => Key::SettingsAppearancePaletteBackground,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{note, resolve, Source};
|
use super::{note, resolve, Source};
|
||||||
@@ -145,6 +155,7 @@ mod tests {
|
|||||||
use unlucky_design::color::SchemeFlavor;
|
use unlucky_design::color::SchemeFlavor;
|
||||||
use unlucky_design::palette::wcag::check;
|
use unlucky_design::palette::wcag::check;
|
||||||
use unlucky_design::palette::{Level, Outcome, Skip};
|
use unlucky_design::palette::{Level, Outcome, Skip};
|
||||||
|
use unlucky_i18n::catalog::Key;
|
||||||
use unlucky_presenter::prefs::PaletteSource;
|
use unlucky_presenter::prefs::PaletteSource;
|
||||||
|
|
||||||
/// 高彩度的图(纯红):取色一定成功。
|
/// 高彩度的图(纯红):取色一定成功。
|
||||||
@@ -193,7 +204,7 @@ mod tests {
|
|||||||
assert!(resolved.provenance.did_fall_back());
|
assert!(resolved.provenance.did_fall_back());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolved.note(),
|
resolved.note(),
|
||||||
Some("Background image could not be read; colours fall back.")
|
Some(Key::SettingsAppearancePaletteBgUndecodable)
|
||||||
);
|
);
|
||||||
assert!(check(&resolved.scheme).is_clean());
|
assert!(check(&resolved.scheme).is_clean());
|
||||||
}
|
}
|
||||||
@@ -215,7 +226,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
other => panic!("灰图必须被判不合格,实际 {other:?}"),
|
other => panic!("灰图必须被判不合格,实际 {other:?}"),
|
||||||
}
|
}
|
||||||
assert!(resolved.note().unwrap().contains("monochrome"));
|
assert_eq!(
|
||||||
|
resolved.note(),
|
||||||
|
Some(Key::SettingsAppearancePaletteBgLowChroma),
|
||||||
|
"灰图的回落原因要指名「彩度不足」"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -276,4 +291,40 @@ mod tests {
|
|||||||
None
|
None
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// **M180 复审 P2-2 的守门**:生效层级的**每一个码值**都必须映射到它自己那条中文句子。
|
||||||
|
///
|
||||||
|
/// 抓得住:① 三个码值被映射到同一条 key(界面上分不出到底生效的是哪一级);
|
||||||
|
/// ② 某条映射被删掉 / 写错(`preset` 落到 background 那条 ⇒ 中文界面说「取自背景图」);
|
||||||
|
/// ③ 有人再加一级却忘了扩映射。
|
||||||
|
/// 抓不住:译文本身的措辞(那是 `catalog/settings/appearance.rs` 的译文守卫的事)。
|
||||||
|
///
|
||||||
|
/// 变异证明:把 `"preset"` 那行删掉(落进兜底)⇒ 本条红,且 `assert_ne!` 直接点名码值。
|
||||||
|
///
|
||||||
|
/// 为什么这条必须有:`the_appearance_card_speaks_chinese` 断的是**整句**非 ASCII,
|
||||||
|
/// 而「配色:preset」里有汉字 ⇒ 整句非 ASCII ⇒ 那个守卫放过它。这个口子只能在这里堵。
|
||||||
|
#[test]
|
||||||
|
fn every_level_code_maps_to_its_own_sentence() {
|
||||||
|
use super::level_key;
|
||||||
|
|
||||||
|
let codes = ["background-image", "wallpaper", "preset"];
|
||||||
|
let keys: Vec<Key> = codes.iter().copied().map(level_key).collect();
|
||||||
|
assert_eq!(
|
||||||
|
keys,
|
||||||
|
vec![
|
||||||
|
Key::SettingsAppearancePaletteBackground,
|
||||||
|
Key::SettingsAppearancePaletteWallpaper,
|
||||||
|
Key::SettingsAppearancePalettePreset,
|
||||||
|
],
|
||||||
|
"码值与档位 key 必须一一对应"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 三条中文句子互不相同(否则「映射对了」但界面上仍分不出层级)。
|
||||||
|
let zh = unlucky_i18n::Lang::ZhCn.catalog();
|
||||||
|
let mut texts: Vec<&str> = keys.iter().map(|key| zh.text(*key)).collect();
|
||||||
|
let total = texts.len();
|
||||||
|
texts.sort_unstable();
|
||||||
|
texts.dedup();
|
||||||
|
assert_eq!(texts.len(), total, "三条档位译文有重复:{texts:?}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,8 +10,9 @@
|
|||||||
//! - 想走 `Widget::operate` 得自己造 `Tree` + 跑布局 + 传 `Renderer`,而单测里**没有渲染器**
|
//! - 想走 `Widget::operate` 得自己造 `Tree` + 跑布局 + 传 `Renderer`,而单测里**没有渲染器**
|
||||||
//! (`pages/download/tests/options_screen.rs` 记的正是这条边界)。
|
//! (`pages/download/tests/options_screen.rs` 记的正是这条边界)。
|
||||||
//!
|
//!
|
||||||
//! 所以取证点退到「字进树之前」那一层:`section::t` 是本分组**唯一**的取词口,它把每条译文
|
//! 所以取证点退到「字进树之前」那一层:`section::t` / `section::tr` 是本分组**唯一**两个取词口,
|
||||||
//! 顺手记一份,测试再断言这批字里有没有汉字。这不是「看源码字符串」那种假守卫——它要求真的把
|
//! 它们把每条译文顺手记一份(M180 起分成两份:`t()` 的 `&'static str` 与 `tr()` 替换完占位符的
|
||||||
|
//! 整句),测试再断言这批字里有没有汉字。这不是「看源码字符串」那种假守卫——它要求真的把
|
||||||
//! [`crate::pages::appearance::section`] 跑一遍(并且走 `settings::view` 的语言分发),只是不画像素。
|
//! [`crate::pages::appearance::section`] 跑一遍(并且走 `settings::view` 的语言分发),只是不画像素。
|
||||||
//!
|
//!
|
||||||
//! ## 为什么是 thread-local
|
//! ## 为什么是 thread-local
|
||||||
@@ -25,7 +26,7 @@ use std::cell::RefCell;
|
|||||||
|
|
||||||
use unlucky_i18n::catalog::Key;
|
use unlucky_i18n::catalog::Key;
|
||||||
|
|
||||||
// 自上次 `take` 以来记录下的 `(key, 译文)`,顺序 = 取词顺序。
|
// 自上次 `take` 以来记录下的 `(key, 译文)`,顺序 = 取词顺序(`t()` 取的 `&'static str`)。
|
||||||
thread_local! {
|
thread_local! {
|
||||||
// clippy 1.99.1(= 本仓 CI 的 stable)对这条 lint **误报**:初始化器已经写成 `const { … }`
|
// clippy 1.99.1(= 本仓 CI 的 stable)对这条 lint **误报**:初始化器已经写成 `const { … }`
|
||||||
// 它还是报「can be made const」。最小复现(clippy-driver 原地可验,无本仓任何符号):
|
// 它还是报「can be made const」。最小复现(clippy-driver 原地可验,无本仓任何符号):
|
||||||
@@ -35,12 +36,33 @@ thread_local! {
|
|||||||
static TEXTS: RefCell<Vec<(Key, &'static str)>> = const { RefCell::new(Vec::new()) };
|
static TEXTS: RefCell<Vec<(Key, &'static str)>> = const { RefCell::new(Vec::new()) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自上次 `take_rendered` 以来记录下的 `(key, 整句)`:`tr()` 那条带占位符的路径(M180 加)。
|
||||||
|
//
|
||||||
|
// 为什么单独一份而不是把上面那份改成 `String`:M179 的守卫
|
||||||
|
// (`pages/settings/tests/appearance_section.rs::the_appearance_group_speaks_chinese_when_the_interface_does`)
|
||||||
|
// 断言的是 `&'static str`,那个文件不在 M180 的 scope 里、不能跟着改签名。两份记录各自的
|
||||||
|
// `take*` 也各自清空,互不干扰。
|
||||||
|
thread_local! {
|
||||||
|
#[allow(clippy::missing_const_for_thread_local)]
|
||||||
|
static RENDERED: RefCell<Vec<(Key, String)>> = const { RefCell::new(Vec::new()) };
|
||||||
|
}
|
||||||
|
|
||||||
/// 记一条(由 [`super::section::t`] 调用;不对外)。
|
/// 记一条(由 [`super::section::t`] 调用;不对外)。
|
||||||
pub(crate) fn record(key: Key, text: &'static str) {
|
pub(crate) fn record(key: Key, text: &'static str) {
|
||||||
TEXTS.with(|texts| texts.borrow_mut().push((key, text)));
|
TEXTS.with(|texts| texts.borrow_mut().push((key, text)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 记一条**替换完占位符**的整句(由 [`super::section::tr`] 调用;不对外)。
|
||||||
|
pub(crate) fn record_rendered(key: Key, text: String) {
|
||||||
|
RENDERED.with(|texts| texts.borrow_mut().push((key, text)));
|
||||||
|
}
|
||||||
|
|
||||||
/// 取走并清空本线程记录下的取词(测试用)。
|
/// 取走并清空本线程记录下的取词(测试用)。
|
||||||
pub(crate) fn take() -> Vec<(Key, &'static str)> {
|
pub(crate) fn take() -> Vec<(Key, &'static str)> {
|
||||||
TEXTS.with(|texts| std::mem::take(&mut *texts.borrow_mut()))
|
TEXTS.with(|texts| std::mem::take(&mut *texts.borrow_mut()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 取走并清空本线程记录下的整句(测试用)。
|
||||||
|
pub(crate) fn take_rendered() -> Vec<(Key, String)> {
|
||||||
|
RENDERED.with(|texts| std::mem::take(&mut *texts.borrow_mut()))
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ use iced::{Element, Length};
|
|||||||
|
|
||||||
use unlucky_design::color::Role;
|
use unlucky_design::color::Role;
|
||||||
use unlucky_design::spacing::Space;
|
use unlucky_design::spacing::Space;
|
||||||
|
use unlucky_i18n::catalog::Key;
|
||||||
|
use unlucky_i18n::Lang;
|
||||||
use unlucky_presenter::prefs::BgFill;
|
use unlucky_presenter::prefs::BgFill;
|
||||||
|
|
||||||
use crate::app::components::inputs::{slider, text_field, Field, Slider};
|
use crate::app::components::inputs::{slider, text_field, Field, Slider};
|
||||||
@@ -15,19 +17,35 @@ use crate::pages::Message;
|
|||||||
use crate::theme::Ui;
|
use crate::theme::Ui;
|
||||||
|
|
||||||
use super::super::background::{self, BG_DIM_MAX};
|
use super::super::background::{self, BG_DIM_MAX};
|
||||||
|
use super::super::palette;
|
||||||
use super::super::state::State;
|
use super::super::state::State;
|
||||||
use super::super::Change;
|
use super::super::Change;
|
||||||
use super::{choice, choice_row, label, note_line, slider_row};
|
use super::{choice, choice_row, fixed2, label, note_line, slider_row, t, tr, tr_note};
|
||||||
|
|
||||||
/// 把 ④ 开关、⑤ 路径、⑥ 诊断、⑦ 填充、⑧ 暗化按视觉顺序推进 `rows`。
|
/// 把 ④ 开关、⑤ 路径、⑥ 诊断、⑦ 填充、⑧ 暗化按视觉顺序推进 `rows`。
|
||||||
pub(super) fn push<'a>(ui: &'a Ui, state: &'a State, rows: &mut Vec<Element<'a, Message>>) {
|
pub(super) fn push<'a>(
|
||||||
|
ui: &'a Ui,
|
||||||
|
state: &'a State,
|
||||||
|
lang: Lang,
|
||||||
|
rows: &mut Vec<Element<'a, Message>>,
|
||||||
|
) {
|
||||||
// ④ 背景图开关
|
// ④ 背景图开关
|
||||||
rows.push(choice_row(
|
rows.push(choice_row(
|
||||||
ui,
|
ui,
|
||||||
"Background image",
|
t(Key::SettingsAppearanceBackgroundImage, lang),
|
||||||
vec![
|
vec![
|
||||||
choice(ui, "Off", !state.bg_enabled, Change::BgEnabled(false)),
|
choice(
|
||||||
choice(ui, "On", state.bg_enabled, Change::BgEnabled(true)),
|
ui,
|
||||||
|
t(Key::SettingsAppearanceOff, lang),
|
||||||
|
!state.bg_enabled,
|
||||||
|
Change::BgEnabled(false),
|
||||||
|
),
|
||||||
|
choice(
|
||||||
|
ui,
|
||||||
|
t(Key::SettingsAppearanceOn, lang),
|
||||||
|
state.bg_enabled,
|
||||||
|
Change::BgEnabled(true),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -35,57 +53,72 @@ pub(super) fn push<'a>(ui: &'a Ui, state: &'a State, rows: &mut Vec<Element<'a,
|
|||||||
let path_field: Element<'a, Message> = text_field(
|
let path_field: Element<'a, Message> = text_field(
|
||||||
ui,
|
ui,
|
||||||
Field::new(&state.bg_path)
|
Field::new(&state.bg_path)
|
||||||
.label("Image path")
|
.label(t(Key::SettingsAppearanceImagePath, lang))
|
||||||
.placeholder("D:/Pictures/background.png"),
|
.placeholder("D:/Pictures/background.png"),
|
||||||
|value| Message::Appearance(Change::BgImage(value)),
|
|value| Message::Appearance(Change::BgImage(value)),
|
||||||
);
|
);
|
||||||
rows.push(
|
rows.push(
|
||||||
column![label(ui, "Image path"), path_field]
|
column![
|
||||||
.spacing(ui.space(Space::X1))
|
label(ui, t(Key::SettingsAppearanceImagePath, lang)),
|
||||||
.width(Length::Fill)
|
path_field
|
||||||
.into(),
|
]
|
||||||
|
.spacing(ui.space(Space::X1))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ⑥ 诊断(读盘失败 / CPU 降级 / 配色回落)——**不许静默**,且必须在**首屏可见处**
|
// ⑥ 诊断(读盘失败 / 配色回落)——**不许静默**,且必须在**首屏可见处**
|
||||||
// (复审 r1 P2-3:文案落在折叠线以下等于没提示)。位置就在背景图那两行的紧下方。
|
// (复审 r1 P2-3:文案落在折叠线以下等于没提示)。位置就在背景图那两行的紧下方。
|
||||||
|
//
|
||||||
|
// CPU 档的「面板/玻璃被强制」那句**不在这里**:它是张卡级状态,挂在被禁用的面板滑块旁边
|
||||||
|
// (`panel.rs` 的 `PanelOpacityForcedHint`),一屏只出现一次(M180 复审 P2-1)。
|
||||||
for note in state.diagnostics() {
|
for note in state.diagnostics() {
|
||||||
rows.push(note_line(ui, note, ui.color(Role::Error)));
|
let text = tr_note(lang, note);
|
||||||
}
|
rows.push(note_line(ui, &text, ui.color(Role::Error)));
|
||||||
if let Some(note) = state.backend_note() {
|
|
||||||
rows.push(note_line(ui, note, ui.color(Role::OnSurfaceVariant)));
|
|
||||||
}
|
}
|
||||||
if let Some(note) = state
|
if let Some(note) = state
|
||||||
.image()
|
.image()
|
||||||
.and_then(|image| background::small_tile_note(image, state.bg_fill))
|
.and_then(|image| background::small_tile_note(image, state.bg_fill))
|
||||||
{
|
{
|
||||||
rows.push(note_line(ui, ¬e, ui.color(Role::OnSurfaceVariant)));
|
let text = tr_note(lang, ¬e);
|
||||||
|
rows.push(note_line(ui, &text, ui.color(Role::OnSurfaceVariant)));
|
||||||
}
|
}
|
||||||
if let Some(note) = state.palette_note() {
|
if let Some(key) = state.palette_note() {
|
||||||
rows.push(note_line(ui, note, ui.color(Role::OnSurfaceVariant)));
|
rows.push(note_line(
|
||||||
|
ui,
|
||||||
|
t(key, lang),
|
||||||
|
ui.color(Role::OnSurfaceVariant),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let resolved = format!("Colours: {}", state.palette_level());
|
// 生效的配色层级:显示的是**回落之后真正生效**的那一级(与底部「配色来源」里用户选的那一档
|
||||||
rows.push(note_line(ui, &resolved, ui.color(Role::OnSurfaceVariant)));
|
// 不一定相同)。码值 → 已有的三条档位 key(不新增平行词表),所以中文界面也读得懂。
|
||||||
|
let level = tr(
|
||||||
|
lang,
|
||||||
|
Key::SettingsAppearancePaletteLevel,
|
||||||
|
&[("level", t(palette::level_key(state.palette_level()), lang))],
|
||||||
|
);
|
||||||
|
rows.push(note_line(ui, &level, ui.color(Role::OnSurfaceVariant)));
|
||||||
|
|
||||||
// ⑦ 填充
|
// ⑦ 填充
|
||||||
rows.push(choice_row(
|
rows.push(choice_row(
|
||||||
ui,
|
ui,
|
||||||
"Fill",
|
t(Key::SettingsAppearanceBgFill, lang),
|
||||||
vec![
|
vec![
|
||||||
choice(
|
choice(
|
||||||
ui,
|
ui,
|
||||||
"Cover",
|
t(Key::SettingsAppearanceBgFillCover, lang),
|
||||||
state.bg_fill == BgFill::Cover,
|
state.bg_fill == BgFill::Cover,
|
||||||
Change::BgFill(BgFill::Cover),
|
Change::BgFill(BgFill::Cover),
|
||||||
),
|
),
|
||||||
choice(
|
choice(
|
||||||
ui,
|
ui,
|
||||||
"Contain",
|
t(Key::SettingsAppearanceBgFillContain, lang),
|
||||||
state.bg_fill == BgFill::Contain,
|
state.bg_fill == BgFill::Contain,
|
||||||
Change::BgFill(BgFill::Contain),
|
Change::BgFill(BgFill::Contain),
|
||||||
),
|
),
|
||||||
choice(
|
choice(
|
||||||
ui,
|
ui,
|
||||||
"Tile",
|
t(Key::SettingsAppearanceBgFillTile, lang),
|
||||||
state.bg_fill == BgFill::Tile,
|
state.bg_fill == BgFill::Tile,
|
||||||
Change::BgFill(BgFill::Tile),
|
Change::BgFill(BgFill::Tile),
|
||||||
),
|
),
|
||||||
@@ -95,10 +128,12 @@ pub(super) fn push<'a>(ui: &'a Ui, state: &'a State, rows: &mut Vec<Element<'a,
|
|||||||
// ⑧ 暗化
|
// ⑧ 暗化
|
||||||
rows.push(slider_row(
|
rows.push(slider_row(
|
||||||
ui,
|
ui,
|
||||||
"Dim",
|
t(Key::SettingsAppearanceBgDim, lang),
|
||||||
format!("{:.0}%", state.bg_dim * 100.0),
|
format!("{:.0}%", state.bg_dim * 100.0),
|
||||||
format!(
|
tr(
|
||||||
"Blends the image toward the panel colour so text stays readable. Max {BG_DIM_MAX:.2}."
|
lang,
|
||||||
|
Key::SettingsAppearanceBgDimHint,
|
||||||
|
&[("max", &fixed2(BG_DIM_MAX))],
|
||||||
),
|
),
|
||||||
state.bg_dim > 0.0,
|
state.bg_dim > 0.0,
|
||||||
slider(
|
slider(
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
//! 外观分组第 ⑨ 段:**配色来源**(背景图 / 壁纸 / 色卡三档;它选的正是 D38 三级优先序的起点)。
|
//! 外观分组第 ⑨ 段:**配色来源**(背景图 / 壁纸 / 色卡三档;它选的正是 D38 三级优先序的起点)。
|
||||||
|
|
||||||
use iced::Element;
|
use iced::Element;
|
||||||
|
use unlucky_i18n::catalog::Key;
|
||||||
|
use unlucky_i18n::Lang;
|
||||||
use unlucky_presenter::prefs::PaletteSource;
|
use unlucky_presenter::prefs::PaletteSource;
|
||||||
|
|
||||||
use crate::pages::Message;
|
use crate::pages::Message;
|
||||||
@@ -9,30 +10,35 @@ use crate::theme::Ui;
|
|||||||
|
|
||||||
use super::super::state::State;
|
use super::super::state::State;
|
||||||
use super::super::Change;
|
use super::super::Change;
|
||||||
use super::{choice, choice_row};
|
use super::{choice, choice_row, t};
|
||||||
|
|
||||||
/// 把 ⑨ 配色来源推进行 `rows`。
|
/// 把 ⑨ 配色来源推进行 `rows`。
|
||||||
pub(super) fn push<'a>(ui: &'a Ui, state: &'a State, rows: &mut Vec<Element<'a, Message>>) {
|
pub(super) fn push<'a>(
|
||||||
|
ui: &'a Ui,
|
||||||
|
state: &'a State,
|
||||||
|
lang: Lang,
|
||||||
|
rows: &mut Vec<Element<'a, Message>>,
|
||||||
|
) {
|
||||||
// ⑨ 配色来源
|
// ⑨ 配色来源
|
||||||
rows.push(choice_row(
|
rows.push(choice_row(
|
||||||
ui,
|
ui,
|
||||||
"Colour source",
|
t(Key::SettingsAppearancePaletteSource, lang),
|
||||||
vec![
|
vec![
|
||||||
choice(
|
choice(
|
||||||
ui,
|
ui,
|
||||||
"Background",
|
t(Key::SettingsAppearancePaletteBackground, lang),
|
||||||
state.palette_source == PaletteSource::BackgroundImage,
|
state.palette_source == PaletteSource::BackgroundImage,
|
||||||
Change::PaletteSource(PaletteSource::BackgroundImage),
|
Change::PaletteSource(PaletteSource::BackgroundImage),
|
||||||
),
|
),
|
||||||
choice(
|
choice(
|
||||||
ui,
|
ui,
|
||||||
"Wallpaper",
|
t(Key::SettingsAppearancePaletteWallpaper, lang),
|
||||||
state.palette_source == PaletteSource::Wallpaper,
|
state.palette_source == PaletteSource::Wallpaper,
|
||||||
Change::PaletteSource(PaletteSource::Wallpaper),
|
Change::PaletteSource(PaletteSource::Wallpaper),
|
||||||
),
|
),
|
||||||
choice(
|
choice(
|
||||||
ui,
|
ui,
|
||||||
"Preset",
|
t(Key::SettingsAppearancePalettePreset, lang),
|
||||||
state.palette_source == PaletteSource::Preset,
|
state.palette_source == PaletteSource::Preset,
|
||||||
Change::PaletteSource(PaletteSource::Preset),
|
Change::PaletteSource(PaletteSource::Preset),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
//! | 面板透明度滑块(下限 0.4,CPU 档禁用) | `appearance.panel_alpha` | [`Change::PanelAlpha`] |
|
//! | 面板透明度滑块(下限 0.4,CPU 档禁用) | `appearance.panel_alpha` | [`Change::PanelAlpha`] |
|
||||||
//! | 背景图开关 | `appearance.bg_image_enabled` | [`Change::BgEnabled`] |
|
//! | 背景图开关 | `appearance.bg_image_enabled` | [`Change::BgEnabled`] |
|
||||||
//! | 背景图路径(文本框) | `appearance.bg_image` | [`Change::BgImage`] |
|
//! | 背景图路径(文本框) | `appearance.bg_image` | [`Change::BgImage`] |
|
||||||
//! | 填充(封面 / 拉伸 / 平铺) | `appearance.bg_fill` | [`Change::BgFill`] |
|
//! | 填充(铺满 / 完整显示 / 平铺) | `appearance.bg_fill` | [`Change::BgFill`] |
|
||||||
//! | 暗化强度(0~0.95) | `appearance.bg_dim` | [`Change::BgDim`] |
|
//! | 暗化强度(0~0.95) | `appearance.bg_dim` | [`Change::BgDim`] |
|
||||||
//! | 配色来源(三档) | `appearance.palette_source` | [`Change::PaletteSource`] |
|
//! | 配色来源(三档) | `appearance.palette_source` | [`Change::PaletteSource`] |
|
||||||
//!
|
//!
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
//!
|
//!
|
||||||
//! | 文件 | 是什么 |
|
//! | 文件 | 是什么 |
|
||||||
//! |---|---|
|
//! |---|---|
|
||||||
//! | 本文件(`section/mod.rs`) | **壳**:[`section`] 按顺序拼各段 + [`card`] 卡片 + 行控件([`label`] / [`slider_row`] / [`choice_row`] / [`choice`] / [`note_line`])+ 取词口 [`t`] |
|
//! | 本文件(`section/mod.rs`) | **壳**:[`section`] 按顺序拼各段 + [`card`] 卡片 + 行控件([`label`] / [`slider_row`] / [`choice_row`] / [`choice`] / [`note_line`])+ 两个取词口 [`t`] / [`tr`] |
|
||||||
//! | [`window`] | ① 整窗透明度 + ② 窗口圆角(含「因整窗 α 不生效」的 notice 与说明段) |
|
//! | [`window`] | ① 整窗透明度 + ② 窗口圆角(含「因整窗 α 不生效」的 notice 与说明段) |
|
||||||
//! | [`panel`] | ③ 面板透明度(CPU 档强制 1.0,值 / 生效值分开显示) |
|
//! | [`panel`] | ③ 面板透明度(CPU 档强制 1.0,值 / 生效值分开显示) |
|
||||||
//! | [`bg_image`] | ④ 背景图开关 + ⑤ 路径 + ⑥ 诊断 + ⑦ 填充 + ⑧ 暗化 |
|
//! | [`bg_image`] | ④ 背景图开关 + ⑤ 路径 + ⑥ 诊断 + ⑦ 填充 + ⑧ 暗化 |
|
||||||
@@ -35,12 +35,18 @@
|
|||||||
//! 4. 唯一一处非 `Element` 实参:`rows.push(column![…])` → 末尾加 `.into()`(`Vec::push` 不做 `Into`);
|
//! 4. 唯一一处非 `Element` 实参:`rows.push(column![…])` → 末尾加 `.into()`(`Vec::push` 不做 `Into`);
|
||||||
//! 5. rustfmt 因为多出来的 `, lang` 把两处调用换成了多行。
|
//! 5. rustfmt 因为多出来的 `, lang` 把两处调用换成了多行。
|
||||||
//!
|
//!
|
||||||
//! ## 语言(M179 只接管子,不补文案)
|
//! ## 语言(M179 接过管子,M180 补齐文案)
|
||||||
//!
|
//!
|
||||||
//! [`t`] 是**唯一**取词口,现在收 `lang`(真值从 `settings::view` 的 `Language.effective`
|
//! [`t`] / [`tr`] 是**唯一**两个取词口(一个拿 `&'static str`、一个拿带占位符的 owned 句子),
|
||||||
//! → `Group.body` → [`section`] 一路递进来),所以中文界面上走词表的那 5 处(窗口圆角一行、
|
//! `lang` 从 `settings::view` 的 `Language.effective` → `Group.body` → [`section`] 一路递进来。
|
||||||
//! 两个选项、说明段、notice)真的是中文。**其余文案仍是英文基线**(字面量,没有 key)——
|
//! M179 只把管子接通(当时本组 27 条文案里真正本地化的只有圆角那 5 条);M180 `I18NAPPEAR` 把
|
||||||
//! 补文案是下一条 mission,M179 只负责让「管子」通到这一层。
|
//! 剩下 22 条字面量与 `state.rs` / `background.rs` / `palette.rs` 的运行时诊断全部搬进
|
||||||
|
//! `catalog/settings/appearance.rs`,于是中文界面上这一页的**卡片文案**没有一句是英文。
|
||||||
|
//!
|
||||||
|
//! 两处例外(M180 复审 L-1 记下的口径):① 写进 `{reason}` 的**原因串**里,来自本仓库自写的
|
||||||
|
//! 两条(`"no path configured"`、`"unknown image format …"`)与 OS / 解码库给出的文本仍是英文——
|
||||||
|
//! 它们是不可翻译的第三方或诊断原文;② `Windows 11` / `macOS` / `Linux` / `PNG` / `JPEG` / `BMP`
|
||||||
|
//! 这些专名。除此之外这一页不出现英文。
|
||||||
//!
|
//!
|
||||||
//! ## 为什么「值」与「生效值」分开显示
|
//! ## 为什么「值」与「生效值」分开显示
|
||||||
//!
|
//!
|
||||||
@@ -80,9 +86,9 @@ pub fn section<'a>(ui: &'a Ui, state: &'a State, lang: Lang) -> Element<'a, Mess
|
|||||||
let mut rows: Vec<Element<'a, Message>> = Vec::new();
|
let mut rows: Vec<Element<'a, Message>> = Vec::new();
|
||||||
|
|
||||||
window::push(ui, state, lang, &mut rows);
|
window::push(ui, state, lang, &mut rows);
|
||||||
panel::push(ui, state, &mut rows);
|
panel::push(ui, state, lang, &mut rows);
|
||||||
bg_image::push(ui, state, &mut rows);
|
bg_image::push(ui, state, lang, &mut rows);
|
||||||
colour_source::push(ui, state, &mut rows);
|
colour_source::push(ui, state, lang, &mut rows);
|
||||||
|
|
||||||
card(
|
card(
|
||||||
ui,
|
ui,
|
||||||
@@ -126,8 +132,8 @@ pub(super) fn label<'a>(ui: &'a Ui, content: &'a str) -> Element<'a, Message> {
|
|||||||
///
|
///
|
||||||
/// `lang` 是**这一次生效的语言**,由 [`section`] 一路递下来(M179 `I18NSEAM` 之前这里按英文目录
|
/// `lang` 是**这一次生效的语言**,由 [`section`] 一路递下来(M179 `I18NSEAM` 之前这里按英文目录
|
||||||
/// 取词,于是中文界面上本分组的 27 条文案里真正本地化 0 条)。现在这个函数是全模块
|
/// 取词,于是中文界面上本分组的 27 条文案里真正本地化 0 条)。现在这个函数是全模块
|
||||||
/// **唯一**的取词点:谁硬编码英文,就不会经过这里——M179 报告里逐行列了那些字面量,补文案是
|
/// **唯一**的取词点:谁硬编码英文,就不会经过这里——M180 `I18NAPPEAR` 已把这一页剩余的 22 处
|
||||||
/// 下一条 mission。
|
/// 字面量与全部运行时诊断搬进词表,见 `catalog/settings/appearance.rs` 的模块文档。
|
||||||
pub(super) fn t(key: Key, lang: Lang) -> &'static str {
|
pub(super) fn t(key: Key, lang: Lang) -> &'static str {
|
||||||
let text = lang.catalog().text(key);
|
let text = lang.catalog().text(key);
|
||||||
// 测试专用:把「这一帧真的取了哪条译文」记一份(iced 0.14 的 `Element` 没有可遍历控件树的
|
// 测试专用:把「这一帧真的取了哪条译文」记一份(iced 0.14 的 `Element` 没有可遍历控件树的
|
||||||
@@ -137,6 +143,28 @@ pub(super) fn t(key: Key, lang: Lang) -> &'static str {
|
|||||||
text
|
text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 带占位符的句子(`{name}` 替换后的 owned 版本)。
|
||||||
|
///
|
||||||
|
/// 为什么不直接 `lang.catalog().render(...)`:这条路径也要进 [`t`] 那份取证记录
|
||||||
|
/// (`probe::record_rendered`),否则「中文界面下这一页每一句都不是纯 ASCII」的守卫会漏掉所有
|
||||||
|
/// 带数字的说明句。数值参数由调用方先格式化好(词表只认 `{name}`,不认 `{:.2}`)。
|
||||||
|
pub(super) fn tr(lang: Lang, key: Key, args: &[(&str, &str)]) -> String {
|
||||||
|
let text = lang.catalog().render(key, args);
|
||||||
|
#[cfg(test)]
|
||||||
|
super::probe::record_rendered(key, text.clone());
|
||||||
|
text
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 两位小数(`{:.2}` 这种格式说明由 Rust 侧做,词表里只留 `{name}`)。
|
||||||
|
pub(super) fn fixed2(value: f32) -> String {
|
||||||
|
format!("{value:.2}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 一条 [`Note`](key + 已格式化的参数)按语言取整句;与 [`tr`] 共用取证口。
|
||||||
|
pub(super) fn tr_note(lang: Lang, note: &super::background::Note) -> String {
|
||||||
|
tr(lang, note.key(), ¬e.args())
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn note_line<'a>(ui: &'a Ui, content: &str, color: Color) -> Element<'a, Message> {
|
pub(super) fn note_line<'a>(ui: &'a Ui, content: &str, color: Color) -> Element<'a, Message> {
|
||||||
// 诊断文案是**运行时拼出来的**(路径、错误原因),只能走 owned 版本。
|
// 诊断文案是**运行时拼出来的**(路径、错误原因),只能走 owned 版本。
|
||||||
frame::text_role_owned(ui, TypeRole::BodySmall, false, content.to_owned(), color)
|
frame::text_role_owned(ui, TypeRole::BodySmall, false, content.to_owned(), color)
|
||||||
|
|||||||
@@ -2,28 +2,45 @@
|
|||||||
|
|
||||||
use iced::Element;
|
use iced::Element;
|
||||||
|
|
||||||
|
use unlucky_i18n::catalog::Key;
|
||||||
|
use unlucky_i18n::Lang;
|
||||||
|
|
||||||
use crate::app::components::inputs::{slider, Slider};
|
use crate::app::components::inputs::{slider, Slider};
|
||||||
use crate::pages::Message;
|
use crate::pages::Message;
|
||||||
use crate::theme::Ui;
|
use crate::theme::Ui;
|
||||||
|
|
||||||
use super::super::alpha::{panel_alpha_from_fraction, panel_fraction};
|
use super::super::alpha::{panel_alpha_from_fraction, panel_fraction, PANEL_ALPHA_FLOOR};
|
||||||
use super::super::state::State;
|
use super::super::state::State;
|
||||||
use super::super::Change;
|
use super::super::Change;
|
||||||
use super::slider_row;
|
use super::{fixed2, slider_row, t, tr};
|
||||||
|
|
||||||
/// 把 ③ 面板透明度推进 `rows`。
|
/// 把 ③ 面板透明度推进 `rows`。
|
||||||
pub(super) fn push<'a>(ui: &'a Ui, state: &'a State, rows: &mut Vec<Element<'a, Message>>) {
|
pub(super) fn push<'a>(
|
||||||
|
ui: &'a Ui,
|
||||||
|
state: &'a State,
|
||||||
|
lang: Lang,
|
||||||
|
rows: &mut Vec<Element<'a, Message>>,
|
||||||
|
) {
|
||||||
// ③ 面板透明度(CPU 档强制 1.0)
|
// ③ 面板透明度(CPU 档强制 1.0)
|
||||||
let forced = state.panel_is_forced();
|
let forced = state.panel_is_forced();
|
||||||
let panel_percent = if forced {
|
let panel_percent = if forced {
|
||||||
format!("100% (forced from {:.2})", state.panel_alpha)
|
// 数值占位:`{requested}` 是请求值(词表不认 `{:.2}`,所以在这里先格式化成两位小数)。
|
||||||
|
tr(
|
||||||
|
lang,
|
||||||
|
Key::SettingsAppearancePanelOpacityForcedValue,
|
||||||
|
&[("requested", &fixed2(state.panel_alpha))],
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
format!("{:.0}%", state.effective_panel_alpha() * 100.0)
|
format!("{:.0}%", state.effective_panel_alpha() * 100.0)
|
||||||
};
|
};
|
||||||
let panel_hint = if forced {
|
let panel_hint = if forced {
|
||||||
"Software renderer: panels stay fully opaque (window transparency still works)."
|
t(Key::SettingsAppearancePanelOpacityForcedHint, lang).to_owned()
|
||||||
} else {
|
} else {
|
||||||
"Only the panel/card layer. Floor 0.40; independent of window transparency."
|
tr(
|
||||||
|
lang,
|
||||||
|
Key::SettingsAppearancePanelOpacityHint,
|
||||||
|
&[("floor", &fixed2(PANEL_ALPHA_FLOOR))],
|
||||||
|
)
|
||||||
};
|
};
|
||||||
let panel_control = if forced {
|
let panel_control = if forced {
|
||||||
slider(Slider::new(ui, panel_fraction(1.0)))
|
slider(Slider::new(ui, panel_fraction(1.0)))
|
||||||
@@ -36,9 +53,9 @@ pub(super) fn push<'a>(ui: &'a Ui, state: &'a State, rows: &mut Vec<Element<'a,
|
|||||||
};
|
};
|
||||||
rows.push(slider_row(
|
rows.push(slider_row(
|
||||||
ui,
|
ui,
|
||||||
"Panel transparency",
|
t(Key::SettingsAppearancePanelOpacity, lang),
|
||||||
panel_percent,
|
panel_percent,
|
||||||
panel_hint.to_owned(),
|
panel_hint,
|
||||||
!forced,
|
!forced,
|
||||||
panel_control,
|
panel_control,
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use crate::theme::Ui;
|
|||||||
use super::super::alpha::{window_alpha_from_fraction, window_fraction, WINDOW_ALPHA_FLOOR};
|
use super::super::alpha::{window_alpha_from_fraction, window_fraction, WINDOW_ALPHA_FLOOR};
|
||||||
use super::super::state::State;
|
use super::super::state::State;
|
||||||
use super::super::Change;
|
use super::super::Change;
|
||||||
use super::{choice, choice_row, note_line, slider_row, t};
|
use super::{choice, choice_row, fixed2, note_line, slider_row, t, tr};
|
||||||
|
|
||||||
/// 把 ① 整窗透明度与 ② 窗口圆角按视觉顺序推进 `rows`。
|
/// 把 ① 整窗透明度与 ② 窗口圆角按视觉顺序推进 `rows`。
|
||||||
pub(super) fn push<'a>(
|
pub(super) fn push<'a>(
|
||||||
@@ -33,11 +33,12 @@ pub(super) fn push<'a>(
|
|||||||
let window_percent = format!("{:.0}%", state.window_opacity * 100.0);
|
let window_percent = format!("{:.0}%", state.window_opacity * 100.0);
|
||||||
rows.push(slider_row(
|
rows.push(slider_row(
|
||||||
ui,
|
ui,
|
||||||
"Window transparency",
|
t(Key::SettingsAppearanceWindowOpacity, lang),
|
||||||
window_percent,
|
window_percent,
|
||||||
format!(
|
tr(
|
||||||
"Applies to the whole window. Floor {:.2}; 1.00 = fully opaque.",
|
lang,
|
||||||
WINDOW_ALPHA_FLOOR
|
Key::SettingsAppearanceWindowOpacityHint,
|
||||||
|
&[("floor", &fixed2(WINDOW_ALPHA_FLOOR))],
|
||||||
),
|
),
|
||||||
state.window_opacity < 1.0,
|
state.window_opacity < 1.0,
|
||||||
slider(
|
slider(
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
//! 变成一条**可读诊断**而不是一个 `unwrap`。
|
//! 变成一条**可读诊断**而不是一个 `unwrap`。
|
||||||
|
|
||||||
use unlucky_design::color::{Scheme, SchemeFlavor};
|
use unlucky_design::color::{Scheme, SchemeFlavor};
|
||||||
|
use unlucky_i18n::catalog::Key;
|
||||||
use unlucky_presenter::prefs::{
|
use unlucky_presenter::prefs::{
|
||||||
AppearanceBase, AppearancePrefs, BgFill, PaletteSource, WindowCorner,
|
AppearanceBase, AppearancePrefs, BgFill, PaletteSource, WindowCorner,
|
||||||
};
|
};
|
||||||
@@ -24,7 +25,7 @@ use unlucky_presenter::prefs::{
|
|||||||
use crate::Backend;
|
use crate::Backend;
|
||||||
|
|
||||||
use super::alpha::{self, effective_glass, effective_panel_alpha, panel_is_forced_opaque};
|
use super::alpha::{self, effective_glass, effective_panel_alpha, panel_is_forced_opaque};
|
||||||
use super::background::{self, Image};
|
use super::background::{self, Image, Note};
|
||||||
use super::palette::{self, Resolved, Source};
|
use super::palette::{self, Resolved, Source};
|
||||||
|
|
||||||
/// 取证开关的环境变量名(截图矩阵与真机日志用;生产默认全部不设)。
|
/// 取证开关的环境变量名(截图矩阵与真机日志用;生产默认全部不设)。
|
||||||
@@ -67,8 +68,8 @@ pub struct State {
|
|||||||
pub selected_group: usize,
|
pub selected_group: usize,
|
||||||
/// 已解码的背景图(`None` = 没设 / 读失败)。
|
/// 已解码的背景图(`None` = 没设 / 读失败)。
|
||||||
image: Option<Image>,
|
image: Option<Image>,
|
||||||
/// 读盘/解码失败的**可读诊断**(按发生顺序追加)。
|
/// 读盘/解码失败的**可读诊断**(按发生顺序追加;`Note` = key + 参数,句子在词表里)。
|
||||||
diagnostics: Vec<String>,
|
diagnostics: Vec<Note>,
|
||||||
/// 上一次的配色解析结果。
|
/// 上一次的配色解析结果。
|
||||||
resolved: Option<Resolved>,
|
resolved: Option<Resolved>,
|
||||||
/// 当前深浅(配色解析的输入)。
|
/// 当前深浅(配色解析的输入)。
|
||||||
@@ -196,24 +197,15 @@ impl State {
|
|||||||
Some(&resolved.scheme)
|
Some(&resolved.scheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 读盘/解码失败的诊断(设置页逐条显示)。
|
/// 读盘/解码失败的诊断(设置页逐条显示;一条 = 一条 key + 它的参数)。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn diagnostics(&self) -> &[String] {
|
pub fn diagnostics(&self) -> &[Note] {
|
||||||
&self.diagnostics
|
&self.diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CPU 档降级的说明(`None` = 没有降级)。
|
/// 配色来源的提示 key(`None` = 一级成功)。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn backend_note(&self) -> Option<&'static str> {
|
pub fn palette_note(&self) -> Option<Key> {
|
||||||
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()
|
self.resolved.as_ref()?.note()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +354,7 @@ impl State {
|
|||||||
}
|
}
|
||||||
match background::load(trimmed) {
|
match background::load(trimmed) {
|
||||||
Ok(image) => self.image = Some(image),
|
Ok(image) => self.image = Some(image),
|
||||||
Err(error) => self.diagnostics.push(error.describe()),
|
Err(error) => self.diagnostics.push(error.note()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
//! `pages::appearance` 的单测:`Change` 的落盘映射(M93)+ **中文界面文案的取证守卫**(M180)。
|
||||||
|
//!
|
||||||
|
//! 为什么是独立文件:`mod.rs` 已经 220 行上下,而 M180 要在模块里加三条取证用例。搬移口径与
|
||||||
|
//! `background/tests.rs` 一致——既有两条用例**一字未动**,只是从 `mod.rs` 的行内 `mod tests` 挪到这里。
|
||||||
|
//!
|
||||||
|
//! ## M180 的两条取证守卫,它们「抓得住什么」
|
||||||
|
//!
|
||||||
|
//! iced 0.14 没有可遍历控件树的公开接口(见 [`super::probe`] 的模块文档),所以取证点只能是
|
||||||
|
//! 取词口 `section::t` / `section::tr`。于是这一对守卫分工如下:
|
||||||
|
//!
|
||||||
|
//! | 守卫 | 抓得住 | 抓不住 |
|
||||||
|
//! |---|---|---|
|
||||||
|
//! | [`the_appearance_card_speaks_chinese`] | 中文界面下取到的每一条文案都是纯 ASCII(= 有人把某条 key 的英文写回中文表 / 中文表里放英文原句) | 硬编码字面量(它压根不过取词口) |
|
||||||
|
//! | [`every_appearance_card_key_is_drawn`] | 某一处控件被改回**写死英文**:那条 key 不再被取,集合里就少一个元素 | 新加一条字面量(新 key 还没有,集合不看它) |
|
||||||
|
//!
|
||||||
|
//! 两条合起来才等于「中文界面下外观卡片里不再出现英文字面量」——单看任一条都有一个真会发生的
|
||||||
|
//! 漏网口,所以这里刻意写两条。
|
||||||
|
|
||||||
|
use super::{Change, APPEARANCE_GROUP};
|
||||||
|
use unlucky_i18n::catalog::Key;
|
||||||
|
use unlucky_i18n::Lang;
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------- 文案(M180 I18NAPPEAR)
|
||||||
|
|
||||||
|
/// 画一帧外观卡片,取走这一帧从词表取到的**全部**字:`(key, 文案)`。
|
||||||
|
///
|
||||||
|
/// 三档状态各画一帧:
|
||||||
|
/// - [`Case::Factory`]:auto 档、没设背景图——覆盖常驻的标签 / 选项 / 说明;
|
||||||
|
/// - [`Case::Cpu`]:CPU 档——覆盖「强制 100%」那一行与软件渲染说明(这两条只在降级态出现);
|
||||||
|
/// - [`Case::Dim`]:整窗 α 调低——覆盖「圆角暂时不生效」那条 notice。
|
||||||
|
///
|
||||||
|
/// 两条取词路径都要收:`t()` 的 `&'static str` 与 `tr()` 替换完占位符的整句(后者不进前者的记录)。
|
||||||
|
fn appearance_texts(lang: Lang, case: Case) -> Vec<(Key, String)> {
|
||||||
|
use unlucky_design::color::SchemeFlavor;
|
||||||
|
use unlucky_presenter::prefs::{AppearanceBase, AppearancePrefs};
|
||||||
|
|
||||||
|
use super::probe;
|
||||||
|
use crate::cli::Backend;
|
||||||
|
use crate::theme::{Families, Ui};
|
||||||
|
|
||||||
|
const FAMILIES: Families = Families {
|
||||||
|
text: "Segoe UI",
|
||||||
|
display: "Segoe UI",
|
||||||
|
mono: "Consolas",
|
||||||
|
};
|
||||||
|
|
||||||
|
let (backend, alpha) = match case {
|
||||||
|
Case::Factory => (Backend::Auto, 1.0),
|
||||||
|
Case::Cpu => (Backend::Cpu, 1.0),
|
||||||
|
Case::Dim => (Backend::Gpu, 0.5),
|
||||||
|
};
|
||||||
|
let mut state = super::State::new(
|
||||||
|
&AppearancePrefs::factory(),
|
||||||
|
&AppearanceBase::factory(),
|
||||||
|
backend,
|
||||||
|
SchemeFlavor::Light,
|
||||||
|
);
|
||||||
|
if alpha < 1.0 {
|
||||||
|
state.apply(Change::WindowOpacity(alpha));
|
||||||
|
}
|
||||||
|
|
||||||
|
let ui = Ui::new(SchemeFlavor::Light, FAMILIES);
|
||||||
|
// 前面几条用例可能留了记录:本线程的取证每次都从清空开始。
|
||||||
|
probe::take();
|
||||||
|
probe::take_rendered();
|
||||||
|
|
||||||
|
let _tree = super::section(&ui, &state, lang);
|
||||||
|
|
||||||
|
let mut texts: Vec<(Key, String)> = probe::take()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(key, text)| (key, text.to_owned()))
|
||||||
|
.collect();
|
||||||
|
texts.extend(probe::take_rendered());
|
||||||
|
|
||||||
|
// 复审 L-2:`{name}` 换完还留着花括号 ⇒ 某处参数名与模板不一致(`("floor", …)` 写成
|
||||||
|
// `("flor", …)` 时句子会原样显示「下限 {floor}」,而「整句非 ASCII」的守卫看不出来)。
|
||||||
|
// 三档状态合起来覆盖本页全部 `tr()` 调用,所以这条断言是全覆盖的。
|
||||||
|
for (key, text) in &texts {
|
||||||
|
assert!(
|
||||||
|
!text.contains('{'),
|
||||||
|
"{case:?}:{} 渲染后还留着占位符「{text}」——`tr()` 的参数名与模板对不上",
|
||||||
|
key.name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
texts
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 画哪一档状态。
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum Case {
|
||||||
|
/// 工厂态(GPU / auto)。
|
||||||
|
Factory,
|
||||||
|
/// CPU 降级态(面板 α 与玻璃被强制)。
|
||||||
|
Cpu,
|
||||||
|
/// 整窗 α 调低(圆角暂时不生效)。
|
||||||
|
Dim,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 这一页**常驻**的 key(三档状态合起来必须一个不缺地取到)。
|
||||||
|
///
|
||||||
|
/// 为什么是手写清单而不是「`Key::ALL` 里所有 `SettingsAppearance*`」:诊断一族(读盘失败 / 配色回落 /
|
||||||
|
/// 小图平铺)取决于运行时状态,单测里要让它们出现得造磁盘上的坏图;那些由
|
||||||
|
/// `background/tests.rs` 与 `palette.rs` 的单测按 key 逐条钉(见各自的用例注释)。
|
||||||
|
const CARD_KEYS: &[Key] = &[
|
||||||
|
Key::SettingsAppearanceCorner,
|
||||||
|
Key::SettingsAppearanceCornerSystem,
|
||||||
|
Key::SettingsAppearanceCornerSquare,
|
||||||
|
Key::SettingsAppearanceCornerHint,
|
||||||
|
Key::SettingsAppearanceCornerSuspended,
|
||||||
|
Key::SettingsAppearanceWindowOpacity,
|
||||||
|
Key::SettingsAppearanceWindowOpacityHint,
|
||||||
|
Key::SettingsAppearancePanelOpacity,
|
||||||
|
Key::SettingsAppearancePanelOpacityHint,
|
||||||
|
Key::SettingsAppearancePanelOpacityForcedValue,
|
||||||
|
Key::SettingsAppearancePanelOpacityForcedHint,
|
||||||
|
Key::SettingsAppearanceBackgroundImage,
|
||||||
|
Key::SettingsAppearanceOff,
|
||||||
|
Key::SettingsAppearanceOn,
|
||||||
|
Key::SettingsAppearanceImagePath,
|
||||||
|
Key::SettingsAppearancePaletteLevel,
|
||||||
|
Key::SettingsAppearanceBgFill,
|
||||||
|
Key::SettingsAppearanceBgFillCover,
|
||||||
|
Key::SettingsAppearanceBgFillContain,
|
||||||
|
Key::SettingsAppearanceBgFillTile,
|
||||||
|
Key::SettingsAppearanceBgDim,
|
||||||
|
Key::SettingsAppearanceBgDimHint,
|
||||||
|
Key::SettingsAppearancePaletteSource,
|
||||||
|
Key::SettingsAppearancePaletteBackground,
|
||||||
|
Key::SettingsAppearancePaletteWallpaper,
|
||||||
|
Key::SettingsAppearancePalettePreset,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// **M180 守门 ①**:中文界面下,外观卡片里取到的每一条文案都不是纯 ASCII。
|
||||||
|
///
|
||||||
|
/// 抓得住:某条 key 的中文译文被换成英文原句(中文表里出现纯 ASCII 的一句);中文译文写成
|
||||||
|
/// `PNG` / `Preset` 这种专名;`tr()` 那条路径把数字拼进英文模板(比如 `Colours: preset`)。
|
||||||
|
/// 抓不住:硬编码字面量——它不过取词口,由 [`every_appearance_card_key_is_drawn`] 与
|
||||||
|
/// `catalog/settings/appearance.rs` 的译文守卫合力守。
|
||||||
|
///
|
||||||
|
/// 变异证明:把 `SettingsAppearanceOff` 的中文改成 `"Off"` ⇒ 本条立刻红。
|
||||||
|
#[test]
|
||||||
|
fn the_appearance_card_speaks_chinese() {
|
||||||
|
// 白名单:技术专名允许是纯 ASCII(现在一条都没有——`%` / 数字不在文案里,它们是参数)。
|
||||||
|
const ASCII_OK: &[&str] = &[];
|
||||||
|
|
||||||
|
for case in [Case::Factory, Case::Cpu, Case::Dim] {
|
||||||
|
let pairs = appearance_texts(Lang::ZhCn, case);
|
||||||
|
assert!(
|
||||||
|
pairs.len() >= 20,
|
||||||
|
"{case:?}:取词口只记到 {} 条文案,渲染路径没走到底(语言管子断了?)",
|
||||||
|
pairs.len()
|
||||||
|
);
|
||||||
|
for (key, text) in &pairs {
|
||||||
|
if ASCII_OK.contains(&text.as_str()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
!text.is_ascii(),
|
||||||
|
"{case:?}:中文界面下外观卡片里出现了纯 ASCII 的文案「{text}」({})\
|
||||||
|
——写死英文,或中文表里放了英文原句",
|
||||||
|
key.name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **M180 守门 ②(漏项)**:这一页每一处控件都必须从词表取词。
|
||||||
|
///
|
||||||
|
/// 抓得住:把某处 `t(Key::X, lang)` / `tr(lang, Key::X, …)` 改回写死的英文字面量——那条 key 不再
|
||||||
|
/// 出现在取词记录里,集合比较当场红(这正是 M172 口径「22 处硬编码」的复发形态)。
|
||||||
|
/// 抓不住:新加一条写死英文(它没有 key,也就没有「少了哪条」)。
|
||||||
|
///
|
||||||
|
/// 变异证明:把 `bg_image.rs` 里 `t(Key::SettingsAppearanceOff, lang)` 换回 `"Off"` ⇒ 本条红。
|
||||||
|
#[test]
|
||||||
|
fn every_appearance_card_key_is_drawn() {
|
||||||
|
let mut seen: Vec<Key> = Vec::new();
|
||||||
|
for case in [Case::Factory, Case::Cpu, Case::Dim] {
|
||||||
|
seen.extend(
|
||||||
|
appearance_texts(Lang::ZhCn, case)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(key, _)| key),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let missing: Vec<&str> = CARD_KEYS
|
||||||
|
.iter()
|
||||||
|
.filter(|key| !seen.contains(key))
|
||||||
|
.map(|key| key.name())
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
missing.is_empty(),
|
||||||
|
"这些 key 一帧都没取到(控件被改回写死英文,或整段没画):{missing:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 英文界面下不许出现汉字(否则「含汉字」可能来自写死的中文,而不是词表)。
|
||||||
|
#[test]
|
||||||
|
fn the_appearance_card_keeps_its_english_baseline() {
|
||||||
|
for case in [Case::Factory, Case::Cpu, Case::Dim] {
|
||||||
|
let texts = appearance_texts(Lang::En, case);
|
||||||
|
let with_han: Vec<&(Key, String)> = texts
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, text)| text.chars().any(is_han))
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
with_han.is_empty(),
|
||||||
|
"{case:?}:英文界面下外观卡片出现了汉字:{with_han:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 有没有汉字(与 `pages/settings/tests/appearance_section.rs` 的 `is_han` 同一段码位)。
|
||||||
|
fn is_han(ch: char) -> bool {
|
||||||
|
('\u{4e00}'..='\u{9fff}').contains(&ch)
|
||||||
|
}
|
||||||
@@ -29,6 +29,49 @@ pub(super) const ROWS: &[(&str, &str, &str)] = &[
|
|||||||
("SettingsAppearanceCornerSquare", "Square", "直角"),
|
("SettingsAppearanceCornerSquare", "Square", "直角"),
|
||||||
("SettingsAppearanceCornerHint", "On Windows 11 and Server 2025 we ask the system for rounded corners; Windows 10 keeps square corners. On macOS and Linux the desktop's own rules apply. We never draw corners ourselves.", "Windows 11 与 Windows Server 2025 上我们请求系统处理外框圆角;Windows 10 保持直角;macOS 与 Linux 交给桌面合成器自己的规则;我们不自己画圆角。"),
|
("SettingsAppearanceCornerHint", "On Windows 11 and Server 2025 we ask the system for rounded corners; Windows 10 keeps square corners. On macOS and Linux the desktop's own rules apply. We never draw corners ourselves.", "Windows 11 与 Windows Server 2025 上我们请求系统处理外框圆角;Windows 10 保持直角;macOS 与 Linux 交给桌面合成器自己的规则;我们不自己画圆角。"),
|
||||||
("SettingsAppearanceCornerSuspended", "Not in effect right now: window transparency is below 100%. Setting it back to 100% restores the rounded corners.", "当前因整窗透明度不生效:把整窗透明度调回 100% 圆角即恢复。"),
|
("SettingsAppearanceCornerSuspended", "Not in effect right now: window transparency is below 100%. Setting it back to 100% restores the rounded corners.", "当前因整窗透明度不生效:把整窗透明度调回 100% 圆角即恢复。"),
|
||||||
|
// M180:外观分组补齐(`catalog/settings/appearance.rs` 的第二批,M116 圆角那 5 条之后)。
|
||||||
|
// 位置就是装配链里那个域的位置——`settings_appearance_entries` 在 `settings_java_entries` 之前,
|
||||||
|
// 所以这 40 行必须落在 `SettingsAppearanceCornerSuspended` 与 `SettingsJavaScan` 之间
|
||||||
|
// (顺序 = `Key::ALL`;为排到文件尾而重排别人的族会让 `split_keeps_every_translation` 整片红)。
|
||||||
|
("SettingsAppearanceWindowOpacity", "Window transparency", "整窗透明度"),
|
||||||
|
("SettingsAppearanceWindowOpacityHint", "Applies to the whole window. Floor {floor}; 1.00 = fully opaque.", "作用于整个窗口。下限 {floor};1.00 = 完全不透明。"),
|
||||||
|
("SettingsAppearancePanelOpacity", "Panel transparency", "面板透明度"),
|
||||||
|
("SettingsAppearancePanelOpacityHint", "Only the panel/card layer. Floor {floor}; independent of window transparency.", "只作用于面板 / 卡片这一层。下限 {floor};与整窗透明度互不影响。"),
|
||||||
|
("SettingsAppearancePanelOpacityForcedValue", "100% (forced from {requested})", "100%(强制,原请求值 {requested})"),
|
||||||
|
("SettingsAppearancePanelOpacityForcedHint", "Software renderer: panels and glass stay fully opaque (window transparency still works).", "软件渲染:面板与玻璃效果恒为不透明(整窗透明度仍然有效)。"),
|
||||||
|
("SettingsAppearanceBackgroundImage", "Background image", "背景图"),
|
||||||
|
("SettingsAppearanceOff", "Off", "关"),
|
||||||
|
("SettingsAppearanceOn", "On", "开"),
|
||||||
|
("SettingsAppearanceImagePath", "Image path", "图片路径"),
|
||||||
|
("SettingsAppearancePaletteLevel", "Colours: {level}", "配色:{level}"),
|
||||||
|
("SettingsAppearanceBgFill", "Fill", "填充方式"),
|
||||||
|
("SettingsAppearanceBgFillCover", "Cover", "铺满"),
|
||||||
|
("SettingsAppearanceBgFillContain", "Contain", "完整显示"),
|
||||||
|
("SettingsAppearanceBgFillTile", "Tile", "平铺"),
|
||||||
|
("SettingsAppearanceBgDim", "Dim", "暗化"),
|
||||||
|
("SettingsAppearanceBgDimHint", "Blends the image toward the panel colour so text stays readable. Max {max}.", "把背景图往面板底色上退,让文字读得清。上限 {max}。"),
|
||||||
|
("SettingsAppearancePaletteSource", "Colour source", "配色来源"),
|
||||||
|
("SettingsAppearancePaletteBackground", "Background", "取自背景图"),
|
||||||
|
("SettingsAppearancePaletteWallpaper", "Wallpaper", "取自壁纸"),
|
||||||
|
("SettingsAppearancePalettePreset", "Preset", "内置色卡"),
|
||||||
|
("SettingsAppearanceBgReadFailed", "Background image could not be read: {reason}", "背景图读不出来:{reason}"),
|
||||||
|
("SettingsAppearanceBgDecodeFailed", "Background image could not be decoded ({reason}; this build decodes PNG, JPEG and BMP)", "背景图解不开({reason};这个构建只支持 PNG、JPEG 与 BMP)"),
|
||||||
|
("SettingsAppearanceBgTooLarge", "Background image is too large to decode: {width}×{height} (limit {limit} px per edge)", "背景图太大,解不开:{width}×{height}(每边上限 {limit} 像素)"),
|
||||||
|
("SettingsAppearanceBgSmallTile", "Small image ({width}×{height}): tiles are scaled up so the window stays covered (max {max} tiles, no gaps).", "图片很小({width}×{height}):图块会被放大,保证窗口铺满(最多 {max} 块,不留缝)。"),
|
||||||
|
("SettingsAppearancePaletteBgUndecodable", "Background image could not be read; colours fall back.", "背景图读不出来,配色回落到下一级。"),
|
||||||
|
("SettingsAppearancePaletteBgTooSmall", "Background image is too small to sample; colours fall back.", "背景图太小,取不了色,配色回落到下一级。"),
|
||||||
|
("SettingsAppearancePaletteBgBadDimensions", "Background image data looks inconsistent; colours fall back.", "背景图数据前后不一致,配色回落到下一级。"),
|
||||||
|
("SettingsAppearancePaletteBgNoOpaquePixels", "Background image is almost fully transparent; colours fall back.", "背景图几乎全透明,配色回落到下一级。"),
|
||||||
|
("SettingsAppearancePaletteBgLowChroma", "Background image is too monochrome to tint from; colours fall back.", "背景图太单调(彩度不足),取不出配色,回落到下一级。"),
|
||||||
|
("SettingsAppearancePaletteBgContrast", "Background image colours would be unreadable; colours fall back.", "按背景图取的颜色读不清,配色回落到下一级。"),
|
||||||
|
("SettingsAppearancePaletteWallpaperUndecodable", "Desktop wallpaper could not be read.", "桌面壁纸读不出来。"),
|
||||||
|
("SettingsAppearancePaletteWallpaperTooSmall", "Desktop wallpaper is too small to sample.", "桌面壁纸太小,取不了色。"),
|
||||||
|
("SettingsAppearancePaletteWallpaperBadDimensions", "Desktop wallpaper data looks inconsistent.", "桌面壁纸数据前后不一致。"),
|
||||||
|
("SettingsAppearancePaletteWallpaperNoOpaquePixels", "Desktop wallpaper is almost fully transparent.", "桌面壁纸几乎全透明。"),
|
||||||
|
("SettingsAppearancePaletteWallpaperLowChroma", "Desktop wallpaper is too monochrome to tint from.", "桌面壁纸太单调(彩度不足),取不出配色。"),
|
||||||
|
("SettingsAppearancePaletteWallpaperContrast", "Desktop wallpaper colours would be unreadable; colours fall back.", "按桌面壁纸取的颜色读不清,配色回落到下一级。"),
|
||||||
|
("SettingsAppearancePalettePresetContrast", "The built-in card needed a small contrast correction.", "内置色卡做了一点对比度修正。"),
|
||||||
|
("SettingsAppearancePaletteAdjusted", "A few colours were nudged for readability.", "有几个颜色为了可读性做了微调。"),
|
||||||
// M175:设置页「Java」分组(`catalog/settings/java.rs` 的 `settings_java_entries`)。
|
// M175:设置页「Java」分组(`catalog/settings/java.rs` 的 `settings_java_entries`)。
|
||||||
// 位置就是装配链里那个域的位置:`settings_java_entries` 在 `settings_appearance_entries` 之后、
|
// 位置就是装配链里那个域的位置:`settings_java_entries` 在 `settings_appearance_entries` 之后、
|
||||||
// `settings_download_entries`(空)与 `settings_ai_entries`(M174)之前,所以这几行必须落在
|
// `settings_download_entries`(空)与 `settings_ai_entries`(M174)之前,所以这几行必须落在
|
||||||
|
|||||||
@@ -1,12 +1,38 @@
|
|||||||
//! 设置页「Appearance」分组文案(M116 首次落地:窗口圆角一档)。
|
//! 设置页「Appearance」分组文案(M116 落了圆角一族;M180 `I18NAPPEAR` 补齐全组)。
|
||||||
//!
|
//!
|
||||||
//! 加 key 时把条目写进 `$($acc)*` 后面即可(形状见 [`crate::catalog`] 的「域装配」一节)。
|
//! 加 key 时把条目写进 `$($acc)*` 后面即可(形状见 [`crate::catalog`] 的「域装配」一节)。
|
||||||
//!
|
//!
|
||||||
//! ## 为什么只有圆角这一组的 key
|
//! ## 两批 key:圆角(M116)在前,其余(M180)在后
|
||||||
//!
|
//!
|
||||||
//! 本分组的既有控件(整窗/面板透明度、背景图、配色来源……)是 M93 落的,文案是英文基线字面量
|
//! M116 只落了圆角那 5 条;本分组其余控件(整窗/面板透明度、背景图、填充、暗化、配色来源)
|
||||||
//! (`pages/appearance/section.rs`)。M116 新增的圆角档位**从一开始就走词表**(tower 口径:
|
//! 是 M93 落的,文案一直是英文基线字面量。M172 审计量出这一页 27 处文案里 22 处硬编码、中文界面
|
||||||
//! 设置项文案不许硬编码),所以这里先只有它这一组;老控件接词表归 i18n 接线 mission。
|
//! 本地化 0/27。M180 把**控件文案 + 运行时诊断 + 配色回落提示**全部搬进本文件:
|
||||||
|
//!
|
||||||
|
//! | 批次 | 条目 | 来源 |
|
||||||
|
//! |---|---|---|
|
||||||
|
//! | M116 | `Corner` … `CornerSuspended` | `pages/appearance/section/window.rs` 的圆角一段 |
|
||||||
|
//! | M180 | `WindowOpacity` … `PaletteAdjusted` | 卡片各段标签/说明 + `state.rs` / `background.rs` / `palette.rs` 的诊断 prose |
|
||||||
|
//!
|
||||||
|
//! ## 为什么诊断也在这里(而不是留在 `state.rs`)
|
||||||
|
//!
|
||||||
|
//! `pages/appearance/state.rs` 已经 390 行、贴着 400 行硬线,放不下任何一张文案表。所以那一层只
|
||||||
|
//! 给 **key + 已格式化好的参数**(`background::Note`),句子一律在本文件里。
|
||||||
|
//!
|
||||||
|
//! ## 数值参数:模板里只有 `{name}`,格式在 Rust 侧做
|
||||||
|
//!
|
||||||
|
//! [`crate::catalog::Catalog::render`] 只做 `{name}` → 值的字面替换,**不认** `{:.2}` 这类格式说明。
|
||||||
|
//! 所以「位置/尺寸/上限」这些数字由调用方先 `format!("{:.2}")` 好再当参数传进来:
|
||||||
|
//!
|
||||||
|
//! | key | 原字面量 | 现在 |
|
||||||
|
//! |---|---|---|
|
||||||
|
//! | `SettingsAppearancePanelOpacityForcedValue` | `format!("100% (forced from {:.2})")` | `{requested}` = `format!("{:.2}")` |
|
||||||
|
//! | `SettingsAppearanceWindowOpacityHint` | `Floor {:.2}` = `WINDOW_ALPHA_FLOOR` | `{floor}` |
|
||||||
|
//! | `SettingsAppearancePanelOpacityHint` | `Floor 0.40` = `PANEL_ALPHA_FLOOR` | `{floor}` |
|
||||||
|
//! | `SettingsAppearanceBgDimHint` | `Max 0.95` = `BG_DIM_MAX` | `{max}` |
|
||||||
|
//! | `SettingsAppearanceBgTooLarge` | `(limit 8192 px per edge)` = `MAX_SOURCE_EDGE` | `{limit}` |
|
||||||
|
//! | `SettingsAppearanceBgSmallTile` | `(max 64 tiles …)` = `MAX_TILES` | `{max}` |
|
||||||
|
//!
|
||||||
|
//! 常量走占位符而不是写进译文,是为了「改常量不用改两套译文」(M180 任务口径)。
|
||||||
//!
|
//!
|
||||||
//! ## 文案口径(不许暗示所有平台一致)
|
//! ## 文案口径(不许暗示所有平台一致)
|
||||||
//!
|
//!
|
||||||
@@ -28,8 +54,205 @@ macro_rules! settings_appearance_entries {
|
|||||||
"Not in effect right now: window transparency is below 100%. Setting it back to 100% restores the rounded corners.",
|
"Not in effect right now: window transparency is below 100%. Setting it back to 100% restores the rounded corners.",
|
||||||
"当前因整窗透明度不生效:把整窗透明度调回 100% 圆角即恢复。"
|
"当前因整窗透明度不生效:把整窗透明度调回 100% 圆角即恢复。"
|
||||||
),
|
),
|
||||||
|
// ---- M180 `I18NAPPEAR`:卡片内文案(① 整窗 / ③ 面板 / ④~⑧ 背景图 / ⑨ 配色来源)
|
||||||
|
SettingsAppearanceWindowOpacity => ("Window transparency", "整窗透明度"),
|
||||||
|
SettingsAppearanceWindowOpacityHint => (
|
||||||
|
"Applies to the whole window. Floor {floor}; 1.00 = fully opaque.",
|
||||||
|
"作用于整个窗口。下限 {floor};1.00 = 完全不透明。"
|
||||||
|
),
|
||||||
|
SettingsAppearancePanelOpacity => ("Panel transparency", "面板透明度"),
|
||||||
|
SettingsAppearancePanelOpacityHint => (
|
||||||
|
"Only the panel/card layer. Floor {floor}; independent of window transparency.",
|
||||||
|
"只作用于面板 / 卡片这一层。下限 {floor};与整窗透明度互不影响。"
|
||||||
|
),
|
||||||
|
SettingsAppearancePanelOpacityForcedValue => (
|
||||||
|
"100% (forced from {requested})",
|
||||||
|
"100%(强制,原请求值 {requested})"
|
||||||
|
),
|
||||||
|
SettingsAppearancePanelOpacityForcedHint => (
|
||||||
|
"Software renderer: panels and glass stay fully opaque (window transparency still works).",
|
||||||
|
"软件渲染:面板与玻璃效果恒为不透明(整窗透明度仍然有效)。"
|
||||||
|
),
|
||||||
|
SettingsAppearanceBackgroundImage => ("Background image", "背景图"),
|
||||||
|
SettingsAppearanceOff => ("Off", "关"),
|
||||||
|
SettingsAppearanceOn => ("On", "开"),
|
||||||
|
SettingsAppearanceImagePath => ("Image path", "图片路径"),
|
||||||
|
SettingsAppearancePaletteLevel => ("Colours: {level}", "配色:{level}"),
|
||||||
|
SettingsAppearanceBgFill => ("Fill", "填充方式"),
|
||||||
|
SettingsAppearanceBgFillCover => ("Cover", "铺满"),
|
||||||
|
SettingsAppearanceBgFillContain => ("Contain", "完整显示"),
|
||||||
|
SettingsAppearanceBgFillTile => ("Tile", "平铺"),
|
||||||
|
SettingsAppearanceBgDim => ("Dim", "暗化"),
|
||||||
|
SettingsAppearanceBgDimHint => (
|
||||||
|
"Blends the image toward the panel colour so text stays readable. Max {max}.",
|
||||||
|
"把背景图往面板底色上退,让文字读得清。上限 {max}。"
|
||||||
|
),
|
||||||
|
SettingsAppearancePaletteSource => ("Colour source", "配色来源"),
|
||||||
|
SettingsAppearancePaletteBackground => ("Background", "取自背景图"),
|
||||||
|
SettingsAppearancePaletteWallpaper => ("Wallpaper", "取自壁纸"),
|
||||||
|
SettingsAppearancePalettePreset => ("Preset", "内置色卡"),
|
||||||
|
// ---- M180:运行时诊断(`background.rs` 的三种读盘失败 + 小图平铺)。
|
||||||
|
//
|
||||||
|
// M180 r1 复审 P2-1:软件渲染那句**只有一条**,就挂在面板那一行(`PanelOpacityForcedHint`)
|
||||||
|
// 上——它挨着被禁用的滑块,正好回答「为什么拖不动」;这里不再有第二句。
|
||||||
|
SettingsAppearanceBgReadFailed => (
|
||||||
|
"Background image could not be read: {reason}",
|
||||||
|
"背景图读不出来:{reason}"
|
||||||
|
),
|
||||||
|
SettingsAppearanceBgDecodeFailed => (
|
||||||
|
"Background image could not be decoded ({reason}; this build decodes PNG, JPEG and BMP)",
|
||||||
|
"背景图解不开({reason};这个构建只支持 PNG、JPEG 与 BMP)"
|
||||||
|
),
|
||||||
|
SettingsAppearanceBgTooLarge => (
|
||||||
|
"Background image is too large to decode: {width}×{height} (limit {limit} px per edge)",
|
||||||
|
"背景图太大,解不开:{width}×{height}(每边上限 {limit} 像素)"
|
||||||
|
),
|
||||||
|
SettingsAppearanceBgSmallTile => (
|
||||||
|
"Small image ({width}×{height}): tiles are scaled up so the window stays covered (max {max} tiles, no gaps).",
|
||||||
|
"图片很小({width}×{height}):图块会被放大,保证窗口铺满(最多 {max} 块,不留缝)。"
|
||||||
|
),
|
||||||
|
// ---- M180:配色回落提示(`palette.rs` 把 design 的 `hint_key()` 翻成下面这些)
|
||||||
|
SettingsAppearancePaletteBgUndecodable => (
|
||||||
|
"Background image could not be read; colours fall back.",
|
||||||
|
"背景图读不出来,配色回落到下一级。"
|
||||||
|
),
|
||||||
|
SettingsAppearancePaletteBgTooSmall => (
|
||||||
|
"Background image is too small to sample; colours fall back.",
|
||||||
|
"背景图太小,取不了色,配色回落到下一级。"
|
||||||
|
),
|
||||||
|
SettingsAppearancePaletteBgBadDimensions => (
|
||||||
|
"Background image data looks inconsistent; colours fall back.",
|
||||||
|
"背景图数据前后不一致,配色回落到下一级。"
|
||||||
|
),
|
||||||
|
SettingsAppearancePaletteBgNoOpaquePixels => (
|
||||||
|
"Background image is almost fully transparent; colours fall back.",
|
||||||
|
"背景图几乎全透明,配色回落到下一级。"
|
||||||
|
),
|
||||||
|
SettingsAppearancePaletteBgLowChroma => (
|
||||||
|
"Background image is too monochrome to tint from; colours fall back.",
|
||||||
|
"背景图太单调(彩度不足),取不出配色,回落到下一级。"
|
||||||
|
),
|
||||||
|
SettingsAppearancePaletteBgContrast => (
|
||||||
|
"Background image colours would be unreadable; colours fall back.",
|
||||||
|
"按背景图取的颜色读不清,配色回落到下一级。"
|
||||||
|
),
|
||||||
|
SettingsAppearancePaletteWallpaperUndecodable => (
|
||||||
|
"Desktop wallpaper could not be read.",
|
||||||
|
"桌面壁纸读不出来。"
|
||||||
|
),
|
||||||
|
SettingsAppearancePaletteWallpaperTooSmall => (
|
||||||
|
"Desktop wallpaper is too small to sample.",
|
||||||
|
"桌面壁纸太小,取不了色。"
|
||||||
|
),
|
||||||
|
SettingsAppearancePaletteWallpaperBadDimensions => (
|
||||||
|
"Desktop wallpaper data looks inconsistent.",
|
||||||
|
"桌面壁纸数据前后不一致。"
|
||||||
|
),
|
||||||
|
SettingsAppearancePaletteWallpaperNoOpaquePixels => (
|
||||||
|
"Desktop wallpaper is almost fully transparent.",
|
||||||
|
"桌面壁纸几乎全透明。"
|
||||||
|
),
|
||||||
|
SettingsAppearancePaletteWallpaperLowChroma => (
|
||||||
|
"Desktop wallpaper is too monochrome to tint from.",
|
||||||
|
"桌面壁纸太单调(彩度不足),取不出配色。"
|
||||||
|
),
|
||||||
|
SettingsAppearancePaletteWallpaperContrast => (
|
||||||
|
"Desktop wallpaper colours would be unreadable; colours fall back.",
|
||||||
|
"按桌面壁纸取的颜色读不清,配色回落到下一级。"
|
||||||
|
),
|
||||||
|
SettingsAppearancePalettePresetContrast => (
|
||||||
|
"The built-in card needed a small contrast correction.",
|
||||||
|
"内置色卡做了一点对比度修正。"
|
||||||
|
),
|
||||||
|
SettingsAppearancePaletteAdjusted => (
|
||||||
|
"A few colours were nudged for readability.",
|
||||||
|
"有几个颜色为了可读性做了微调。"
|
||||||
|
),
|
||||||
} }
|
} }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) use settings_appearance_entries;
|
pub(crate) use settings_appearance_entries;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::catalog::{placeholders, Catalog, Key};
|
||||||
|
use crate::Lang;
|
||||||
|
|
||||||
|
/// 本域的 key(`Key::ALL` 里按名字前缀筛;不手工列表,漏一条就少一条守卫)。
|
||||||
|
fn appearance_keys() -> Vec<Key> {
|
||||||
|
Key::ALL
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|key| key.name().starts_with("SettingsAppearance"))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 有没有汉字(与 `pages/settings/tests/appearance_section.rs` 的 `is_han` 同一段码位)。
|
||||||
|
fn is_han(ch: char) -> bool {
|
||||||
|
('\u{4e00}'..='\u{9fff}').contains(&ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **M180 守门 ②**:外观分组的每条 key 都必须真翻过——英文照抄进中文表就地判红。
|
||||||
|
///
|
||||||
|
/// 抓得住:① 新加 key 时中文栏直接抄英文(`chinese_catalog_is_actually_chinese` 也抓这条,
|
||||||
|
/// 但那条在 `catalog/tests.rs`、不属于本 mission 的 scope,而且不查「中文字里有没有汉字」);
|
||||||
|
/// ② 中文译文变成纯 ASCII(比如只写了 `PNG` / `Preset` 这种专名);③ 占位符集合两边不一致
|
||||||
|
/// (`{floor}` 只在一边有 ⇒ `render()` 吐出半截句子)。
|
||||||
|
/// 抓不住:译文翻得对不对(语义只能人审)。
|
||||||
|
///
|
||||||
|
/// 变异证明:把任一条的第二个字符串换成第一个(`("Cover", "Cover")`)⇒ 本条立刻红(`assert_ne!`
|
||||||
|
/// 与 `is_han` 命中同一行)。
|
||||||
|
#[test]
|
||||||
|
fn every_appearance_key_is_translated_into_chinese() {
|
||||||
|
let keys = appearance_keys();
|
||||||
|
assert!(
|
||||||
|
keys.len() >= 40,
|
||||||
|
"外观分组至少 40 条 key(M180 落地时是 45 条),实际 {}:\
|
||||||
|
key 被搬走或前缀改名了?",
|
||||||
|
keys.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
let en = Catalog::of(Lang::En);
|
||||||
|
let zh = Catalog::of(Lang::ZhCn);
|
||||||
|
for key in keys {
|
||||||
|
let (english, chinese) = (en.text(key), zh.text(key));
|
||||||
|
assert_ne!(english, chinese, "{} 的中文是英文照抄", key.name());
|
||||||
|
assert!(
|
||||||
|
chinese.chars().any(is_han),
|
||||||
|
"{} 的中文里一个汉字都没有:{chinese}",
|
||||||
|
key.name()
|
||||||
|
);
|
||||||
|
let mut left = placeholders(english);
|
||||||
|
let mut right = placeholders(chinese);
|
||||||
|
left.sort_unstable();
|
||||||
|
right.sort_unstable();
|
||||||
|
assert_eq!(left, right, "{} 的占位符两套译文不一致", key.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 数值参数必须走占位符,不许把常量写进译文(改常量时不该还要改两套译文)。
|
||||||
|
#[test]
|
||||||
|
fn numeric_limits_are_placeholders_not_text() {
|
||||||
|
let en = Catalog::of(Lang::En);
|
||||||
|
for (key, needle, placeholder) in [
|
||||||
|
(Key::SettingsAppearanceWindowOpacityHint, "0.08", "floor"),
|
||||||
|
(Key::SettingsAppearancePanelOpacityHint, "0.40", "floor"),
|
||||||
|
(Key::SettingsAppearanceBgDimHint, "0.95", "max"),
|
||||||
|
(Key::SettingsAppearanceBgTooLarge, "8192", "limit"),
|
||||||
|
(Key::SettingsAppearanceBgSmallTile, "64", "max"),
|
||||||
|
] {
|
||||||
|
let text = en.text(key);
|
||||||
|
assert!(
|
||||||
|
!text.contains(needle),
|
||||||
|
"{} 把常量 {needle} 写死在译文里了(应当走 {{{placeholder}}})",
|
||||||
|
key.name()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
placeholders(text).contains(&placeholder),
|
||||||
|
"{} 缺 {{{placeholder}}} 占位符:{text}",
|
||||||
|
key.name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user