//! 强调色的**运行期状态**(M250 ACCENTWIRE):输入原文 → 解析结果 → 生效方案。 //! //! 这一层存在的理由:`appearance.accent_color` 此前是死线——键、输入框、presenter 往返都在, //! 但造 tokens 的唯一入口恒走 `DesignTokens::brand(board)`,用户填了色界面没变化。接线要落地 //! 的不只是「把 hex 喂进 `Scheme::from_accent`」,还有**失败要可见**: //! //! | 输入 | 生效配色 | `error` | 落盘值 | //! |---|---|---|---| //! | 空串 | 出厂品牌板(`DesignTokens::brand`) | `None` | `""` | //! | `006A5C`(青绿) | `DesignTokens::with_accent`(只换三族) | `None` | `#` + `006A5C` | //! | `#12` / `zzzzzz` / 超长串 | **保持上一次合法的值**(不静默回退成出厂色) | `Some(..)` | 上一次合法的值 | //! //! 「保持上一次合法值」而不是「回退出厂板」是刻意的:回退会让「填错」和「填对但没生效」在屏幕上 //! 长得一模一样,用户唯一的线索就只剩那条错行——而那正是这里要保证不被丢掉的。 //! //! 解析本身不在这里:`AccentChoice::parse` 是唯一真相(`unlucky_presenter::prefs`),本文件只负责 //! 把结果接到 `Scheme` 与 UI 上。 use unlucky_design::color::{Color, Scheme, SchemeFlavor}; use unlucky_presenter::prefs::{AccentChoice, AccentError}; use crate::theme::accent_scheme; /// 出图矩阵的强调色开关(`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, /// 非法输入的结构化原因(`None` = 没填或填得合法)。 error: Option, /// 合法自定义色时的方案(`Ui` 直接用它;`None` = 走出厂品牌板)。 scheme: Option, } impl Accent { /// 从配置读一份(启动时调一次)。配置里存着非法值时**也**亮错行,不静默吞掉。 #[must_use] pub fn new(text: &str, flavor: SchemeFlavor) -> Self { let mut accent = Self { text: text.to_owned(), choice: AccentChoice::Factory, color: None, error: None, scheme: None, }; accent.retry(flavor); accent } /// 设置页改了一次输入(受控输入框:每敲一个字符都走这里)。 pub fn set(&mut self, text: &str, flavor: SchemeFlavor) { self.text = text.to_owned(); self.retry(flavor); } /// 配置被外部改写后重置(热切:CLI / AI 改完 1 秒内生效)。 pub fn reset(&mut self, text: &str, flavor: SchemeFlavor) { *self = Self::new(text, flavor); } /// 深浅切了:强调族要按新模式重派生(表面色仍由 `State::refresh_palette` 那条路管)。 pub fn set_flavor(&mut self, flavor: SchemeFlavor) { self.scheme = self.color.map(|accent| accent_scheme(flavor, accent)); } /// 输入框原文。 #[must_use] pub fn text(&self) -> &str { &self.text } /// 结构化失败原因(`None` = 合法)。 #[must_use] pub fn error(&self) -> Option { self.error } /// 解析出来的强调色(`None` = 出厂板;日志与截图判据读它)。 #[must_use] pub fn color(&self) -> Option { self.color } /// 生效的强调色方案(`Ui` 的唯一入口:`Some` ⇒ 走 [`crate::theme::Ui::with_accent`] 的同一条路)。 #[must_use] pub fn scheme(&self) -> Option<&Scheme> { self.scheme.as_ref() } /// 落盘值:`""` = 出厂,`#RRGGBB` = 自定义。**永远是上一次合法值**——非法手输不写盘。 #[must_use] pub fn persist_value(&self) -> &str { self.choice.as_str() } /// 取证日志里的一个词:`factory` / 规范化的 hex / `invalid(Short { digits: 2 })`。 /// /// 为什么日志要带这个:截图与真机复现只能靠一行文字分辨「强调色没生效」与「强调色生效了但 /// 图上看不出来」——后者是这条 mission 的验收判据,前者是它的 bug。 #[must_use] pub fn log_token(&self) -> String { match self.error { Some(error) => format!("invalid({error:?})"), None if self.choice.as_str().is_empty() => "factory".to_owned(), None => self.persist_value().to_owned(), } } /// 按当前 `text` 重算:合法 ⇒ 换方案 + 清错;非法 ⇒ **方案不动** + 记错。 fn retry(&mut self, flavor: SchemeFlavor) { match AccentChoice::parse(&self.text) { Ok(choice) => { self.color = Color::from_hex(choice.as_str()); self.scheme = self.color.map(|accent| accent_scheme(flavor, accent)); self.choice = choice; self.error = None; } Err(error) => self.error = Some(error), } } }