Merge branch 'feat/m250-accentwire-apply-custom-accent-colo'
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
5487f661d9
18 files changed
+1006
-41
No files matched your search
@@ -83,7 +83,7 @@ impl App {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 其余键:走 bridge(它知道怎么保留强调色这类本 mission 不管的字段)。
|
// 其余键:走 bridge(M250 起强调色也在这条路上,取的是上一次合法值)。
|
||||||
Some(_) => self.persist_appearance(),
|
Some(_) => self.persist_appearance(),
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,7 @@ impl App {
|
|||||||
self.apply_window_corner("reload");
|
self.apply_window_corner("reload");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 把外观写回 `launcher.json`(只改本 mission 管的字段,强调色/玻璃原样保留)。
|
/// 把外观写回 `launcher.json`(本页控件管的那批字段 + 强调色;其余键原样保留)。
|
||||||
fn persist_appearance(&mut self) {
|
fn persist_appearance(&mut self) {
|
||||||
let mut prefs = self.appearance_prefs.clone();
|
let mut prefs = self.appearance_prefs.clone();
|
||||||
prefs.bg_image_enabled = self.appearance.bg_enabled;
|
prefs.bg_image_enabled = self.appearance.bg_enabled;
|
||||||
@@ -119,6 +119,8 @@ impl App {
|
|||||||
prefs.bg_dim = self.appearance.bg_dim;
|
prefs.bg_dim = self.appearance.bg_dim;
|
||||||
prefs.panel_alpha = self.appearance.panel_alpha;
|
prefs.panel_alpha = self.appearance.panel_alpha;
|
||||||
prefs.glass = self.appearance.glass;
|
prefs.glass = self.appearance.glass;
|
||||||
|
// M250:强调色也是「UI 写入字段」了——取**上一次合法值**(非法手输不写盘,见 `accent::Accent`)。
|
||||||
|
prefs.accent_color = self.appearance.accent.persist_value().to_owned();
|
||||||
|
|
||||||
self.bridge.persist(&prefs, self.theme_mode);
|
self.bridge.persist(&prefs, self.theme_mode);
|
||||||
self.appearance_prefs = prefs;
|
self.appearance_prefs = prefs;
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ pub struct App {
|
|||||||
/// 外观运行期状态(M93):面板 α / 背景图 / 配色来源 / 诊断。
|
/// 外观运行期状态(M93):面板 α / 背景图 / 配色来源 / 诊断。
|
||||||
pub(super) appearance: pages::appearance::State,
|
pub(super) appearance: pages::appearance::State,
|
||||||
/// 设置文件里那 8 个「UI 写入」外观字段的最近快照(写回时只改我们真改了的字段,
|
/// 设置文件里那 8 个「UI 写入」外观字段的最近快照(写回时只改我们真改了的字段,
|
||||||
/// 强调色这类本 mission 不管的字段原样保留)。
|
/// 没动过的字段原样保留;M250 起强调色也在「真改了」的那一列里)。
|
||||||
pub(super) appearance_prefs: unlucky_presenter::prefs::AppearancePrefs,
|
pub(super) appearance_prefs: unlucky_presenter::prefs::AppearancePrefs,
|
||||||
/// 窗口句柄(`Message::WindowHandle` 之后才有;非 Windows 平台恒 `None`)。
|
/// 窗口句柄(`Message::WindowHandle` 之后才有;非 Windows 平台恒 `None`)。
|
||||||
pub(super) hwnd: Option<isize>,
|
pub(super) hwnd: Option<isize>,
|
||||||
|
|||||||
@@ -40,7 +40,11 @@ impl App {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 按「配色来源是否取色成功」重建 [`Ui`]。
|
/// 按「强调色 / 配色来源是否接管」重建 [`Ui`]。
|
||||||
|
///
|
||||||
|
/// **优先级(M250):显式强调色 > D38 取色方案 > 出厂品牌板**。手填的色是直接意图,取色是从
|
||||||
|
/// 图片派生的,所以强调色只在用户真的填了合法值时压过取色——两条路都只决定「颜色来自哪里」,
|
||||||
|
/// `flavor`(明暗 / 跟随系统)仍由 `self.appearance.flavor()` 决定。
|
||||||
///
|
///
|
||||||
/// 色卡档(③)刻意继续走 `Scheme::baseline`:`resolve_source` 的色卡档返回的是
|
/// 色卡档(③)刻意继续走 `Scheme::baseline`:`resolve_source` 的色卡档返回的是
|
||||||
/// `DEFAULT_CARD`(天蓝卡)方案,与令牌 baseline 逐色不同,应用它会把**全部既有验收图的
|
/// `DEFAULT_CARD`(天蓝卡)方案,与令牌 baseline 逐色不同,应用它会把**全部既有验收图的
|
||||||
@@ -49,17 +53,37 @@ impl App {
|
|||||||
// 面板 α(D36)在这里注入 `Ui`:全仓所有表面色都从 `Ui::layer` 出去,所以这是**唯一**
|
// 面板 α(D36)在这里注入 `Ui`:全仓所有表面色都从 `Ui::layer` 出去,所以这是**唯一**
|
||||||
// 生效点(外壳 / 三页卡片 / 外观分组卡片 / 空态图标块自动同源,调用点没有机会各处自乘)。
|
// 生效点(外壳 / 三页卡片 / 外观分组卡片 / 空态图标块自动同源,调用点没有机会各处自乘)。
|
||||||
let panel_alpha = self.appearance.effective_panel_alpha();
|
let panel_alpha = self.appearance.effective_panel_alpha();
|
||||||
self.ui = match self.appearance.derived_scheme() {
|
// 三选一规则只在 `theme::tokens` 里写一遍:出图 harness 也调它(两头各抄一份迟早
|
||||||
Some(scheme) => {
|
// 出现「图上有强调色、产品里没有」,而那种偏差在图上看不出来)。
|
||||||
|
let (ui, colors) = crate::theme::tokens(
|
||||||
|
self.appearance.flavor(),
|
||||||
|
self.appearance.accent.color(),
|
||||||
|
self.appearance.derived_scheme().copied(),
|
||||||
|
self.families,
|
||||||
|
);
|
||||||
|
match colors {
|
||||||
|
crate::theme::Colors::Accent => {
|
||||||
|
// M250:一行里同时给「强调色生效了」与「取色方案被谁压住」,两件事都能读出来。
|
||||||
|
diag!(
|
||||||
|
"[appearance] accent={} → Ui::with_accent(只换 primary/secondary/tertiary;取色方案 palette={} 被压住)",
|
||||||
|
self.appearance.accent.log_token(),
|
||||||
|
self.appearance.palette_level()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
crate::theme::Colors::Scheme => {
|
||||||
diag!(
|
diag!(
|
||||||
"[appearance] palette={} → Ui::from_scheme(取色方案接管 49 个角色)",
|
"[appearance] palette={} → Ui::from_scheme(取色方案接管 49 个角色)",
|
||||||
self.appearance.palette_level()
|
self.appearance.palette_level()
|
||||||
);
|
);
|
||||||
Ui::from_scheme(*scheme, self.families)
|
|
||||||
}
|
}
|
||||||
None => Ui::new(self.appearance.flavor(), self.families),
|
crate::theme::Colors::Board => {
|
||||||
|
// 非法值:**保持出厂板**,但错行留在设置页(不静默),日志里也留一条。
|
||||||
|
if let Some(error) = self.appearance.accent.error() {
|
||||||
|
diag!("[appearance] accent 非法({error:?})→ 出厂板,错行见设置页");
|
||||||
}
|
}
|
||||||
.with_panel_alpha(panel_alpha);
|
}
|
||||||
|
}
|
||||||
|
self.ui = ui.with_panel_alpha(panel_alpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 按设置文件里的主题模式切一次外观;`--theme` 显式给了就不动。
|
/// 按设置文件里的主题模式切一次外观;`--theme` 显式给了就不动。
|
||||||
@@ -151,3 +175,58 @@ impl App {
|
|||||||
&self.ui
|
&self.ui
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::super::App;
|
||||||
|
use crate::app::components::feedback::progress;
|
||||||
|
use crate::cli::Options;
|
||||||
|
use crate::pages::appearance::Change;
|
||||||
|
use crate::theme::Families;
|
||||||
|
use unlucky_design::color::{Color, SchemeFlavor};
|
||||||
|
use unlucky_presenter::mock::MockBackend;
|
||||||
|
use unlucky_presenter::settings_bridge::SettingsBridge;
|
||||||
|
|
||||||
|
const FAMILIES: Families = Families {
|
||||||
|
text: "Segoe UI",
|
||||||
|
display: "Segoe UI",
|
||||||
|
mono: "Consolas",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 强调色接线(M250 ACCENTWIRE)的 App 级判据:`rebuild_ui` 真的换了令牌。
|
||||||
|
///
|
||||||
|
/// **这条是判据一的变异探针**:把 [`App::rebuild_ui`] 改回「恒走 `Ui::new` 出厂板」
|
||||||
|
/// (或在它前面短路掉 `accent.color()` 分支),下面 `assert_ne!` 当场红——`pages/appearance`
|
||||||
|
/// 那条用例只证明「`Ui::with_accent` 能给出不同的色」,抓不到「谁去调它」。
|
||||||
|
///
|
||||||
|
/// 抓得住:接线不存在 / 被短路 / 强调色解析结果没进 `Ui`。
|
||||||
|
/// 抓不住:像素(样式闭包被别处覆盖)——那是出图的事。
|
||||||
|
#[test]
|
||||||
|
fn rebuild_ui_follows_the_configured_accent() {
|
||||||
|
let mut app = App::with_presenter(
|
||||||
|
Options::default(),
|
||||||
|
FAMILIES,
|
||||||
|
SchemeFlavor::Light,
|
||||||
|
Box::new(MockBackend::new()),
|
||||||
|
SettingsBridge::memory_only(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 出厂态:同一组件(进度指示条 = `Role::Primary`)取到的值
|
||||||
|
let (_, factory_primary) = progress::linear_colors(app.ui());
|
||||||
|
assert_eq!(app.ui().name(), "ull-silver");
|
||||||
|
|
||||||
|
// 设置页填色 → `on_appearance` 会 rebuild 一次(与真机路径同一个函数)
|
||||||
|
app.on_appearance(Change::Accent(Color::rgb(0x00, 0x6A, 0x5C).to_hex()));
|
||||||
|
let (_, accent_primary) = progress::linear_colors(app.ui());
|
||||||
|
|
||||||
|
assert_ne!(
|
||||||
|
factory_primary, accent_primary,
|
||||||
|
"填了自定义强调色,`rebuild_ui` 却还给出出厂板的 Primary ⇒ 接线没落地"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
app.ui().flavor(),
|
||||||
|
SchemeFlavor::Light,
|
||||||
|
"强调色只换颜色来源,不改明暗"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
//! 强调色的**运行期状态**(M250 ACCENTWIRE):输入原文 → 解析结果 → 生效色。
|
||||||
|
//!
|
||||||
|
//! 这一层存在的理由:`appearance.accent_color` 此前是死线——键、输入框、presenter 往返都在,
|
||||||
|
//! 但造 tokens 的唯一入口恒走 `DesignTokens::brand(board)`,用户填了色界面没变化。接线要落地
|
||||||
|
//! 的不只是「把 hex 喂进 `Scheme::from_accent`」,还有**失败要可见**:
|
||||||
|
//!
|
||||||
|
//! | 输入 | 生效配色 | `error()` | 落盘值 |
|
||||||
|
//! |---|---|---|---|
|
||||||
|
//! | 空串 | 出厂品牌板(`DesignTokens::brand`) | `None` | `""` |
|
||||||
|
//! | `006A5C` | `DesignTokens::with_accent`(只换三族) | `None` | `#` + `006A5C` |
|
||||||
|
//! | `zzzzzz` / 超长串 | **保持上一次合法的值**(不静默回退成出厂色) | `Some(..)` | 上一次合法的值 |
|
||||||
|
//! | `#12`(还没打完) | 同上(还没生效) | `None`,`pending` 里挂着 | 上一次合法的值 |
|
||||||
|
//!
|
||||||
|
//! 「保持上一次合法值」而不是「回退出厂板」是刻意的:回退会让「填错」和「填对但没生效」在屏幕上
|
||||||
|
//! 长得一模一样,用户唯一的线索就只剩那条错行——而那正是这里要保证不被丢掉的。
|
||||||
|
//!
|
||||||
|
//! ## 何时才算「错」(M250 r1 p2-3)
|
||||||
|
//!
|
||||||
|
//! 受控输入框**每敲一个字符**都会调 [`Accent::set`]。手打 `006A5C` 的过程里,前 5 个字符都是
|
||||||
|
//! `Short`,若立刻亮红就会闪 5 次——用户没打错,只是没打完。所以只有两种情形立刻报错:
|
||||||
|
//!
|
||||||
|
//! 1. **不可能再靠打字变合法**:出现非十六进制字符(`Digit`)或超过 6 位(`Long`)——任何继续
|
||||||
|
//! 输入都只会更错,立刻亮;
|
||||||
|
//! 2. **提交**([`Accent::commit`]):离开这一格、点到别的外观控件、或配置读回。
|
||||||
|
//!
|
||||||
|
//! 配置读回走 [`Accent::new`](`commit` 语义):文件里存着坏值必须**当场**看得见,不能等用户来点。
|
||||||
|
//!
|
||||||
|
//! 解析本身不在这里:`AccentChoice::parse` 是唯一真相(`unlucky_presenter::prefs`),本文件只负责
|
||||||
|
//! 把结果接到色值与 UI 上。
|
||||||
|
|
||||||
|
use unlucky_design::color::Color;
|
||||||
|
use unlucky_presenter::prefs::{AccentChoice, AccentError};
|
||||||
|
|
||||||
|
/// 出图矩阵的强调色开关(`ULL_APPEARANCE_ACCENT=#RRGGBB`;产品路径读 `launcher.json`)。
|
||||||
|
///
|
||||||
|
/// 放在本文件而不是 `state.rs` 的 `ENV_*` 那一族:`state.rs` 正好 400 行(硬线),这一条
|
||||||
|
/// 属于强调色自己,跟 `Accent` 放一起也更好找。语义与其它 `ULL_APPEARANCE_*` 一致——只影响这次进程。
|
||||||
|
pub const ENV_ACCENT: &str = "ULL_APPEARANCE_ACCENT";
|
||||||
|
|
||||||
|
/// 强调色的运行期状态(`App` 经 `pages::appearance::State` 持有一份)。
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Accent {
|
||||||
|
/// 输入框的原文(受控控件要它;非法输入也必须原样留着,否则用户看不见自己错在哪)。
|
||||||
|
text: String,
|
||||||
|
/// 上一次**合法**的解析结果(非法输入不改它 —— 生效色因此不会跳回出厂板)。
|
||||||
|
choice: AccentChoice,
|
||||||
|
/// 解析出来的强调色(`None` = 出厂板,强调色由品牌色板决定)。
|
||||||
|
color: Option<Color>,
|
||||||
|
/// **现在要显示**的结构化原因(`None` = 不显示)。
|
||||||
|
error: Option<AccentError>,
|
||||||
|
/// 已经打错、但「还可能被打字修好」的原因(输入中暂不显示,[`Accent::commit`] 才亮出来)。
|
||||||
|
pending: Option<AccentError>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Accent {
|
||||||
|
/// 从配置读一份(启动时调一次)。配置里存着非法值时**当场**亮错行,不静默吞掉。
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(text: &str) -> Self {
|
||||||
|
let mut accent = Self {
|
||||||
|
text: text.to_owned(),
|
||||||
|
choice: AccentChoice::Factory,
|
||||||
|
color: None,
|
||||||
|
error: None,
|
||||||
|
pending: None,
|
||||||
|
};
|
||||||
|
accent.retry(true);
|
||||||
|
accent
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置页改了一次输入(受控输入框:每敲一个字符都走这里)。
|
||||||
|
///
|
||||||
|
/// 「短但还可能是合法前缀」的输入只挂进 `pending`,不亮红(见模块文档 §何时才算「错」)。
|
||||||
|
pub fn set(&mut self, text: &str) {
|
||||||
|
self.text = text.to_owned();
|
||||||
|
self.retry(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 配置被外部改写后重置(热切:CLI / AI 改完 1 秒内生效)。
|
||||||
|
pub fn reset(&mut self, text: &str) {
|
||||||
|
*self = Self::new(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 提交:把「输入中暂不报」的错亮出来。离开这一格就该走它(见 [`Accent::pending`])。
|
||||||
|
pub fn commit(&mut self) {
|
||||||
|
if self.error.is_none() {
|
||||||
|
self.error = self.pending.take();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 输入框原文。
|
||||||
|
#[must_use]
|
||||||
|
pub fn text(&self) -> &str {
|
||||||
|
&self.text
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 要显示的结构化失败原因(`None` = 合法,或还没打完)。
|
||||||
|
#[must_use]
|
||||||
|
pub fn error(&self) -> Option<AccentError> {
|
||||||
|
self.error
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析出来的强调色(`None` = 出厂板;`rebuild_ui` 与出图判据读它)。
|
||||||
|
#[must_use]
|
||||||
|
pub fn color(&self) -> Option<Color> {
|
||||||
|
self.color
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 落盘值:`""` = 出厂,`#RRGGBB` = 自定义。**永远是上一次合法值**——非法手输不写盘。
|
||||||
|
#[must_use]
|
||||||
|
pub fn persist_value(&self) -> &str {
|
||||||
|
self.choice.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 取证日志里的一个词:`factory` / 规范化的 hex / `invalid(..)` / `pending(..)`。
|
||||||
|
///
|
||||||
|
/// 为什么日志要带这个:截图与真机复现只能靠一行文字分辨「强调色没生效」与「强调色生效了但
|
||||||
|
/// 图上看不出来」——后者是这条 mission 的验收判据,前者是它的 bug。
|
||||||
|
#[must_use]
|
||||||
|
pub fn log_token(&self) -> String {
|
||||||
|
if let Some(error) = self.error {
|
||||||
|
return format!("invalid({error:?})");
|
||||||
|
}
|
||||||
|
if let Some(error) = self.pending {
|
||||||
|
return format!("pending({error:?})");
|
||||||
|
}
|
||||||
|
if self.choice.as_str().is_empty() {
|
||||||
|
"factory".to_owned()
|
||||||
|
} else {
|
||||||
|
self.persist_value().to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 按当前 `text` 重算:合法 ⇒ 换色 + 清错;非法 ⇒ **色不动**,按 `show` 决定报不报。
|
||||||
|
fn retry(&mut self, show: bool) {
|
||||||
|
match AccentChoice::parse(&self.text) {
|
||||||
|
Ok(choice) => {
|
||||||
|
self.color = Color::from_hex(choice.as_str());
|
||||||
|
self.choice = choice;
|
||||||
|
self.error = None;
|
||||||
|
self.pending = None;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
// 只有「位数不足」还有机会靠继续打字补成合法;非 hex 字符与超 6 位已经回不了头。
|
||||||
|
let hopeless = !matches!(error, AccentError::Short { .. });
|
||||||
|
if show || hopeless {
|
||||||
|
self.error = Some(error);
|
||||||
|
self.pending = None;
|
||||||
|
} else {
|
||||||
|
self.pending = Some(error);
|
||||||
|
self.error = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,6 +60,7 @@ use crate::pages::frame;
|
|||||||
use crate::pages::Message;
|
use crate::pages::Message;
|
||||||
use crate::theme::Ui;
|
use crate::theme::Ui;
|
||||||
|
|
||||||
|
pub mod accent;
|
||||||
pub mod alpha;
|
pub mod alpha;
|
||||||
pub mod background;
|
pub mod background;
|
||||||
|
|
||||||
@@ -129,6 +130,8 @@ pub enum Change {
|
|||||||
PaletteSource(unlucky_presenter::prefs::PaletteSource),
|
PaletteSource(unlucky_presenter::prefs::PaletteSource),
|
||||||
/// 窗口圆角档位(M116:跟随系统 / 直角)。
|
/// 窗口圆角档位(M116:跟随系统 / 直角)。
|
||||||
Corner(unlucky_presenter::prefs::WindowCorner),
|
Corner(unlucky_presenter::prefs::WindowCorner),
|
||||||
|
/// 自定义强调色输入(M250:原文;空串 = 恢复出厂板)。
|
||||||
|
Accent(String),
|
||||||
/// 设置页切分组(左列点选)。
|
/// 设置页切分组(左列点选)。
|
||||||
SelectGroup(usize),
|
SelectGroup(usize),
|
||||||
}
|
}
|
||||||
@@ -146,6 +149,7 @@ impl Change {
|
|||||||
Self::BgDim(_) => unlucky_presenter::prefs::keys::BG_DIM,
|
Self::BgDim(_) => unlucky_presenter::prefs::keys::BG_DIM,
|
||||||
Self::PaletteSource(_) => unlucky_presenter::prefs::keys::PALETTE_SOURCE,
|
Self::PaletteSource(_) => unlucky_presenter::prefs::keys::PALETTE_SOURCE,
|
||||||
Self::Corner(_) => unlucky_presenter::prefs::keys::WINDOW_CORNER,
|
Self::Corner(_) => unlucky_presenter::prefs::keys::WINDOW_CORNER,
|
||||||
|
Self::Accent(_) => unlucky_presenter::prefs::keys::ACCENT_COLOR,
|
||||||
Self::SelectGroup(_) => return None,
|
Self::SelectGroup(_) => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -153,6 +157,10 @@ impl Change {
|
|||||||
/// 这条改动的落盘字符串形态(与 `schema` 的取值词表逐字一致)。
|
/// 这条改动的落盘字符串形态(与 `schema` 的取值词表逐字一致)。
|
||||||
///
|
///
|
||||||
/// `None` = 不落盘(分组选择)。数字一律给 `f64` 的十进制字面量(`store.set` 按 schema 类型转)。
|
/// `None` = 不落盘(分组选择)。数字一律给 `f64` 的十进制字面量(`store.set` 按 schema 类型转)。
|
||||||
|
///
|
||||||
|
/// ⚠️ `Accent` 这一格的字符串是**用户原文**:真正落盘的值来自
|
||||||
|
/// `State::current_values()` 的 `accent_color`(上一次**合法**值,见 `accent::Accent`),
|
||||||
|
/// 因为非法手输不许写进配置。这里保留原文只为「每条改动都点名到自己的键」这条不变量。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn value(&self) -> Option<String> {
|
pub fn value(&self) -> Option<String> {
|
||||||
Some(match self {
|
Some(match self {
|
||||||
@@ -164,6 +172,7 @@ impl Change {
|
|||||||
Self::BgDim(dim) => format!("{dim}"),
|
Self::BgDim(dim) => format!("{dim}"),
|
||||||
Self::PaletteSource(source) => source.as_str().to_owned(),
|
Self::PaletteSource(source) => source.as_str().to_owned(),
|
||||||
Self::Corner(corner) => corner.as_str().to_owned(),
|
Self::Corner(corner) => corner.as_str().to_owned(),
|
||||||
|
Self::Accent(text) => text.clone(),
|
||||||
Self::SelectGroup(_) => return None,
|
Self::SelectGroup(_) => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
//! 外观分组第 ⓪ 段(视觉上排在最前):**自定义强调色**(M250 ACCENTWIRE)。
|
||||||
|
//!
|
||||||
|
//! 为什么这一段值得单开:`appearance.accent_color` 从 M41 起就有键、有 presenter 往返、
|
||||||
|
//! 有 `ACCENT_PRESETS` 色板,却**没有任何 UI 写者**——造 tokens 的唯一入口恒走
|
||||||
|
//! `DesignTokens::brand(board)`,用户即使改了配置文件,界面也不会变。接线之后这一段就是那条
|
||||||
|
//! 链路的手动入口,也是**非法值唯一能被看见的地方**(错行紧贴输入框,不折叠、不写进日志了事)。
|
||||||
|
//!
|
||||||
|
//! ## 三件控件对应的键与消息
|
||||||
|
//!
|
||||||
|
//! | 控件 | 键 | 消息 |
|
||||||
|
//! |---|---|---|
|
||||||
|
//! | 强调色文本框(受控,输入即生效) | `appearance.accent_color` | [`Change::Accent`] |
|
||||||
|
//! | 「出厂」+ 8 个预设色块 | 同上(写入色板里的 hex) | [`Change::Accent`] |
|
||||||
|
//! | 非法值错行 | —(不进配置) | 无 |
|
||||||
|
//!
|
||||||
|
//! ## 色块为什么画成圆点而不是药丸
|
||||||
|
//!
|
||||||
|
//! 9 颗写着十六进制串的药丸会变成一排噪声,而且色相要靠读字获取——色板控件的可见内容**就是**
|
||||||
|
//! 颜色本身。所以色块 = `ShapeScale::Full` 的实心圆 + 描边;选中态加粗描边(不靠色相区分:
|
||||||
|
//! 色块底下垫的是它自己的颜色,色相对比不可靠)。
|
||||||
|
|
||||||
|
use iced::widget::tooltip::{self, Tooltip};
|
||||||
|
use iced::widget::{button, column, container, row, text, Space as Gap};
|
||||||
|
use iced::{Background, Border, Element, Length, Padding, Shadow, Theme as IcedTheme};
|
||||||
|
|
||||||
|
use unlucky_design::color::Role;
|
||||||
|
use unlucky_design::shape::ShapeScale;
|
||||||
|
use unlucky_design::spacing::Space;
|
||||||
|
use unlucky_i18n::catalog::Key;
|
||||||
|
use unlucky_i18n::Lang;
|
||||||
|
use unlucky_presenter::prefs::{AccentError, ACCENT_PRESETS};
|
||||||
|
|
||||||
|
use crate::app::components::inputs::{text_field, Field};
|
||||||
|
use crate::pages::Message;
|
||||||
|
use crate::theme::{to_iced, Ui, STROKE_PX};
|
||||||
|
|
||||||
|
use super::super::accent::Accent;
|
||||||
|
use super::super::state::State;
|
||||||
|
use super::super::Change;
|
||||||
|
use super::{choice, label, note_line, t, tr};
|
||||||
|
|
||||||
|
/// 把 ⓪ 段推进 `rows`。
|
||||||
|
pub(super) fn push<'a>(
|
||||||
|
ui: &'a Ui,
|
||||||
|
state: &'a State,
|
||||||
|
lang: Lang,
|
||||||
|
rows: &mut Vec<Element<'a, Message>>,
|
||||||
|
) {
|
||||||
|
let accent = &state.accent;
|
||||||
|
|
||||||
|
// 受控输入框 + 失败态:`Field::error()` 让框本身变成错误色,错行再补「错在哪」。
|
||||||
|
let mut field = Field::new(accent.text())
|
||||||
|
.label(t(Key::SettingsAppearanceAccent, lang))
|
||||||
|
.placeholder("#RRGGBB");
|
||||||
|
if accent.error().is_some() {
|
||||||
|
field = field.error();
|
||||||
|
}
|
||||||
|
let input: Element<'a, Message> = text_field(ui, field, |value| {
|
||||||
|
Message::Appearance(Change::Accent(value))
|
||||||
|
});
|
||||||
|
rows.push(
|
||||||
|
column![label(ui, t(Key::SettingsAppearanceAccent, lang)), input]
|
||||||
|
.spacing(ui.space(Space::X1))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
rows.push(presets(ui, accent, lang));
|
||||||
|
|
||||||
|
// 非法值:**结构化的原因**,不是「格式不对」这种没法行动的一句话。
|
||||||
|
if let Some(error) = accent.error() {
|
||||||
|
let text = reason(lang, error);
|
||||||
|
rows.push(note_line(ui, &text, ui.color(Role::Error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 说明:空 = 出厂板;自定义色只换三族(中性表面与 error 保持基线是 design 既定语义)。
|
||||||
|
rows.push(note_line(
|
||||||
|
ui,
|
||||||
|
t(Key::SettingsAppearanceAccentHint, lang),
|
||||||
|
ui.color(Role::OnSurfaceVariant),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 「预设」两行:先「标签 + 出厂」,再一排 8 颗色块。
|
||||||
|
///
|
||||||
|
/// 为什么分两行:一行放 9 个控件在设置卡片的正文宽度里会被挤成「7 + 1」折行,最后一颗色块
|
||||||
|
/// 孤零零掉到下一行(出图实测)。分两行是**有意**的排版,不是被挤出来的。
|
||||||
|
fn presets<'a>(ui: &'a Ui, accent: &'a Accent, lang: Lang) -> Element<'a, Message> {
|
||||||
|
let current = accent.persist_value();
|
||||||
|
let head = row![
|
||||||
|
container(label(ui, t(Key::SettingsAppearanceAccentPresets, lang)))
|
||||||
|
.align_y(iced::Alignment::Center),
|
||||||
|
choice(
|
||||||
|
ui,
|
||||||
|
t(Key::SettingsAppearanceAccentFactory, lang),
|
||||||
|
current.is_empty(),
|
||||||
|
Change::Accent(String::new()),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.spacing(ui.space(Space::X2))
|
||||||
|
.align_y(iced::Alignment::Center);
|
||||||
|
|
||||||
|
let swatches: Vec<Element<'a, Message>> = ACCENT_PRESETS
|
||||||
|
.iter()
|
||||||
|
.map(|hex| swatch(ui, hex, current == *hex))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
column![
|
||||||
|
head,
|
||||||
|
row(swatches)
|
||||||
|
.spacing(ui.space(Space::X2))
|
||||||
|
.align_y(iced::Alignment::Center),
|
||||||
|
]
|
||||||
|
.spacing(ui.space(Space::X2))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding(Padding::from([ui.space(Space::X2), 0.0]))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 一颗色块:实心圆 + 描边(选中态加粗)。点击写入该预设。
|
||||||
|
fn swatch<'a>(ui: &'a Ui, hex: &'static str, selected: bool) -> Element<'a, Message> {
|
||||||
|
let fill = unlucky_design::color::Color::from_hex(hex).map(to_iced);
|
||||||
|
// 直径取控件高度的六成(40 → 24dp):与药丸按钮同一行时不抢节奏。
|
||||||
|
let diameter = ui.metrics().control_height * 0.6;
|
||||||
|
let ring = if selected {
|
||||||
|
ui.color(Role::OnSurface)
|
||||||
|
} else {
|
||||||
|
ui.color(Role::OutlineVariant)
|
||||||
|
};
|
||||||
|
let width = if selected { STROKE_PX * 2.0 } else { STROKE_PX };
|
||||||
|
let radius = ui.radius(ShapeScale::Full, diameter);
|
||||||
|
|
||||||
|
let dot: Element<'a, Message> = button(
|
||||||
|
Gap::new()
|
||||||
|
.width(Length::Fixed(diameter))
|
||||||
|
.height(Length::Fixed(diameter)),
|
||||||
|
)
|
||||||
|
.padding(Padding::from(0.0))
|
||||||
|
.style(move |_theme: &IcedTheme, status| button::Style {
|
||||||
|
background: fill.map(Background::Color),
|
||||||
|
text_color: ring,
|
||||||
|
border: Border {
|
||||||
|
color: match status {
|
||||||
|
button::Status::Hovered | button::Status::Pressed => ui.color(Role::OnSurface),
|
||||||
|
_ => ring,
|
||||||
|
},
|
||||||
|
width,
|
||||||
|
radius,
|
||||||
|
},
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
snap: true,
|
||||||
|
})
|
||||||
|
.on_press(Message::Appearance(Change::Accent(hex.to_owned())))
|
||||||
|
.into();
|
||||||
|
|
||||||
|
// 色块本体只有颜色、没有字:补一条可读提示(hex),否则键盘与读屏用户只能靠位置辨色。
|
||||||
|
Tooltip::new(dot, hint(ui, hex), tooltip::Position::Top).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 色块的悬停提示:`Overlay` 底 + `BodySmall` 的 hex 原文(与 `pages/agent` 的 tooltip 同一手法)。
|
||||||
|
fn hint<'a>(ui: &'a Ui, hex: &'a str) -> iced::widget::Container<'a, Message> {
|
||||||
|
let style = ui.style(unlucky_design::typography::TypeRole::BodySmall);
|
||||||
|
container(
|
||||||
|
text(hex.to_owned())
|
||||||
|
.size(style.size_px)
|
||||||
|
.font(ui.font_of(style))
|
||||||
|
.color(ui.color(Role::OnSurface)),
|
||||||
|
)
|
||||||
|
.padding(Padding::from([ui.space(Space::X1), ui.space(Space::X2)]))
|
||||||
|
.style(move |_theme: &IcedTheme| container::Style {
|
||||||
|
background: Some(Background::Color(
|
||||||
|
ui.layer(unlucky_design::elevation::MaterialLayer::Overlay),
|
||||||
|
)),
|
||||||
|
border: Border {
|
||||||
|
radius: ui.radius_dp(ShapeScale::Small),
|
||||||
|
..Border::default()
|
||||||
|
},
|
||||||
|
..container::Style::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 结构化原因 → 一句话(参数由 Rust 侧格式化,词表只认 `{name}`)。
|
||||||
|
fn reason(lang: Lang, error: AccentError) -> String {
|
||||||
|
match error {
|
||||||
|
AccentError::Short { digits } => tr(
|
||||||
|
lang,
|
||||||
|
Key::SettingsAppearanceAccentShort,
|
||||||
|
&[("digits", &digits.to_string())],
|
||||||
|
),
|
||||||
|
AccentError::Long { digits } => tr(
|
||||||
|
lang,
|
||||||
|
Key::SettingsAppearanceAccentLong,
|
||||||
|
&[("digits", &digits.to_string())],
|
||||||
|
),
|
||||||
|
AccentError::Digit { ch } => tr(
|
||||||
|
lang,
|
||||||
|
Key::SettingsAppearanceAccentDigit,
|
||||||
|
&[("ch", &visible(ch))],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 把一个字符变成**看得见**的形态:控制字符 / 空白 / 零宽与双向控制符(`Cf`)一律写 `U+XXXX`。
|
||||||
|
///
|
||||||
|
/// 为什么不能直接 `ch.to_string()`:用户粘进来的可能是不换行空格、零宽空格或 RTL 覆盖符——
|
||||||
|
/// 那几个字符在句子里**看不见**,错行会变成「强调色里有非十六进制字符:」后面一片空白,
|
||||||
|
/// 用户根本不知道自己错在哪。
|
||||||
|
fn visible(ch: char) -> String {
|
||||||
|
if ch.is_alphanumeric() || ch.is_ascii_graphic() {
|
||||||
|
ch.to_string()
|
||||||
|
} else {
|
||||||
|
format!("U+{:04X}", u32::from(ch))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::visible;
|
||||||
|
|
||||||
|
/// 错行里的字符必须**看得见**:不可见字符写成 `U+XXXX`,可见字符原样(r1 小事 2)。
|
||||||
|
///
|
||||||
|
/// 抓得住:把 `visible` 改回 `ch.to_string()`——`U+00A0` / `U+200B` 那两条立刻红(错行会变成
|
||||||
|
/// 「强调色里有非十六进制字符:」后面一片空白,用户不知道自己错在哪)。
|
||||||
|
/// 抓不住:句子本身(词表在 `unlucky-i18n`)。
|
||||||
|
#[test]
|
||||||
|
fn invisible_characters_are_shown_as_code_points() {
|
||||||
|
assert_eq!(visible('\u{a0}'), "U+00A0", "不换行空格看不见");
|
||||||
|
assert_eq!(visible('\u{200b}'), "U+200B", "零宽空格看不见");
|
||||||
|
assert_eq!(visible('\u{202e}'), "U+202E", "RTL 覆盖符看不见");
|
||||||
|
assert_eq!(visible(' '), "U+0020", "普通空格也看不见");
|
||||||
|
assert_eq!(visible('z'), "z", "可见字符原样");
|
||||||
|
assert_eq!(visible('#'), "#", "ASCII 标点原样");
|
||||||
|
assert_eq!(visible('中'), "中", "非 ASCII 可见字符原样");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
//!
|
//!
|
||||||
//! | 控件 | 键 | 改动消息 |
|
//! | 控件 | 键 | 改动消息 |
|
||||||
//! |---|---|---|
|
//! |---|---|---|
|
||||||
|
//! | 强调色文本框 + 预设色块(M250) | `appearance.accent_color` | [`Change::Accent`] |
|
||||||
//! | 整窗透明度滑块(下限 0.08) | `appearance.window_opacity` | [`Change::WindowOpacity`] |
|
//! | 整窗透明度滑块(下限 0.08) | `appearance.window_opacity` | [`Change::WindowOpacity`] |
|
||||||
//! | 窗口圆角(跟随系统 / 直角,M116) | `appearance.window_corner` | [`Change::Corner`] |
|
//! | 窗口圆角(跟随系统 / 直角,M116) | `appearance.window_corner` | [`Change::Corner`] |
|
||||||
//! | 面板透明度滑块(下限 0.4,CPU 档禁用) | `appearance.panel_alpha` | [`Change::PanelAlpha`] |
|
//! | 面板透明度滑块(下限 0.4,CPU 档禁用) | `appearance.panel_alpha` | [`Change::PanelAlpha`] |
|
||||||
@@ -72,6 +73,7 @@ use crate::theme::Ui;
|
|||||||
use super::state::State;
|
use super::state::State;
|
||||||
use super::Change;
|
use super::Change;
|
||||||
|
|
||||||
|
mod accent;
|
||||||
mod bg_image;
|
mod bg_image;
|
||||||
mod colour_source;
|
mod colour_source;
|
||||||
mod panel;
|
mod panel;
|
||||||
@@ -84,6 +86,8 @@ mod window;
|
|||||||
pub fn section<'a>(ui: &'a Ui, state: &'a State, lang: Lang) -> Element<'a, Message> {
|
pub fn section<'a>(ui: &'a Ui, state: &'a State, lang: Lang) -> Element<'a, Message> {
|
||||||
let mut rows: Vec<Element<'a, Message>> = Vec::new();
|
let mut rows: Vec<Element<'a, Message>> = Vec::new();
|
||||||
|
|
||||||
|
// M250:强调色排在最前 —— 它是「配色可自定义」这一页最直接的入口(键一直在,控件是新的)。
|
||||||
|
accent::push(ui, state, lang, &mut rows);
|
||||||
window::push(ui, state, lang, &mut rows);
|
window::push(ui, state, lang, &mut rows);
|
||||||
panel::push(ui, state, lang, &mut rows);
|
panel::push(ui, state, lang, &mut rows);
|
||||||
bg_image::push(ui, state, lang, &mut rows);
|
bg_image::push(ui, state, lang, &mut rows);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ use unlucky_presenter::prefs::{
|
|||||||
|
|
||||||
use crate::Backend;
|
use crate::Backend;
|
||||||
|
|
||||||
|
use super::accent::{Accent, ENV_ACCENT};
|
||||||
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, Note};
|
use super::background::{self, Image, Note};
|
||||||
use super::palette::{self, Resolved, Source};
|
use super::palette::{self, Resolved, Source};
|
||||||
@@ -60,6 +61,8 @@ pub struct State {
|
|||||||
pub palette_source: PaletteSource,
|
pub palette_source: PaletteSource,
|
||||||
/// 玻璃拟态开关(CPU 档强制关)。
|
/// 玻璃拟态开关(CPU 档强制关)。
|
||||||
pub glass: bool,
|
pub glass: bool,
|
||||||
|
/// 自定义强调色(M250 ACCENTWIRE:输入原文 / 解析结果 / 生效方案 / 结构化错误都在里面)。
|
||||||
|
pub accent: Accent,
|
||||||
/// 窗口圆角策略(M116:`system` / `square`;真能不能圆由平台判,见 `platform::plan_corner`)。
|
/// 窗口圆角策略(M116:`system` / `square`;真能不能圆由平台判,见 `platform::plan_corner`)。
|
||||||
pub window_corner: WindowCorner,
|
pub window_corner: WindowCorner,
|
||||||
/// 渲染后端(决定 CPU 档的两条降级)。
|
/// 渲染后端(决定 CPU 档的两条降级)。
|
||||||
@@ -95,6 +98,7 @@ impl State {
|
|||||||
palette_source: base.palette_source,
|
palette_source: base.palette_source,
|
||||||
window_corner: base.window_corner,
|
window_corner: base.window_corner,
|
||||||
glass: prefs.glass,
|
glass: prefs.glass,
|
||||||
|
accent: Accent::new(&prefs.accent_color),
|
||||||
backend,
|
backend,
|
||||||
selected_group: super::APPEARANCE_GROUP,
|
selected_group: super::APPEARANCE_GROUP,
|
||||||
image: None,
|
image: None,
|
||||||
@@ -132,6 +136,10 @@ impl State {
|
|||||||
if let Some(value) = std::env::var_os(ENV_PALETTE) {
|
if let Some(value) = std::env::var_os(ENV_PALETTE) {
|
||||||
self.palette_source = PaletteSource::from_key_value(&value.to_string_lossy());
|
self.palette_source = PaletteSource::from_key_value(&value.to_string_lossy());
|
||||||
}
|
}
|
||||||
|
if let Some(value) = std::env::var_os(ENV_ACCENT) {
|
||||||
|
// 取证开关 = 「像刚敲进去的」(走输入中规则),不是配置读回。
|
||||||
|
self.accent.set(&value.to_string_lossy());
|
||||||
|
}
|
||||||
self.refresh_background();
|
self.refresh_background();
|
||||||
self.refresh_palette();
|
self.refresh_palette();
|
||||||
}
|
}
|
||||||
@@ -218,7 +226,7 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 深浅切换后重算配色(`Ui` 换主题时调)。
|
/// 深浅切换后重算配色(`Ui` 换主题时调)。强调色不参与:它是用户给的绝对色值,与深浅无关。
|
||||||
pub fn set_flavor(&mut self, flavor: SchemeFlavor) {
|
pub fn set_flavor(&mut self, flavor: SchemeFlavor) {
|
||||||
if self.flavor == flavor {
|
if self.flavor == flavor {
|
||||||
return;
|
return;
|
||||||
@@ -236,6 +244,10 @@ impl State {
|
|||||||
/// 应用一条改动(设置页 → 状态)。返回**是否需要立刻下发整窗 α**。
|
/// 应用一条改动(设置页 → 状态)。返回**是否需要立刻下发整窗 α**。
|
||||||
pub fn apply(&mut self, change: super::Change) -> bool {
|
pub fn apply(&mut self, change: super::Change) -> bool {
|
||||||
use super::Change;
|
use super::Change;
|
||||||
|
// M250 p2-3:其余外观改动都意味着用户离开了强调色那一格 ⇒ 提交(理由见 `accent` 模块文档)。
|
||||||
|
if !matches!(change, Change::Accent(_)) {
|
||||||
|
self.accent.commit();
|
||||||
|
}
|
||||||
match change {
|
match change {
|
||||||
Change::WindowOpacity(alpha) => {
|
Change::WindowOpacity(alpha) => {
|
||||||
self.window_opacity = alpha;
|
self.window_opacity = alpha;
|
||||||
@@ -275,6 +287,10 @@ impl State {
|
|||||||
self.window_corner = policy;
|
self.window_corner = policy;
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
Change::Accent(text) => {
|
||||||
|
self.accent.set(&text);
|
||||||
|
false
|
||||||
|
}
|
||||||
Change::SelectGroup(index) => {
|
Change::SelectGroup(index) => {
|
||||||
self.selected_group = index;
|
self.selected_group = index;
|
||||||
false
|
false
|
||||||
@@ -282,11 +298,9 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 写回配置用的快照(`SettingsBridge::persist` 覆盖的那 8 个字段)。
|
/// 写回配置用的快照(`SettingsBridge::persist` 覆盖的那 9 个字段)。
|
||||||
///
|
///
|
||||||
/// 注意:**强调色 / 玻璃不在本 mission 的管辖内**,调用方(`app::appearance`)会拿配置里
|
/// M250 起 `accent_color` 也在里面(以前恒给空串 ⇒ 设置页改的强调色落不了盘),给的是**上一次合法值**。
|
||||||
/// 已有的快照做基底、只覆盖本 mission 真改了的字段,所以这里不产出完整快照。
|
|
||||||
/// 这个函数只给「当前值是什么」一个只读视图(日志与测试用)。
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn current_values(&self) -> AppearancePrefs {
|
pub fn current_values(&self) -> AppearancePrefs {
|
||||||
AppearancePrefs {
|
AppearancePrefs {
|
||||||
@@ -296,7 +310,7 @@ impl State {
|
|||||||
bg_dim: self.bg_dim,
|
bg_dim: self.bg_dim,
|
||||||
panel_alpha: self.panel_alpha,
|
panel_alpha: self.panel_alpha,
|
||||||
glass: self.glass,
|
glass: self.glass,
|
||||||
accent_color: String::new(),
|
accent_color: self.accent.persist_value().to_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +322,7 @@ impl State {
|
|||||||
.map_or_else(|| "none".to_owned(), |byte| byte.to_string());
|
.map_or_else(|| "none".to_owned(), |byte| byte.to_string());
|
||||||
format!(
|
format!(
|
||||||
"[appearance] {context} window_alpha={:.3} panel_alpha={:.3} (requested {:.3}) \
|
"[appearance] {context} window_alpha={:.3} panel_alpha={:.3} (requested {:.3}) \
|
||||||
byte={byte} corner={} backend={} bg={} fill={} dim={:.2} palette={} group={}",
|
byte={byte} corner={} backend={} bg={} fill={} dim={:.2} palette={} accent={} group={}",
|
||||||
self.window_opacity,
|
self.window_opacity,
|
||||||
self.effective_panel_alpha(),
|
self.effective_panel_alpha(),
|
||||||
self.panel_alpha,
|
self.panel_alpha,
|
||||||
@@ -322,6 +336,7 @@ impl State {
|
|||||||
self.bg_fill.as_str(),
|
self.bg_fill.as_str(),
|
||||||
self.bg_dim,
|
self.bg_dim,
|
||||||
self.palette_level(),
|
self.palette_level(),
|
||||||
|
self.accent.log_token(),
|
||||||
self.selected_group
|
self.selected_group
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -336,6 +351,8 @@ impl State {
|
|||||||
self.bg_fill = prefs.bg_fill;
|
self.bg_fill = prefs.bg_fill;
|
||||||
self.bg_dim = prefs.bg_dim;
|
self.bg_dim = prefs.bg_dim;
|
||||||
self.glass = prefs.glass;
|
self.glass = prefs.glass;
|
||||||
|
// M250:文件被外部改过(CLI / AI)时强调色也要热生效,错行同样重置。
|
||||||
|
self.accent.reset(&prefs.accent_color);
|
||||||
if self.bg_path != prefs.bg_image {
|
if self.bg_path != prefs.bg_image {
|
||||||
self.bg_path = prefs.bg_image.clone();
|
self.bg_path = prefs.bg_image.clone();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ use unlucky_i18n::catalog::Key;
|
|||||||
use unlucky_i18n::Lang;
|
use unlucky_i18n::Lang;
|
||||||
use unlucky_presenter::prefs::{keys, BgFill, PaletteSource};
|
use unlucky_presenter::prefs::{keys, BgFill, PaletteSource};
|
||||||
|
|
||||||
|
mod accent;
|
||||||
|
|
||||||
/// 抓得住:每条改动都点名到自己那个 schema 键(写错键 = 静默写到别的配置项上)。
|
/// 抓得住:每条改动都点名到自己那个 schema 键(写错键 = 静默写到别的配置项上)。
|
||||||
/// 抓不住:这些键是否真的在 core 的 schema 白名单里(那由 presenter/core 的测试守)。
|
/// 抓不住:这些键是否真的在 core 的 schema 白名单里(那由 presenter/core 的测试守)。
|
||||||
#[test]
|
#[test]
|
||||||
@@ -93,7 +95,7 @@ fn appearance_texts(lang: Lang, case: Case) -> Vec<(Key, String)> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let (backend, alpha) = match case {
|
let (backend, alpha) = match case {
|
||||||
Case::Factory => (Backend::Auto, 1.0),
|
Case::Factory | Case::BadAccent => (Backend::Auto, 1.0),
|
||||||
Case::Cpu => (Backend::Cpu, 1.0),
|
Case::Cpu => (Backend::Cpu, 1.0),
|
||||||
Case::Dim => (Backend::Gpu, 0.5),
|
Case::Dim => (Backend::Gpu, 0.5),
|
||||||
};
|
};
|
||||||
@@ -106,6 +108,10 @@ fn appearance_texts(lang: Lang, case: Case) -> Vec<(Key, String)> {
|
|||||||
if alpha < 1.0 {
|
if alpha < 1.0 {
|
||||||
state.apply(Change::WindowOpacity(alpha));
|
state.apply(Change::WindowOpacity(alpha));
|
||||||
}
|
}
|
||||||
|
if matches!(case, Case::BadAccent) {
|
||||||
|
// M250:错行只在失败态出现,所以「三档状态合起来覆盖本页全部 key」需要这一档。
|
||||||
|
state.apply(Change::Accent("zzzzzz".to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
let ui = Ui::new(SchemeFlavor::Light, FAMILIES);
|
let ui = Ui::new(SchemeFlavor::Light, FAMILIES);
|
||||||
// 前面几条用例可能留了记录:本线程的取证每次都从清空开始。
|
// 前面几条用例可能留了记录:本线程的取证每次都从清空开始。
|
||||||
@@ -142,6 +148,8 @@ enum Case {
|
|||||||
Cpu,
|
Cpu,
|
||||||
/// 整窗 α 调低(圆角暂时不生效)。
|
/// 整窗 α 调低(圆角暂时不生效)。
|
||||||
Dim,
|
Dim,
|
||||||
|
/// 强调色填了非法值(M250:错行 + 输入框失败态)。
|
||||||
|
BadAccent,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 这一页**常驻**的 key(三档状态合起来必须一个不缺地取到)。
|
/// 这一页**常驻**的 key(三档状态合起来必须一个不缺地取到)。
|
||||||
@@ -150,6 +158,11 @@ enum Case {
|
|||||||
/// 小图平铺)取决于运行时状态,单测里要让它们出现得造磁盘上的坏图;那些由
|
/// 小图平铺)取决于运行时状态,单测里要让它们出现得造磁盘上的坏图;那些由
|
||||||
/// `background/tests.rs` 与 `palette.rs` 的单测按 key 逐条钉(见各自的用例注释)。
|
/// `background/tests.rs` 与 `palette.rs` 的单测按 key 逐条钉(见各自的用例注释)。
|
||||||
const CARD_KEYS: &[Key] = &[
|
const CARD_KEYS: &[Key] = &[
|
||||||
|
Key::SettingsAppearanceAccent,
|
||||||
|
Key::SettingsAppearanceAccentPresets,
|
||||||
|
Key::SettingsAppearanceAccentFactory,
|
||||||
|
Key::SettingsAppearanceAccentHint,
|
||||||
|
Key::SettingsAppearanceAccentDigit,
|
||||||
Key::SettingsAppearanceCorner,
|
Key::SettingsAppearanceCorner,
|
||||||
Key::SettingsAppearanceCornerSystem,
|
Key::SettingsAppearanceCornerSystem,
|
||||||
Key::SettingsAppearanceCornerSquare,
|
Key::SettingsAppearanceCornerSquare,
|
||||||
@@ -191,7 +204,7 @@ fn the_appearance_card_speaks_chinese() {
|
|||||||
// 白名单:技术专名允许是纯 ASCII(现在一条都没有——`%` / 数字不在文案里,它们是参数)。
|
// 白名单:技术专名允许是纯 ASCII(现在一条都没有——`%` / 数字不在文案里,它们是参数)。
|
||||||
const ASCII_OK: &[&str] = &[];
|
const ASCII_OK: &[&str] = &[];
|
||||||
|
|
||||||
for case in [Case::Factory, Case::Cpu, Case::Dim] {
|
for case in [Case::Factory, Case::Cpu, Case::Dim, Case::BadAccent] {
|
||||||
let pairs = appearance_texts(Lang::ZhCn, case);
|
let pairs = appearance_texts(Lang::ZhCn, case);
|
||||||
assert!(
|
assert!(
|
||||||
pairs.len() >= 20,
|
pairs.len() >= 20,
|
||||||
@@ -222,7 +235,7 @@ fn the_appearance_card_speaks_chinese() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn every_appearance_card_key_is_drawn() {
|
fn every_appearance_card_key_is_drawn() {
|
||||||
let mut seen: Vec<Key> = Vec::new();
|
let mut seen: Vec<Key> = Vec::new();
|
||||||
for case in [Case::Factory, Case::Cpu, Case::Dim] {
|
for case in [Case::Factory, Case::Cpu, Case::Dim, Case::BadAccent] {
|
||||||
seen.extend(
|
seen.extend(
|
||||||
appearance_texts(Lang::ZhCn, case)
|
appearance_texts(Lang::ZhCn, case)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -244,7 +257,7 @@ fn every_appearance_card_key_is_drawn() {
|
|||||||
/// 英文界面下不许出现汉字(否则「含汉字」可能来自写死的中文,而不是词表)。
|
/// 英文界面下不许出现汉字(否则「含汉字」可能来自写死的中文,而不是词表)。
|
||||||
#[test]
|
#[test]
|
||||||
fn the_appearance_card_keeps_its_english_baseline() {
|
fn the_appearance_card_keeps_its_english_baseline() {
|
||||||
for case in [Case::Factory, Case::Cpu, Case::Dim] {
|
for case in [Case::Factory, Case::Cpu, Case::Dim, Case::BadAccent] {
|
||||||
let texts = appearance_texts(Lang::En, case);
|
let texts = appearance_texts(Lang::En, case);
|
||||||
let with_han: Vec<&(Key, String)> = texts
|
let with_han: Vec<&(Key, String)> = texts
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
//! 强调色接线的两条判据(M250 ACCENTWIRE):判据一「两套配置下同一组件色值不同」、
|
||||||
|
//! 判据二「非法值拿到结构化原因且不静默回退」。
|
||||||
|
//!
|
||||||
|
//! 为什么从 `tests.rs` 再拆一层:这两条用例加起来 150 行,`tests.rs` 会越过 400 行硬线
|
||||||
|
//! (M180 把它从 `mod.rs` 拆出来就是为了这个)。判据本身与写进 `tests.rs` 的版本逐字一致,
|
||||||
|
//! 只是换了落点。
|
||||||
|
|
||||||
|
use super::Change;
|
||||||
|
|
||||||
|
/// 青绿(`00 6A 5C`)由 design 的 `Color` 拼出来:本 crate 的 `src/**` 不许出现十六进制色值
|
||||||
|
/// 字面量(`tests/no_hardcoded_tokens.rs` 把测试文件也扫在内——那条门禁是对的,色值只能来自令牌)。
|
||||||
|
const TEAL: unlucky_design::color::Color = unlucky_design::color::Color::rgb(0x00, 0x6A, 0x5C);
|
||||||
|
|
||||||
|
/// 判据一:`accent_color` 为空 与 为青绿(见 [`TEAL`])时,**同一组件取到的 primary 类角色色值必须不同**。
|
||||||
|
///
|
||||||
|
/// 取色点是 `progress::linear_colors`(进度条指示条 = `Role::Primary`,实心按钮的容器也是它):
|
||||||
|
/// 断言的是**取到的值**,不是「没 panic」——把接线改回「恒走出厂板」,两边相等当场红。
|
||||||
|
///
|
||||||
|
/// 抓得住:① 接线不存在(两套 Ui 同色);② 只改了 `Primary` 而漏掉 secondary/tertiary;
|
||||||
|
/// ③ 有人把 `Scheme::from_accent` 的中性基准换掉(下面那条 `assert_eq` 会红)。
|
||||||
|
/// 抓不住:像素(样式闭包被别处覆盖)——那是出图与 `components/**` 的 probe 用例的事。
|
||||||
|
#[test]
|
||||||
|
fn custom_accent_changes_the_component_colours_it_reaches() {
|
||||||
|
use crate::app::components::feedback::progress;
|
||||||
|
use crate::cli::Backend;
|
||||||
|
use crate::theme::{Families, Ui};
|
||||||
|
use unlucky_design::color::{Role, Scheme, SchemeFlavor};
|
||||||
|
use unlucky_presenter::prefs::{AppearanceBase, AppearancePrefs};
|
||||||
|
|
||||||
|
use super::super::State;
|
||||||
|
|
||||||
|
const FAMILIES: Families = Families {
|
||||||
|
text: "Segoe UI",
|
||||||
|
display: "Segoe UI",
|
||||||
|
mono: "Consolas",
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut state = State::new(
|
||||||
|
&AppearancePrefs::factory(),
|
||||||
|
&AppearanceBase::factory(),
|
||||||
|
Backend::Gpu,
|
||||||
|
SchemeFlavor::Light,
|
||||||
|
);
|
||||||
|
// 空配置 = 出厂板(`rebuild_ui` 的 `(None, None)` 分支同一条路)
|
||||||
|
assert!(
|
||||||
|
state.accent.color().is_none(),
|
||||||
|
"空串是「用出厂板」,不该被解析成自定义色"
|
||||||
|
);
|
||||||
|
let factory = Ui::new(SchemeFlavor::Light, FAMILIES);
|
||||||
|
|
||||||
|
// 填青绿 = `rebuild_ui` 的 `(Some(accent), _)` 分支
|
||||||
|
state.apply(Change::Accent(TEAL.to_hex()));
|
||||||
|
assert_eq!(state.accent.error(), None, "合法 hex 不该报错");
|
||||||
|
let accent = state.accent.color().expect("合法 hex 必须解析成颜色");
|
||||||
|
let custom = Ui::with_accent(SchemeFlavor::Light, accent, FAMILIES);
|
||||||
|
|
||||||
|
let (_, factory_primary) = progress::linear_colors(&factory);
|
||||||
|
let (_, custom_primary) = progress::linear_colors(&custom);
|
||||||
|
assert_eq!(factory_primary, factory.color(Role::Primary));
|
||||||
|
assert_eq!(custom_primary, custom.color(Role::Primary));
|
||||||
|
assert_ne!(
|
||||||
|
factory_primary, custom_primary,
|
||||||
|
"同一组件的 primary 类角色在两套配置下同色 ⇒ 强调色没接线"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 强调族逐条跟着变(不只 primary)。
|
||||||
|
//
|
||||||
|
// `OnPrimary` 刻意**不在**清单里:深浅两套方案的 on-primary 都是 tone 100/20 的白/深色,
|
||||||
|
// 强调色只换色相,这一格在两套方案下同为纯白——令牌表里两格同值是合法的,钉它会假红。
|
||||||
|
// 「强调族真的换了色相」由下面三个色相承载角色负责:Primary / PrimaryContainer / Tertiary。
|
||||||
|
let baseline = Ui::from_scheme(Scheme::baseline(SchemeFlavor::Light), FAMILIES);
|
||||||
|
for role in [
|
||||||
|
Role::Primary,
|
||||||
|
Role::PrimaryContainer,
|
||||||
|
Role::Secondary,
|
||||||
|
Role::Tertiary,
|
||||||
|
] {
|
||||||
|
assert_ne!(
|
||||||
|
custom.color(role),
|
||||||
|
baseline.color(role),
|
||||||
|
"{role:?} 没跟着强调色走"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// **中性表面与 error 保持基线**:`Scheme::from_accent` 的既定语义(design 侧登记,不是没接线)
|
||||||
|
for role in [
|
||||||
|
Role::Surface,
|
||||||
|
Role::SurfaceContainerHighest,
|
||||||
|
Role::OnSurface,
|
||||||
|
Role::Outline,
|
||||||
|
Role::Error,
|
||||||
|
Role::OnError,
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
custom.color(role),
|
||||||
|
baseline.color(role),
|
||||||
|
"{role:?} 被强调色染了:`Scheme::from_accent` 只换 primary/secondary/tertiary 三族"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 判据二:非法值必须拿到**结构化的原因**,生效色不回退、不写盘,tokens 仍可构造(不崩)。
|
||||||
|
///
|
||||||
|
/// 抓得住:把非法分支改成「静默回退出厂色」——`color()` 会变成 `None`、`persist_value()` 会变成
|
||||||
|
/// 空串,三条断言当场红;把错误吞成 `bool`——`error()` 不再是 `Some(..)`,也红。
|
||||||
|
/// 抓不住:错行在屏幕上到底画没画出来(`every_appearance_card_key_is_drawn` 的 `BadAccent` 档
|
||||||
|
/// 证明取词口被走到;像素由出图负责)。
|
||||||
|
#[test]
|
||||||
|
fn illegal_accent_reports_a_reason_and_never_silently_falls_back() {
|
||||||
|
use crate::cli::Backend;
|
||||||
|
use crate::theme::{Families, Ui};
|
||||||
|
use unlucky_design::color::{Role, SchemeFlavor};
|
||||||
|
use unlucky_presenter::prefs::{AccentError, AppearanceBase, AppearancePrefs};
|
||||||
|
|
||||||
|
use super::super::State;
|
||||||
|
|
||||||
|
const FAMILIES: Families = Families {
|
||||||
|
text: "Segoe UI",
|
||||||
|
display: "Segoe UI",
|
||||||
|
mono: "Consolas",
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut state = State::new(
|
||||||
|
&AppearancePrefs::factory(),
|
||||||
|
&AppearanceBase::factory(),
|
||||||
|
Backend::Gpu,
|
||||||
|
SchemeFlavor::Light,
|
||||||
|
);
|
||||||
|
state.apply(Change::Accent(TEAL.to_hex()));
|
||||||
|
let good = state.accent.color().expect("先放一个合法值");
|
||||||
|
let good_primary = Ui::with_accent(SchemeFlavor::Light, good, FAMILIES).color(Role::Primary);
|
||||||
|
|
||||||
|
// 超长串由合法值拼出来:`src/**` 的色值字面量门禁把注释与测试也算在内(那条门禁是对的)。
|
||||||
|
//
|
||||||
|
// 三档里只有后两档**当场**亮:`#12` 是「还可能是合法前缀」(用户可能正打到一半),它的
|
||||||
|
// 亮灯时机由 `accent_errors_only_appear_when_they_are_actionable` 与 `commit` 覆盖。
|
||||||
|
let long = format!("{}00112233", TEAL.to_hex());
|
||||||
|
for (text, expected) in [
|
||||||
|
("zzzzzz".to_owned(), AccentError::Digit { ch: 'z' }),
|
||||||
|
(long, AccentError::Long { digits: 14 }),
|
||||||
|
] {
|
||||||
|
state.apply(Change::Accent(text.clone()));
|
||||||
|
assert_eq!(
|
||||||
|
state.accent.error(),
|
||||||
|
Some(expected),
|
||||||
|
"{text}: 不可能变合法的输入必须当场亮且原因结构化(不是一句 bool / 一句「格式不对」)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
state.accent.color(),
|
||||||
|
Some(good),
|
||||||
|
"{text}: 静默回退出厂色了(非法输入不该改生效色)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
state.accent.persist_value(),
|
||||||
|
TEAL.to_hex(),
|
||||||
|
"{text}: 非法值写进配置了"
|
||||||
|
);
|
||||||
|
// tokens 仍可构造,取到的还是上一次合法色
|
||||||
|
let ui = Ui::with_accent(SchemeFlavor::Light, state.accent.color().unwrap(), FAMILIES);
|
||||||
|
assert_eq!(ui.color(Role::Primary), good_primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// `#12`:输入中不亮,**提交**后必须亮(否则「非法值可见」会在短输入上落空)
|
||||||
|
state.apply(Change::Accent("#12".to_owned()));
|
||||||
|
assert_eq!(state.accent.error(), None, "#12: 输入中先不亮");
|
||||||
|
state.accent.commit();
|
||||||
|
assert_eq!(
|
||||||
|
state.accent.error(),
|
||||||
|
Some(AccentError::Short { digits: 2 }),
|
||||||
|
"#12: 提交后必须亮且原因正确"
|
||||||
|
);
|
||||||
|
assert_eq!(state.accent.color(), Some(good), "#12: 提交也不许改生效色");
|
||||||
|
|
||||||
|
// 空串是**合法**的「回出厂板」,不是错误
|
||||||
|
state.apply(Change::Accent(String::new()));
|
||||||
|
assert_eq!(state.accent.error(), None, "空串不是错误");
|
||||||
|
assert!(state.accent.color().is_none(), "空串 = 出厂板");
|
||||||
|
assert_eq!(state.accent.persist_value(), "");
|
||||||
|
|
||||||
|
// 小写、缺 `#` 都吃得下;落盘一律规范化成大写的 `#RRGGBB`
|
||||||
|
state.apply(Change::Accent(TEAL.to_hex().to_lowercase()));
|
||||||
|
assert_eq!(state.accent.persist_value(), TEAL.to_hex());
|
||||||
|
state.apply(Change::Accent(
|
||||||
|
TEAL.to_hex().trim_start_matches('#').to_owned(),
|
||||||
|
));
|
||||||
|
assert_eq!(state.accent.persist_value(), TEAL.to_hex());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **p2-3**:错行与 `Field::error()` 的出现时机 —— 只在「提交」或「不可能再变合法」时。
|
||||||
|
///
|
||||||
|
/// 抓得住:把 `Accent::retry` 的延后逻辑删掉(所有错都立刻亮)⇒ 第一段当场红(手打青绿
|
||||||
|
/// 的每一个前缀都会亮);把 `commit` 改成空实现 ⇒ 第二段红(提交后不亮,等于「非法值不可见」);
|
||||||
|
/// 把配置读回也走延后 ⇒ 第三段红。
|
||||||
|
/// 抓不住:像素(红框与红字到底画没画)——那是出图与 `every_appearance_card_key_is_drawn` 的事。
|
||||||
|
#[test]
|
||||||
|
fn accent_errors_only_appear_when_they_are_actionable() {
|
||||||
|
use crate::cli::Backend;
|
||||||
|
use unlucky_design::color::SchemeFlavor;
|
||||||
|
use unlucky_presenter::prefs::{AccentError, AppearanceBase, AppearancePrefs};
|
||||||
|
|
||||||
|
use super::super::State;
|
||||||
|
|
||||||
|
let factory = AppearancePrefs::factory();
|
||||||
|
let mut state = State::new(
|
||||||
|
&factory,
|
||||||
|
&AppearanceBase::factory(),
|
||||||
|
Backend::Gpu,
|
||||||
|
SchemeFlavor::Light,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ① 手打青绿的过程:每个**合法前缀**都不许亮红(否则一次输入闪 5 次)
|
||||||
|
for prefix in ["#", "#0", "#00", "#006", "#006A", "#006A5"] {
|
||||||
|
state.apply(Change::Accent(prefix.to_owned()));
|
||||||
|
assert_eq!(
|
||||||
|
state.accent.error(),
|
||||||
|
None,
|
||||||
|
"{prefix:?}: 还没打完就亮红了(手打一个合法色会闪好几次)"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
state.accent.color().is_none(),
|
||||||
|
"{prefix:?}: 没打完的输入不该生效"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
state.apply(Change::Accent(TEAL.to_hex()));
|
||||||
|
assert_eq!(state.accent.error(), None);
|
||||||
|
assert!(state.accent.color().is_some(), "打完了就该生效");
|
||||||
|
|
||||||
|
// ② 输入中留着不打完 ⇒ 不亮;**提交**(点到别的外观控件)⇒ 亮且原因正确
|
||||||
|
state.apply(Change::Accent("#12".to_owned()));
|
||||||
|
assert_eq!(state.accent.error(), None, "输入中先不亮");
|
||||||
|
state.apply(Change::PanelAlpha(0.8));
|
||||||
|
assert_eq!(
|
||||||
|
state.accent.error(),
|
||||||
|
Some(AccentError::Short { digits: 2 }),
|
||||||
|
"离开这一格之后必须亮,否则非法值就不可见了"
|
||||||
|
);
|
||||||
|
|
||||||
|
// ③ 配置读回(`Accent::new`)⇒ **当场**亮,不等用户来点
|
||||||
|
let bad = AppearancePrefs {
|
||||||
|
accent_color: "#12".to_owned(),
|
||||||
|
..AppearancePrefs::factory()
|
||||||
|
};
|
||||||
|
let loaded = State::new(
|
||||||
|
&bad,
|
||||||
|
&AppearanceBase::factory(),
|
||||||
|
Backend::Gpu,
|
||||||
|
SchemeFlavor::Light,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
loaded.accent.error(),
|
||||||
|
Some(AccentError::Short { digits: 2 }),
|
||||||
|
"配置文件里的坏值必须当场可见"
|
||||||
|
);
|
||||||
|
assert!(loaded.accent.color().is_none(), "坏值不生效,走出厂板");
|
||||||
|
}
|
||||||
@@ -18,7 +18,6 @@ use crate::fonts;
|
|||||||
use crate::pages::appearance;
|
use crate::pages::appearance;
|
||||||
use crate::probe::{self, Probe};
|
use crate::probe::{self, Probe};
|
||||||
use crate::renderer;
|
use crate::renderer;
|
||||||
use crate::theme::Ui;
|
|
||||||
|
|
||||||
use super::{demo_model, Harness, Message, Request, CAPTURE_POSITION};
|
use super::{demo_model, Harness, Message, Request, CAPTURE_POSITION};
|
||||||
|
|
||||||
@@ -98,7 +97,12 @@ pub fn run(request: Request) -> Result<(), String> {
|
|||||||
|
|
||||||
// 外观**真状态**(`--appearance=real`):工厂态 + `ULL_APPEARANCE_*` 覆盖。
|
// 外观**真状态**(`--appearance=real`):工厂态 + `ULL_APPEARANCE_*` 覆盖。
|
||||||
// 这不是给设置页塞常量:状态从 `State::new` / `apply_env` 来,打日志的是它算出来的生效值。
|
// 这不是给设置页塞常量:状态从 `State::new` / `apply_env` 来,打日志的是它算出来的生效值。
|
||||||
let appearance = request.real_appearance.then(|| {
|
//
|
||||||
|
// M250:**gallery 也要这一份** —— 强调色出图矩阵拍的是组件库(buttons + progress),
|
||||||
|
// 而 `--appearance=real` 按设计只对设置页生效(`request.rs` 的守卫,别绕过它)。所以判据加
|
||||||
|
// 一条:设了 `ULL_APPEARANCE_ACCENT` 就造状态,让 `Ui` 按强调色求值(设置页的语义不变)。
|
||||||
|
let wants_accent = std::env::var_os(appearance::accent::ENV_ACCENT).is_some();
|
||||||
|
let appearance = (request.real_appearance || wants_accent).then(|| {
|
||||||
let mut state = appearance::State::new(
|
let mut state = appearance::State::new(
|
||||||
&AppearancePrefs::factory(),
|
&AppearancePrefs::factory(),
|
||||||
&AppearanceBase::factory(),
|
&AppearanceBase::factory(),
|
||||||
@@ -116,7 +120,7 @@ pub fn run(request: Request) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"[harness] 外观真状态:window={:.2} panel={:.2}(生效 {:.2})bg={} fill={:?} dim={:.2} \
|
"[harness] 外观真状态:window={:.2} panel={:.2}(生效 {:.2})bg={} fill={:?} dim={:.2} \
|
||||||
配色来源={:?} 选中分组={}",
|
配色来源={:?} accent={} 选中分组={}",
|
||||||
state.window_opacity,
|
state.window_opacity,
|
||||||
state.panel_alpha,
|
state.panel_alpha,
|
||||||
state.effective_panel_alpha(),
|
state.effective_panel_alpha(),
|
||||||
@@ -124,6 +128,7 @@ pub fn run(request: Request) -> Result<(), String> {
|
|||||||
state.bg_fill,
|
state.bg_fill,
|
||||||
state.bg_dim,
|
state.bg_dim,
|
||||||
state.palette_source,
|
state.palette_source,
|
||||||
|
state.accent.log_token(),
|
||||||
state.selected_group,
|
state.selected_group,
|
||||||
);
|
);
|
||||||
state
|
state
|
||||||
@@ -147,9 +152,18 @@ pub fn run(request: Request) -> Result<(), String> {
|
|||||||
if boot.screenshot.is_some() {
|
if boot.screenshot.is_some() {
|
||||||
model.forced = Some(Interaction::Enabled);
|
model.forced = Some(Interaction::Enabled);
|
||||||
}
|
}
|
||||||
|
// M250:gallery 出图也要能看见强调色。**规则不在这里抄** —— 调产品用的同一个
|
||||||
|
// `theme::tokens`(优先级只定义一次)。如实补一句:harness 不建 `App`,所以这里
|
||||||
|
// 没有 D38 取色方案(第三个实参恒 `None`),出图**不覆盖**那条分支。
|
||||||
|
let (ui, _) = crate::theme::tokens(
|
||||||
|
boot.flavor,
|
||||||
|
appearance.as_ref().and_then(|state| state.accent.color()),
|
||||||
|
None,
|
||||||
|
families,
|
||||||
|
);
|
||||||
(
|
(
|
||||||
Harness {
|
Harness {
|
||||||
ui: Ui::new(boot.flavor, families),
|
ui,
|
||||||
model,
|
model,
|
||||||
appearance: appearance.clone(),
|
appearance: appearance.clone(),
|
||||||
form: crate::pages::demo_launch_form(),
|
form: crate::pages::demo_launch_form(),
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
//! `Scheme::baseline`(tone 派生)退回它本来的角色——D38 取色链的中性锚点。两条路径并列:
|
//! `Scheme::baseline`(tone 派生)退回它本来的角色——D38 取色链的中性锚点。两条路径并列:
|
||||||
//!
|
//!
|
||||||
//! - [`Ui::brand`]:出厂外观,色板自己决定深浅([`BrandBoard::flavor`]);
|
//! - [`Ui::brand`]:出厂外观,色板自己决定深浅([`BrandBoard::flavor`]);
|
||||||
|
//! - [`Ui::with_accent`]:用户自定义强调色接管 primary/secondary/tertiary 三族(M250),
|
||||||
|
//! 中性表面与 error 保持基线;
|
||||||
//! - [`Ui::from_scheme`]:背景图 / 壁纸取色成功时接管 49 个角色(取色失败回落色板);
|
//! - [`Ui::from_scheme`]:背景图 / 壁纸取色成功时接管 49 个角色(取色失败回落色板);
|
||||||
//! - [`Ui::baseline`]:老的 tone 派生口径,只给对照与测试用(产品路径不再默认走它)。
|
//! - [`Ui::baseline`]:老的 tone 派生口径,只给对照与测试用(产品路径不再默认走它)。
|
||||||
//!
|
//!
|
||||||
@@ -124,6 +126,19 @@ impl Ui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 按**用户自定义强调色**求值(M250 ACCENTWIRE):`DesignTokens::with_accent` 的 iced 侧入口。
|
||||||
|
///
|
||||||
|
/// 语义来自 design 的 [`Scheme::from_accent`](既定产品口径,不是缺口):只换
|
||||||
|
/// primary / secondary / tertiary 三族,**中性表面与 error 保持基线**——「换强调色不该让整个
|
||||||
|
/// 窗口底色变脏」。所以拿它跟 [`Ui::new`] 比,Primary 类角色会不同,Surface 系列**不会**。
|
||||||
|
pub fn with_accent(
|
||||||
|
flavor: SchemeFlavor,
|
||||||
|
accent: unlucky_design::color::Color,
|
||||||
|
families: Families,
|
||||||
|
) -> Self {
|
||||||
|
Self::from_scheme(accent_scheme(flavor, accent), families)
|
||||||
|
}
|
||||||
|
|
||||||
/// 当前平台(决定字体族与字重降级)。
|
/// 当前平台(决定字体族与字重降级)。
|
||||||
pub fn platform(&self) -> Platform {
|
pub fn platform(&self) -> Platform {
|
||||||
self.platform
|
self.platform
|
||||||
@@ -315,6 +330,48 @@ pub fn to_iced(color: unlucky_design::color::Color) -> Color {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 强调色 → 49 角色方案:`DesignTokens::with_accent` 的**唯一**调用点(M250)。
|
||||||
|
///
|
||||||
|
/// 放在适配层而不是调用点,是因为「强调色只换三族、中性色保持基线」这条语义属于令牌层;
|
||||||
|
/// 页面侧只该拿到「一套方案」,不该各自决定换哪几个角色。
|
||||||
|
pub fn accent_scheme(flavor: SchemeFlavor, accent: unlucky_design::color::Color) -> Scheme {
|
||||||
|
unlucky_design::DesignTokens::with_accent(flavor, accent).colors
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 这一屏的颜色从哪来(M250)。日志与出图都靠它说清「是谁生效」。
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Colors {
|
||||||
|
/// 用户显式填的强调色(`appearance.accent_color`)。
|
||||||
|
Accent,
|
||||||
|
/// D38 取色方案(背景图 / 壁纸取色成功,`State::derived_scheme`)。
|
||||||
|
Scheme,
|
||||||
|
/// 出厂品牌色板(`DesignTokens::brand`)。
|
||||||
|
Board,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 强调色 / 取色方案 / 出厂板 **三选一**:全仓**唯一**的优先级定义点(M250)。
|
||||||
|
///
|
||||||
|
/// 产品路径(`App::rebuild_ui`)与出图 harness(`pages::harness`)都调它。为什么必须有这一个
|
||||||
|
/// 函数:两头各抄一份两选一/三选一,迟早出现「出图上有强调色、产品里没有」或者反过来,而那种
|
||||||
|
/// 偏差在图上看不出来(图是对的,产品是错的)。
|
||||||
|
///
|
||||||
|
/// 优先级:**显式强调色 > D38 取色方案 > 出厂板**。手填的色是直接意图,取色是从图片派生的。
|
||||||
|
/// `flavor`(明暗 / 跟随系统)不在这里解析,由调用方按现有主题逻辑给。
|
||||||
|
pub fn tokens(
|
||||||
|
flavor: SchemeFlavor,
|
||||||
|
accent: Option<unlucky_design::color::Color>,
|
||||||
|
scheme: Option<Scheme>,
|
||||||
|
families: Families,
|
||||||
|
) -> (Ui, Colors) {
|
||||||
|
if let Some(accent) = accent {
|
||||||
|
(Ui::with_accent(flavor, accent, families), Colors::Accent)
|
||||||
|
} else if let Some(scheme) = scheme {
|
||||||
|
(Ui::from_scheme(scheme, families), Colors::Scheme)
|
||||||
|
} else {
|
||||||
|
(Ui::new(flavor, families), Colors::Board)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 令牌字重 → iced 字重。
|
/// 令牌字重 → iced 字重。
|
||||||
pub fn weight_of(weight: Weight) -> iced::font::Weight {
|
pub fn weight_of(weight: Weight) -> iced::font::Weight {
|
||||||
match weight {
|
match weight {
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ pub fn launcher_schema() -> SettingSchema {
|
|||||||
"",
|
"",
|
||||||
9,
|
9,
|
||||||
AiPolicy::audited(),
|
AiPolicy::audited(),
|
||||||
"自定义强调色 #RRGGBB(空 = 出厂 #0078D4;accent 令牌全域生效)",
|
"自定义强调色 #RRGGBB(空 = 出厂品牌板;非空时只换 primary/secondary/tertiary 三族,中性表面与 error 保持基线;非法值不写盘,设置页给错行)",
|
||||||
))
|
))
|
||||||
// M91(P4 / D36·D37·D38):「外观下层」三键——只登记取值与范围,判定与落地都在 UI 侧。
|
// M91(P4 / D36·D37·D38):「外观下层」三键——只登记取值与范围,判定与落地都在 UI 侧。
|
||||||
// 分级同 D17 那批(audited/Medium):纯视觉、可一键复原,但要留审计痕迹。
|
// 分级同 D17 那批(audited/Medium):纯视觉、可一键复原,但要留审计痕迹。
|
||||||
|
|||||||
@@ -195,8 +195,8 @@ use loader::loader_entries;
|
|||||||
use logs::logs_entries;
|
use logs::logs_entries;
|
||||||
use nav::nav_entries;
|
use nav::nav_entries;
|
||||||
use settings::{
|
use settings::{
|
||||||
settings_ai_entries, settings_appearance_entries, settings_download_entries,
|
settings_accent_entries, settings_ai_entries, settings_appearance_entries,
|
||||||
settings_general_entries, settings_java_entries,
|
settings_download_entries, settings_general_entries, settings_java_entries,
|
||||||
};
|
};
|
||||||
use update::update_entries;
|
use update::update_entries;
|
||||||
|
|
||||||
@@ -233,6 +233,9 @@ nav_entries! {
|
|||||||
// M168:中文字体下载状态行(设置页 General 组)。同样只追加在链尾之后——
|
// M168:中文字体下载状态行(设置页 General 组)。同样只追加在链尾之后——
|
||||||
// `catalog/font.rs` 那四条老文案在链中间,往那里追加会平移其后所有 key(金表整片红)。
|
// `catalog/font.rs` 那四条老文案在链中间,往那里追加会平移其后所有 key(金表整片红)。
|
||||||
font_install_entries,
|
font_install_entries,
|
||||||
|
// M250:强调色一族(设置页 Appearance 组的第一个控件)。同理追加在链尾——`appearance`
|
||||||
|
// 那个域在链中间,往那里追加会平移其后所有 key(金表整片红)。
|
||||||
|
settings_accent_entries,
|
||||||
finish,
|
finish,
|
||||||
{}
|
{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,4 +233,11 @@ pub(super) const ROWS: &[(&str, &str, &str)] = &[
|
|||||||
("FontCjkStatusUnknown", "Not determined yet; nothing was downloaded", "尚未判定;没有下载任何字体"),
|
("FontCjkStatusUnknown", "Not determined yet; nothing was downloaded", "尚未判定;没有下载任何字体"),
|
||||||
("FontCjkStatusUnprobeable", "System fonts cannot be probed, so nothing was downloaded", "无法探测系统字体,因此没有下载"),
|
("FontCjkStatusUnprobeable", "System fonts cannot be probed, so nothing was downloaded", "无法探测系统字体,因此没有下载"),
|
||||||
("FontCjkStatusHint", "Downloaded once into the data directory and cached; the next start does not download it again. A mirror can be set with ui.font_cdn.", "只下这一次,缓存在数据目录里,下次启动不重下。镜像可用配置键 ui.font_cdn 指定。"),
|
("FontCjkStatusHint", "Downloaded once into the data directory and cached; the next start does not download it again. A mirror can be set with ui.font_cdn.", "只下这一次,缓存在数据目录里,下次启动不重下。镜像可用配置键 ui.font_cdn 指定。"),
|
||||||
|
("SettingsAppearanceAccent", "Accent colour", "强调色"),
|
||||||
|
("SettingsAppearanceAccentPresets", "Presets", "预设"),
|
||||||
|
("SettingsAppearanceAccentFactory", "Factory", "出厂"),
|
||||||
|
("SettingsAppearanceAccentHint", "Empty = the factory accent from the brand board. A custom accent replaces primary / secondary / tertiary only; neutral surfaces and error colours do not follow it.", "空 = 出厂品牌板的强调色。自定义强调色只替换 primary / secondary / tertiary 三族;中性表面与错误色不随强调色变。"),
|
||||||
|
("SettingsAppearanceAccentShort", "Accent needs 6 hex digits (RRGGBB); got {digits}.", "强调色要 6 位十六进制(RRGGBB),现在 {digits} 位。"),
|
||||||
|
("SettingsAppearanceAccentLong", "Accent has {digits} digits; a colour takes exactly 6.", "强调色有 {digits} 位,一个颜色只要 6 位。"),
|
||||||
|
("SettingsAppearanceAccentDigit", "Accent contains a character that is not a hex digit: {ch}", "强调色里有非十六进制字符:{ch}"),
|
||||||
];
|
];
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
//! 设置页「Appearance」分组**强调色**一族的文案(M250 ACCENTWIRE)。
|
||||||
|
//!
|
||||||
|
//! ## 为什么单开一个域文件,而不是写进 `settings/appearance.rs`
|
||||||
|
//!
|
||||||
|
//! `settings_appearance_entries` 在装配链**中间**。往那个域后面追加,会把其后所有 key 在
|
||||||
|
//! `Key::ALL` 里整体平移 ⇒ 金表(逐 index 逐字节)整片红。本域挂在**链尾**
|
||||||
|
//! (`font_install_entries` 之后),于是既有顺序一个不动,金表只需在 `tail2::ROWS` 末尾追加
|
||||||
|
//! (M104 / M106 / M107 / M109 / M114 / M159 / M168 都是这条做法)。
|
||||||
|
//!
|
||||||
|
//! key 前缀仍叫 `SettingsAppearanceAccent*`:`catalog/settings/appearance.rs` 的译文守卫按
|
||||||
|
//! **名字前缀**筛 key,改名会让那两条守卫漏掉这一族。
|
||||||
|
//!
|
||||||
|
//! ## 三档失败原因为什么分开写
|
||||||
|
//!
|
||||||
|
//! `#12`(位数不足)、`#006A5C00`(位数过多)、`zzzzzz`(非十六进制)是三种不同的手误,
|
||||||
|
//! 而「格式不对」对用户没有行动性。分开写之后错行能直接说「现在 2 位」「有非十六进制字符 z」。
|
||||||
|
//!
|
||||||
|
//! 域文件的形状见 [`crate::catalog`] 的「域装配」一节。
|
||||||
|
|
||||||
|
macro_rules! settings_accent_entries {
|
||||||
|
($next:ident, $($rest:ident,)* { $($acc:tt)* }) => {
|
||||||
|
$next! { $($rest,)* {
|
||||||
|
$($acc)*
|
||||||
|
// ── 强调色(M250:设置页 Appearance 组的第一个控件)
|
||||||
|
SettingsAppearanceAccent => ("Accent colour", "强调色"),
|
||||||
|
SettingsAppearanceAccentPresets => ("Presets", "预设"),
|
||||||
|
SettingsAppearanceAccentFactory => ("Factory", "出厂"),
|
||||||
|
SettingsAppearanceAccentHint => (
|
||||||
|
"Empty = the factory accent from the brand board. A custom accent replaces primary / secondary / tertiary only; neutral surfaces and error colours do not follow it.",
|
||||||
|
"空 = 出厂品牌板的强调色。自定义强调色只替换 primary / secondary / tertiary 三族;中性表面与错误色不随强调色变。"
|
||||||
|
),
|
||||||
|
SettingsAppearanceAccentShort => (
|
||||||
|
"Accent needs 6 hex digits (RRGGBB); got {digits}.",
|
||||||
|
"强调色要 6 位十六进制(RRGGBB),现在 {digits} 位。"
|
||||||
|
),
|
||||||
|
SettingsAppearanceAccentLong => (
|
||||||
|
"Accent has {digits} digits; a colour takes exactly 6.",
|
||||||
|
"强调色有 {digits} 位,一个颜色只要 6 位。"
|
||||||
|
),
|
||||||
|
SettingsAppearanceAccentDigit => (
|
||||||
|
"Accent contains a character that is not a hex digit: {ch}",
|
||||||
|
"强调色里有非十六进制字符:{ch}"
|
||||||
|
),
|
||||||
|
} }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) use settings_accent_entries;
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
//! | 文件 | 分组(`pages/settings` 的 `GROUPS`) | 现状 |
|
//! | 文件 | 分组(`pages/settings` 的 `GROUPS`) | 现状 |
|
||||||
//! |---|---|---|
|
//! |---|---|---|
|
||||||
//! | [`general`] | General(Language / Theme / Startup page / Close button) | 2 条 key(语言) |
|
//! | [`general`] | General(Language / Theme / Startup page / Close button) | 2 条 key(语言) |
|
||||||
|
//! | [`accent`] | Appearance 组的**强调色**一族(M250) | 7 条 key(标签 / 预设 / 三档失败原因 / 说明) |
|
||||||
//! | [`appearance`] | Appearance(Density / Corner style / Motion / Text scale) | 预留,暂无 key |
|
//! | [`appearance`] | Appearance(Density / Corner style / Motion / Text scale) | 预留,暂无 key |
|
||||||
//! | [`java`] | Java(Runtime / Default memory / JVM arguments) | 预留,暂无 key |
|
//! | [`java`] | Java(Runtime / Default memory / JVM arguments) | 预留,暂无 key |
|
||||||
//! | [`download`] | Downloads(Preferred source / Concurrency / Verify checksums) | 预留,暂无 key |
|
//! | [`download`] | Downloads(Preferred source / Concurrency / Verify checksums) | 预留,暂无 key |
|
||||||
@@ -15,12 +16,14 @@
|
|||||||
//! 「预留」的文件里是**空转移器**(只把累积表原样交给下一个域):加 key 时把条目写进它
|
//! 「预留」的文件里是**空转移器**(只把累积表原样交给下一个域):加 key 时把条目写进它
|
||||||
//! `$($acc)*` 后面即可——签名不用改,`catalog.rs` 不用碰,跨线零冲突。
|
//! `$($acc)*` 后面即可——签名不用改,`catalog.rs` 不用碰,跨线零冲突。
|
||||||
|
|
||||||
|
mod accent;
|
||||||
mod ai;
|
mod ai;
|
||||||
mod appearance;
|
mod appearance;
|
||||||
mod download;
|
mod download;
|
||||||
mod general;
|
mod general;
|
||||||
mod java;
|
mod java;
|
||||||
|
|
||||||
|
pub(crate) use accent::settings_accent_entries;
|
||||||
pub(crate) use ai::settings_ai_entries;
|
pub(crate) use ai::settings_ai_entries;
|
||||||
pub(crate) use appearance::settings_appearance_entries;
|
pub(crate) use appearance::settings_appearance_entries;
|
||||||
pub(crate) use download::settings_download_entries;
|
pub(crate) use download::settings_download_entries;
|
||||||
|
|||||||
@@ -39,6 +39,72 @@ pub const ACCENT_PRESETS: [&str; 8] = [
|
|||||||
"#0078D4", "#0F6CBD", "#107C10", "#9D5D00", "#C42B1C", "#8764B8", "#C239B3", "#00B7C3",
|
"#0078D4", "#0F6CBD", "#107C10", "#9D5D00", "#C42B1C", "#8764B8", "#C239B3", "#00B7C3",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// 强调色输入的**结构化失败原因**(M250 ACCENTWIRE)。
|
||||||
|
///
|
||||||
|
/// 为什么不是 `bool`:接线之后「填坏的串」必须能变成一条看得见的原因,回退成 `false` 只会让
|
||||||
|
/// 用户再体验一次「保存成功、界面没变」。空串**不在这里**:空 = 出厂品牌板(合法取值)。
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum AccentError {
|
||||||
|
/// 去掉 `#` 后不足 6 位(`#12`、`#006A5`)。
|
||||||
|
Short {
|
||||||
|
/// 实际位数(原样报给用户,别把「6 位」写死在句子里)。
|
||||||
|
digits: usize,
|
||||||
|
},
|
||||||
|
/// 去掉 `#` 后超过 6 位(`#006A5C00`、粘了整行的串)。
|
||||||
|
Long {
|
||||||
|
/// 实际位数。
|
||||||
|
digits: usize,
|
||||||
|
},
|
||||||
|
/// 含非十六进制字符(`zzzzzz`)。
|
||||||
|
Digit {
|
||||||
|
/// 第一个不合法字符(比「格式不对」有用:用户能一眼看到错在哪个字符)。
|
||||||
|
ch: char,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 一次强调色输入的解析结果:`appearance.accent_color` 的**唯一**解析点(M250):设置页输入框、
|
||||||
|
/// 配置读回、出图取证开关都走它。少了 `#` 也接受(`006A5C` = `#006A5C`)——core 的 `normalize_hex`
|
||||||
|
/// 同样接受,而 `ull config set appearance.accent_color 006A5C` 写出来的值必须能被界面吃下
|
||||||
|
/// (否则 CLI 设的值会在设置页亮红行、颜色却是生效的)。输出一律规范化为大写 `#RRGGBB`。
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum AccentChoice {
|
||||||
|
/// 空串 = 出厂品牌板(`DesignTokens::brand`)。**不是错误**。
|
||||||
|
Factory,
|
||||||
|
/// 合法的 `#RRGGBB`(大写、带 `#`)。
|
||||||
|
Custom(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccentChoice {
|
||||||
|
/// 解析一段用户输入 / 配置值。
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// 位数不是 6 或含非十六进制字符时返回 [`AccentError`];空串是 `Ok(Factory)`。
|
||||||
|
pub fn parse(text: &str) -> Result<Self, AccentError> {
|
||||||
|
let trimmed = text.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return Ok(Self::Factory);
|
||||||
|
}
|
||||||
|
let body = trimmed.strip_prefix('#').unwrap_or(trimmed);
|
||||||
|
if let Some(ch) = body.chars().find(|ch| !ch.is_ascii_hexdigit()) {
|
||||||
|
return Err(AccentError::Digit { ch });
|
||||||
|
}
|
||||||
|
match body.chars().count() {
|
||||||
|
digits @ 0..=5 => Err(AccentError::Short { digits }),
|
||||||
|
6 => Ok(Self::Custom(format!("#{}", body.to_ascii_uppercase()))),
|
||||||
|
digits => Err(AccentError::Long { digits }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 落盘字符串(`""` = 出厂;`#RRGGBB` = 自定义)。
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Factory => "",
|
||||||
|
Self::Custom(hex) => hex,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 磁贴尺寸(Q24-3:小 6 列 / 中 4 列 / 大 3 列)。
|
/// 磁贴尺寸(Q24-3:小 6 列 / 中 4 列 / 大 3 列)。
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
pub enum TileSize {
|
pub enum TileSize {
|
||||||
@@ -158,22 +224,21 @@ impl AppearancePrefs {
|
|||||||
self.bg_image_enabled && !self.bg_image.trim().is_empty()
|
self.bg_image_enabled && !self.bg_image.trim().is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 规范化写入强调色:`#RRGGBB` 才接受(空串 = 恢复出厂)。
|
/// 规范化写入强调色:`#RRGGBB` 才接受(空串 = 恢复出厂);返回 `false` = 输入不合法、
|
||||||
///
|
/// 现状不变。需要**原因**时走 [`AppearancePrefs::set_accent_checked`](设置页走那条)。
|
||||||
/// 返回 `false` 表示输入不合法(调用方据此亮设置页的 warn 行,不改现状)。
|
|
||||||
pub fn set_accent(&mut self, text: &str) -> bool {
|
pub fn set_accent(&mut self, text: &str) -> bool {
|
||||||
let trimmed = text.trim();
|
self.set_accent_checked(text).is_ok()
|
||||||
if trimmed.is_empty() {
|
|
||||||
self.accent_color.clear();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
match unlucky_core::setting::appearance::normalize_hex(trimmed) {
|
|
||||||
Some(hex) => {
|
|
||||||
self.accent_color = hex;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
None => false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 与 [`AppearancePrefs::set_accent`] 同一条路,但把结构化原因交出去。失败时**不动**
|
||||||
|
/// `accent_color`(保持上一次合法值):非法串既不落盘、也不改生效色,只在 UI 上留一条原因。
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// 位数不是 6 或含非十六进制字符时返回 [`AccentError`](见 [`AccentChoice::parse`])。
|
||||||
|
pub fn set_accent_checked(&mut self, text: &str) -> Result<(), AccentError> {
|
||||||
|
let choice = AccentChoice::parse(text)?;
|
||||||
|
self.accent_color = choice.as_str().to_owned();
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 用预设色板第 `index` 个颜色(越界忽略)。
|
/// 用预设色板第 `index` 个颜色(越界忽略)。
|
||||||
|
|||||||
Reference in new issue
Block a user