fix(app-iced): 复审 r1 的四条 p2 + 三条小事(M250)
- p2-1 删死状态:`Accent::scheme` 字段 / `scheme()` 取值口 / `set_flavor` 重派生全仓零读者,
且注释谎称「Ui 的唯一入口」。色值 `color` 与深浅无关,`Ui` 由 `rebuild_ui` 重算,
所以整条删掉,`new/set/reset` 也不再收 `flavor`
- p2-2 规则只定义一次:新增 `theme::tokens(flavor, accent, scheme, families) -> (Ui, Colors)`,
`App::rebuild_ui` 与出图 harness 都调它(harness 传 scheme=None,如实说明不覆盖 D38 分支);
README 改成「同一条规则、不是同一个调用点」
- p2-3 逐键不闪红:`Accent` 区分「显示中的错」与 `pending`。只有**不可能再变合法**(非 hex / 超 6 位)
才立刻亮;位数不足先挂 pending,等 `commit()`(点到别的外观控件)或配置读回才亮
- p2-4 订正 `app/state.rs:151` 那句反话(强调色现在是 UI 写入字段)
- 小事:hint 文案改「中性表面与错误色不随强调色变」;`Digit{ch}` 里的不可见字符显示成 U+XXXX;
8 颗色块补 tooltip(hex 原文,Overlay 底)
判据:新用例 `accent_errors_only_appear_when_they_are_actionable` 覆盖「输入中不亮 / 提交后亮 /
配置读回立刻亮」三态;gallery 出图逐字节与重构前相同(规则搬迁没改像素)。
This commit is contained in:
1 parent
c609827c49
commit
14282f3325
10 files changed
+261
-78
No files matched your search
@@ -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>,
|
||||||
|
|||||||
@@ -53,36 +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 (
|
// 三选一规则只在 `theme::tokens` 里写一遍:出图 harness 也调它(两头各抄一份迟早
|
||||||
|
// 出现「图上有强调色、产品里没有」,而那种偏差在图上看不出来)。
|
||||||
|
let (ui, colors) = crate::theme::tokens(
|
||||||
|
self.appearance.flavor(),
|
||||||
self.appearance.accent.color(),
|
self.appearance.accent.color(),
|
||||||
self.appearance.derived_scheme(),
|
self.appearance.derived_scheme().copied(),
|
||||||
) {
|
self.families,
|
||||||
(Some(accent), _) => {
|
);
|
||||||
|
match colors {
|
||||||
|
crate::theme::Colors::Accent => {
|
||||||
// M250:一行里同时给「强调色生效了」与「取色方案被谁压住」,两件事都能读出来。
|
// M250:一行里同时给「强调色生效了」与「取色方案被谁压住」,两件事都能读出来。
|
||||||
diag!(
|
diag!(
|
||||||
"[appearance] accent={} → Ui::with_accent(只换 primary/secondary/tertiary;\
|
"[appearance] accent={} → Ui::with_accent(只换 primary/secondary/tertiary;取色方案 palette={} 被压住)",
|
||||||
取色方案 palette={} 被显式强调色压住)",
|
|
||||||
self.appearance.accent.log_token(),
|
self.appearance.accent.log_token(),
|
||||||
self.appearance.palette_level()
|
self.appearance.palette_level()
|
||||||
);
|
);
|
||||||
Ui::with_accent(self.appearance.flavor(), accent, self.families)
|
|
||||||
}
|
}
|
||||||
(None, Some(scheme)) => {
|
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, None) => {
|
crate::theme::Colors::Board => {
|
||||||
// 非法值:**保持出厂板**,但错行留在设置页(不静默),日志里也留一条。
|
// 非法值:**保持出厂板**,但错行留在设置页(不静默),日志里也留一条。
|
||||||
if let Some(error) = self.appearance.accent.error() {
|
if let Some(error) = self.appearance.accent.error() {
|
||||||
diag!("[appearance] accent 非法({error:?})→ 出厂板,错行见设置页");
|
diag!("[appearance] accent 非法({error:?})→ 出厂板,错行见设置页");
|
||||||
}
|
}
|
||||||
Ui::new(self.appearance.flavor(), self.families)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.with_panel_alpha(panel_alpha);
|
self.ui = ui.with_panel_alpha(panel_alpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 按设置文件里的主题模式切一次外观;`--theme` 显式给了就不动。
|
/// 按设置文件里的主题模式切一次外观;`--theme` 显式给了就不动。
|
||||||
|
|||||||
@@ -1,26 +1,36 @@
|
|||||||
//! 强调色的**运行期状态**(M250 ACCENTWIRE):输入原文 → 解析结果 → 生效方案。
|
//! 强调色的**运行期状态**(M250 ACCENTWIRE):输入原文 → 解析结果 → 生效色。
|
||||||
//!
|
//!
|
||||||
//! 这一层存在的理由:`appearance.accent_color` 此前是死线——键、输入框、presenter 往返都在,
|
//! 这一层存在的理由:`appearance.accent_color` 此前是死线——键、输入框、presenter 往返都在,
|
||||||
//! 但造 tokens 的唯一入口恒走 `DesignTokens::brand(board)`,用户填了色界面没变化。接线要落地
|
//! 但造 tokens 的唯一入口恒走 `DesignTokens::brand(board)`,用户填了色界面没变化。接线要落地
|
||||||
//! 的不只是「把 hex 喂进 `Scheme::from_accent`」,还有**失败要可见**:
|
//! 的不只是「把 hex 喂进 `Scheme::from_accent`」,还有**失败要可见**:
|
||||||
//!
|
//!
|
||||||
//! | 输入 | 生效配色 | `error` | 落盘值 |
|
//! | 输入 | 生效配色 | `error()` | 落盘值 |
|
||||||
//! |---|---|---|---|
|
//! |---|---|---|---|
|
||||||
//! | 空串 | 出厂品牌板(`DesignTokens::brand`) | `None` | `""` |
|
//! | 空串 | 出厂品牌板(`DesignTokens::brand`) | `None` | `""` |
|
||||||
//! | `006A5C`(青绿) | `DesignTokens::with_accent`(只换三族) | `None` | `#` + `006A5C` |
|
//! | `006A5C` | `DesignTokens::with_accent`(只换三族) | `None` | `#` + `006A5C` |
|
||||||
//! | `#12` / `zzzzzz` / 超长串 | **保持上一次合法的值**(不静默回退成出厂色) | `Some(..)` | 上一次合法的值 |
|
//! | `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`),本文件只负责
|
//! 解析本身不在这里:`AccentChoice::parse` 是唯一真相(`unlucky_presenter::prefs`),本文件只负责
|
||||||
//! 把结果接到 `Scheme` 与 UI 上。
|
//! 把结果接到色值与 UI 上。
|
||||||
|
|
||||||
use unlucky_design::color::{Color, Scheme, SchemeFlavor};
|
use unlucky_design::color::Color;
|
||||||
use unlucky_presenter::prefs::{AccentChoice, AccentError};
|
use unlucky_presenter::prefs::{AccentChoice, AccentError};
|
||||||
|
|
||||||
use crate::theme::accent_scheme;
|
|
||||||
|
|
||||||
/// 出图矩阵的强调色开关(`ULL_APPEARANCE_ACCENT=#RRGGBB`;产品路径读 `launcher.json`)。
|
/// 出图矩阵的强调色开关(`ULL_APPEARANCE_ACCENT=#RRGGBB`;产品路径读 `launcher.json`)。
|
||||||
///
|
///
|
||||||
/// 放在本文件而不是 `state.rs` 的 `ENV_*` 那一族:`state.rs` 正好 400 行(硬线),这一条
|
/// 放在本文件而不是 `state.rs` 的 `ENV_*` 那一族:`state.rs` 正好 400 行(硬线),这一条
|
||||||
@@ -36,41 +46,45 @@ pub struct Accent {
|
|||||||
choice: AccentChoice,
|
choice: AccentChoice,
|
||||||
/// 解析出来的强调色(`None` = 出厂板,强调色由品牌色板决定)。
|
/// 解析出来的强调色(`None` = 出厂板,强调色由品牌色板决定)。
|
||||||
color: Option<Color>,
|
color: Option<Color>,
|
||||||
/// 非法输入的结构化原因(`None` = 没填或填得合法)。
|
/// **现在要显示**的结构化原因(`None` = 不显示)。
|
||||||
error: Option<AccentError>,
|
error: Option<AccentError>,
|
||||||
/// 合法自定义色时的方案(`Ui` 直接用它;`None` = 走出厂品牌板)。
|
/// 已经打错、但「还可能被打字修好」的原因(输入中暂不显示,[`Accent::commit`] 才亮出来)。
|
||||||
scheme: Option<Scheme>,
|
pending: Option<AccentError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Accent {
|
impl Accent {
|
||||||
/// 从配置读一份(启动时调一次)。配置里存着非法值时**也**亮错行,不静默吞掉。
|
/// 从配置读一份(启动时调一次)。配置里存着非法值时**当场**亮错行,不静默吞掉。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(text: &str, flavor: SchemeFlavor) -> Self {
|
pub fn new(text: &str) -> Self {
|
||||||
let mut accent = Self {
|
let mut accent = Self {
|
||||||
text: text.to_owned(),
|
text: text.to_owned(),
|
||||||
choice: AccentChoice::Factory,
|
choice: AccentChoice::Factory,
|
||||||
color: None,
|
color: None,
|
||||||
error: None,
|
error: None,
|
||||||
scheme: None,
|
pending: None,
|
||||||
};
|
};
|
||||||
accent.retry(flavor);
|
accent.retry(true);
|
||||||
accent
|
accent
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 设置页改了一次输入(受控输入框:每敲一个字符都走这里)。
|
/// 设置页改了一次输入(受控输入框:每敲一个字符都走这里)。
|
||||||
pub fn set(&mut self, text: &str, flavor: SchemeFlavor) {
|
///
|
||||||
|
/// 「短但还可能是合法前缀」的输入只挂进 `pending`,不亮红(见模块文档 §何时才算「错」)。
|
||||||
|
pub fn set(&mut self, text: &str) {
|
||||||
self.text = text.to_owned();
|
self.text = text.to_owned();
|
||||||
self.retry(flavor);
|
self.retry(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 配置被外部改写后重置(热切:CLI / AI 改完 1 秒内生效)。
|
/// 配置被外部改写后重置(热切:CLI / AI 改完 1 秒内生效)。
|
||||||
pub fn reset(&mut self, text: &str, flavor: SchemeFlavor) {
|
pub fn reset(&mut self, text: &str) {
|
||||||
*self = Self::new(text, flavor);
|
*self = Self::new(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 深浅切了:强调族要按新模式重派生(表面色仍由 `State::refresh_palette` 那条路管)。
|
/// 提交:把「输入中暂不报」的错亮出来。离开这一格就该走它(见 [`Accent::pending`])。
|
||||||
pub fn set_flavor(&mut self, flavor: SchemeFlavor) {
|
pub fn commit(&mut self) {
|
||||||
self.scheme = self.color.map(|accent| accent_scheme(flavor, accent));
|
if self.error.is_none() {
|
||||||
|
self.error = self.pending.take();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 输入框原文。
|
/// 输入框原文。
|
||||||
@@ -79,53 +93,63 @@ impl Accent {
|
|||||||
&self.text
|
&self.text
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 结构化失败原因(`None` = 合法)。
|
/// 要显示的结构化失败原因(`None` = 合法,或还没打完)。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn error(&self) -> Option<AccentError> {
|
pub fn error(&self) -> Option<AccentError> {
|
||||||
self.error
|
self.error
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 解析出来的强调色(`None` = 出厂板;日志与截图判据读它)。
|
/// 解析出来的强调色(`None` = 出厂板;`rebuild_ui` 与出图判据读它)。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn color(&self) -> Option<Color> {
|
pub fn color(&self) -> Option<Color> {
|
||||||
self.color
|
self.color
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 生效的强调色方案(`Ui` 的唯一入口:`Some` ⇒ 走 [`crate::theme::Ui::with_accent`] 的同一条路)。
|
|
||||||
#[must_use]
|
|
||||||
pub fn scheme(&self) -> Option<&Scheme> {
|
|
||||||
self.scheme.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 落盘值:`""` = 出厂,`#RRGGBB` = 自定义。**永远是上一次合法值**——非法手输不写盘。
|
/// 落盘值:`""` = 出厂,`#RRGGBB` = 自定义。**永远是上一次合法值**——非法手输不写盘。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn persist_value(&self) -> &str {
|
pub fn persist_value(&self) -> &str {
|
||||||
self.choice.as_str()
|
self.choice.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 取证日志里的一个词:`factory` / 规范化的 hex / `invalid(Short { digits: 2 })`。
|
/// 取证日志里的一个词:`factory` / 规范化的 hex / `invalid(..)` / `pending(..)`。
|
||||||
///
|
///
|
||||||
/// 为什么日志要带这个:截图与真机复现只能靠一行文字分辨「强调色没生效」与「强调色生效了但
|
/// 为什么日志要带这个:截图与真机复现只能靠一行文字分辨「强调色没生效」与「强调色生效了但
|
||||||
/// 图上看不出来」——后者是这条 mission 的验收判据,前者是它的 bug。
|
/// 图上看不出来」——后者是这条 mission 的验收判据,前者是它的 bug。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn log_token(&self) -> String {
|
pub fn log_token(&self) -> String {
|
||||||
match self.error {
|
if let Some(error) = self.error {
|
||||||
Some(error) => format!("invalid({error:?})"),
|
return format!("invalid({error:?})");
|
||||||
None if self.choice.as_str().is_empty() => "factory".to_owned(),
|
}
|
||||||
None => self.persist_value().to_owned(),
|
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` 重算:合法 ⇒ 换方案 + 清错;非法 ⇒ **方案不动** + 记错。
|
/// 按当前 `text` 重算:合法 ⇒ 换色 + 清错;非法 ⇒ **色不动**,按 `show` 决定报不报。
|
||||||
fn retry(&mut self, flavor: SchemeFlavor) {
|
fn retry(&mut self, show: bool) {
|
||||||
match AccentChoice::parse(&self.text) {
|
match AccentChoice::parse(&self.text) {
|
||||||
Ok(choice) => {
|
Ok(choice) => {
|
||||||
self.color = Color::from_hex(choice.as_str());
|
self.color = Color::from_hex(choice.as_str());
|
||||||
self.scheme = self.color.map(|accent| accent_scheme(flavor, accent));
|
|
||||||
self.choice = choice;
|
self.choice = choice;
|
||||||
self.error = None;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(error) => self.error = Some(error),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
//! 颜色本身。所以色块 = `ShapeScale::Full` 的实心圆 + 描边;选中态加粗描边(不靠色相区分:
|
//! 颜色本身。所以色块 = `ShapeScale::Full` 的实心圆 + 描边;选中态加粗描边(不靠色相区分:
|
||||||
//! 色块底下垫的是它自己的颜色,色相对比不可靠)。
|
//! 色块底下垫的是它自己的颜色,色相对比不可靠)。
|
||||||
|
|
||||||
use iced::widget::{button, column, container, row, Space as Gap};
|
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 iced::{Background, Border, Element, Length, Padding, Shadow, Theme as IcedTheme};
|
||||||
|
|
||||||
use unlucky_design::color::Role;
|
use unlucky_design::color::Role;
|
||||||
@@ -129,7 +130,7 @@ fn swatch<'a>(ui: &'a Ui, hex: &'static str, selected: bool) -> Element<'a, Mess
|
|||||||
let width = if selected { STROKE_PX * 2.0 } else { STROKE_PX };
|
let width = if selected { STROKE_PX * 2.0 } else { STROKE_PX };
|
||||||
let radius = ui.radius(ShapeScale::Full, diameter);
|
let radius = ui.radius(ShapeScale::Full, diameter);
|
||||||
|
|
||||||
button(
|
let dot: Element<'a, Message> = button(
|
||||||
Gap::new()
|
Gap::new()
|
||||||
.width(Length::Fixed(diameter))
|
.width(Length::Fixed(diameter))
|
||||||
.height(Length::Fixed(diameter)),
|
.height(Length::Fixed(diameter)),
|
||||||
@@ -150,7 +151,32 @@ fn swatch<'a>(ui: &'a Ui, hex: &'static str, selected: bool) -> Element<'a, Mess
|
|||||||
snap: true,
|
snap: true,
|
||||||
})
|
})
|
||||||
.on_press(Message::Appearance(Change::Accent(hex.to_owned())))
|
.on_press(Message::Appearance(Change::Accent(hex.to_owned())))
|
||||||
.into()
|
.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}`)。
|
/// 结构化原因 → 一句话(参数由 Rust 侧格式化,词表只认 `{name}`)。
|
||||||
@@ -169,7 +195,20 @@ fn reason(lang: Lang, error: AccentError) -> String {
|
|||||||
AccentError::Digit { ch } => tr(
|
AccentError::Digit { ch } => tr(
|
||||||
lang,
|
lang,
|
||||||
Key::SettingsAppearanceAccentDigit,
|
Key::SettingsAppearanceAccentDigit,
|
||||||
&[("ch", &ch.to_string())],
|
&[("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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,7 +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, flavor),
|
accent: Accent::new(&prefs.accent_color),
|
||||||
backend,
|
backend,
|
||||||
selected_group: super::APPEARANCE_GROUP,
|
selected_group: super::APPEARANCE_GROUP,
|
||||||
image: None,
|
image: None,
|
||||||
@@ -137,7 +137,8 @@ impl State {
|
|||||||
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) {
|
if let Some(value) = std::env::var_os(ENV_ACCENT) {
|
||||||
self.accent.set(&value.to_string_lossy(), self.flavor);
|
// 取证开关 = 「像刚敲进去的」(走输入中规则),不是配置读回。
|
||||||
|
self.accent.set(&value.to_string_lossy());
|
||||||
}
|
}
|
||||||
self.refresh_background();
|
self.refresh_background();
|
||||||
self.refresh_palette();
|
self.refresh_palette();
|
||||||
@@ -225,14 +226,12 @@ 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;
|
||||||
}
|
}
|
||||||
self.flavor = flavor;
|
self.flavor = flavor;
|
||||||
// M250:自定义强调色也要按新模式重派生(否则切深浅后强调族停在旧模式)。
|
|
||||||
self.accent.set_flavor(flavor);
|
|
||||||
self.refresh_palette();
|
self.refresh_palette();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,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;
|
||||||
@@ -285,8 +288,7 @@ impl State {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
Change::Accent(text) => {
|
Change::Accent(text) => {
|
||||||
// M250:非法输入不改生效色(`Accent::set` 保留上一次合法值),只挂原因。
|
self.accent.set(&text);
|
||||||
self.accent.set(&text, self.flavor);
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
Change::SelectGroup(index) => {
|
Change::SelectGroup(index) => {
|
||||||
@@ -298,8 +300,7 @@ impl State {
|
|||||||
|
|
||||||
/// 写回配置用的快照(`SettingsBridge::persist` 覆盖的那 9 个字段)。
|
/// 写回配置用的快照(`SettingsBridge::persist` 覆盖的那 9 个字段)。
|
||||||
///
|
///
|
||||||
/// M250 起 `accent_color` 也在里面:以前这里恒给空串(当时「强调色不在本 mission 管辖内」),
|
/// M250 起 `accent_color` 也在里面(以前恒给空串 ⇒ 设置页改的强调色落不了盘),给的是**上一次合法值**。
|
||||||
/// 而 `persist_appearance` 拿配置里的旧快照做基底 ⇒ 设置页改的强调色落不了盘。现在给**上一次合法值**。
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn current_values(&self) -> AppearancePrefs {
|
pub fn current_values(&self) -> AppearancePrefs {
|
||||||
AppearancePrefs {
|
AppearancePrefs {
|
||||||
@@ -351,7 +352,7 @@ impl State {
|
|||||||
self.bg_dim = prefs.bg_dim;
|
self.bg_dim = prefs.bg_dim;
|
||||||
self.glass = prefs.glass;
|
self.glass = prefs.glass;
|
||||||
// M250:文件被外部改过(CLI / AI)时强调色也要热生效,错行同样重置。
|
// M250:文件被外部改过(CLI / AI)时强调色也要热生效,错行同样重置。
|
||||||
self.accent.reset(&prefs.accent_color, self.flavor);
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,9 +130,11 @@ fn illegal_accent_reports_a_reason_and_never_silently_falls_back() {
|
|||||||
let good_primary = Ui::with_accent(SchemeFlavor::Light, good, FAMILIES).color(Role::Primary);
|
let good_primary = Ui::with_accent(SchemeFlavor::Light, good, FAMILIES).color(Role::Primary);
|
||||||
|
|
||||||
// 超长串由合法值拼出来:`src/**` 的色值字面量门禁把注释与测试也算在内(那条门禁是对的)。
|
// 超长串由合法值拼出来:`src/**` 的色值字面量门禁把注释与测试也算在内(那条门禁是对的)。
|
||||||
|
//
|
||||||
|
// 三档里只有后两档**当场**亮:`#12` 是「还可能是合法前缀」(用户可能正打到一半),它的
|
||||||
|
// 亮灯时机由 `accent_errors_only_appear_when_they_are_actionable` 与 `commit` 覆盖。
|
||||||
let long = format!("{}00112233", TEAL.to_hex());
|
let long = format!("{}00112233", TEAL.to_hex());
|
||||||
for (text, expected) in [
|
for (text, expected) in [
|
||||||
("#12".to_owned(), AccentError::Short { digits: 2 }),
|
|
||||||
("zzzzzz".to_owned(), AccentError::Digit { ch: 'z' }),
|
("zzzzzz".to_owned(), AccentError::Digit { ch: 'z' }),
|
||||||
(long, AccentError::Long { digits: 14 }),
|
(long, AccentError::Long { digits: 14 }),
|
||||||
] {
|
] {
|
||||||
@@ -140,7 +142,7 @@ fn illegal_accent_reports_a_reason_and_never_silently_falls_back() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
state.accent.error(),
|
state.accent.error(),
|
||||||
Some(expected),
|
Some(expected),
|
||||||
"{text}: 失败原因必须是结构化的(不是一句 bool / 一句「格式不对」)"
|
"{text}: 不可能变合法的输入必须当场亮且原因结构化(不是一句 bool / 一句「格式不对」)"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
state.accent.color(),
|
state.accent.color(),
|
||||||
@@ -157,6 +159,17 @@ fn illegal_accent_reports_a_reason_and_never_silently_falls_back() {
|
|||||||
assert_eq!(ui.color(Role::Primary), good_primary);
|
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()));
|
state.apply(Change::Accent(String::new()));
|
||||||
assert_eq!(state.accent.error(), None, "空串不是错误");
|
assert_eq!(state.accent.error(), None, "空串不是错误");
|
||||||
@@ -171,3 +184,71 @@ fn illegal_accent_reports_a_reason_and_never_silently_falls_back() {
|
|||||||
));
|
));
|
||||||
assert_eq!(state.accent.persist_value(), TEAL.to_hex());
|
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};
|
||||||
|
|
||||||
@@ -153,14 +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 {
|
||||||
// M250:gallery 出图也要能看见强调色 —— 与产品路径同一条规则(显式强调色 >
|
ui,
|
||||||
// 出厂板),来源是 `ULL_APPEARANCE_ACCENT`(`State::apply_env` 灌进来的)。
|
|
||||||
ui: match appearance.as_ref().and_then(|state| state.accent.color()) {
|
|
||||||
Some(accent) => Ui::with_accent(boot.flavor, accent, families),
|
|
||||||
None => Ui::new(boot.flavor, families),
|
|
||||||
},
|
|
||||||
model,
|
model,
|
||||||
appearance: appearance.clone(),
|
appearance: appearance.clone(),
|
||||||
form: crate::pages::demo_launch_form(),
|
form: crate::pages::demo_launch_form(),
|
||||||
|
|||||||
@@ -338,6 +338,40 @@ pub fn accent_scheme(flavor: SchemeFlavor, accent: unlucky_design::color::Color)
|
|||||||
unlucky_design::DesignTokens::with_accent(flavor, accent).colors
|
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 {
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ pub(super) const ROWS: &[(&str, &str, &str)] = &[
|
|||||||
("SettingsAppearanceAccent", "Accent colour", "强调色"),
|
("SettingsAppearanceAccent", "Accent colour", "强调色"),
|
||||||
("SettingsAppearanceAccentPresets", "Presets", "预设"),
|
("SettingsAppearanceAccentPresets", "Presets", "预设"),
|
||||||
("SettingsAppearanceAccentFactory", "Factory", "出厂"),
|
("SettingsAppearanceAccentFactory", "Factory", "出厂"),
|
||||||
("SettingsAppearanceAccentHint", "Empty = the factory accent from the brand board. A custom accent replaces primary / secondary / tertiary only; neutral surfaces and error keep the baseline.", "空 = 出厂品牌板的强调色。自定义强调色只替换 primary / secondary / tertiary 三族;中性表面与 error 保持基线。"),
|
("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} 位。"),
|
("SettingsAppearanceAccentShort", "Accent needs 6 hex digits (RRGGBB); got {digits}.", "强调色要 6 位十六进制(RRGGBB),现在 {digits} 位。"),
|
||||||
("SettingsAppearanceAccentLong", "Accent has {digits} digits; a colour takes exactly 6.", "强调色有 {digits} 位,一个颜色只要 6 位。"),
|
("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}"),
|
("SettingsAppearanceAccentDigit", "Accent contains a character that is not a hex digit: {ch}", "强调色里有非十六进制字符:{ch}"),
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ macro_rules! settings_accent_entries {
|
|||||||
SettingsAppearanceAccentPresets => ("Presets", "预设"),
|
SettingsAppearanceAccentPresets => ("Presets", "预设"),
|
||||||
SettingsAppearanceAccentFactory => ("Factory", "出厂"),
|
SettingsAppearanceAccentFactory => ("Factory", "出厂"),
|
||||||
SettingsAppearanceAccentHint => (
|
SettingsAppearanceAccentHint => (
|
||||||
"Empty = the factory accent from the brand board. A custom accent replaces primary / secondary / tertiary only; neutral surfaces and error keep the baseline.",
|
"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 三族;中性表面与 error 保持基线。"
|
"空 = 出厂品牌板的强调色。自定义强调色只替换 primary / secondary / tertiary 三族;中性表面与错误色不随强调色变。"
|
||||||
),
|
),
|
||||||
SettingsAppearanceAccentShort => (
|
SettingsAppearanceAccentShort => (
|
||||||
"Accent needs 6 hex digits (RRGGBB); got {digits}.",
|
"Accent needs 6 hex digits (RRGGBB); got {digits}.",
|
||||||
|
|||||||
Reference in new issue
Block a user