- 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 出图逐字节与重构前相同(规则搬迁没改像素)。
233 lines
10 KiB
Rust
233 lines
10 KiB
Rust
//! 主题与 `Ui` 的重算口径(从 `state.rs` 搬出来守 400 行硬线;M119 拆,纯搬不改)。
|
||
//!
|
||
//! 三件事在这里:①`Ui` 什么时候重建([`App::rebuild_ui`])②主题档怎么定
|
||
//! ([`App::set_flavor`] / [`App::apply_config_theme`])③读当前求值器([`App::ui`])。
|
||
//!
|
||
//! 截图通道的判据 [`App::has_screenshot`] 也放在这儿:上面三条里两条都要「截图不读用户配置」。
|
||
|
||
use crate::app::diag::diag;
|
||
use crate::app::message::Message;
|
||
use iced::Task;
|
||
use unlucky_design::color::SchemeFlavor;
|
||
use unlucky_presenter::prefs::ThemeMode;
|
||
|
||
use crate::theme::Ui;
|
||
|
||
use super::App;
|
||
|
||
impl App {
|
||
/// 本次进程是否走截图通道。
|
||
pub(in crate::app) fn has_screenshot(&self) -> bool {
|
||
self.options.screenshot.is_some()
|
||
}
|
||
|
||
/// 切主题:重建 [`Ui`](= 重建 Style),**不动任何 UI 状态**。
|
||
///
|
||
/// M93 起 `Ui` 有两个来源:配色来源取到了图(①②级)就用解析出的方案,否则用令牌 baseline。
|
||
/// 所以这里改叫「重算 `Ui`」:先让外观状态记住新深浅(配色解析要它),再重建。
|
||
pub(in crate::app) fn set_flavor(&mut self, flavor: SchemeFlavor) {
|
||
if self.ui.flavor() == flavor && self.appearance.flavor() == flavor {
|
||
return;
|
||
}
|
||
|
||
self.appearance.set_flavor(flavor);
|
||
let previous = self.ui.name();
|
||
self.rebuild_ui();
|
||
diag!(
|
||
"[theme] {} -> {} (Theme::name 变化触发缓存重算)",
|
||
previous,
|
||
self.ui.name()
|
||
);
|
||
}
|
||
|
||
/// 按「强调色 / 配色来源是否接管」重建 [`Ui`]。
|
||
///
|
||
/// **优先级(M250):显式强调色 > D38 取色方案 > 出厂品牌板**。手填的色是直接意图,取色是从
|
||
/// 图片派生的,所以强调色只在用户真的填了合法值时压过取色——两条路都只决定「颜色来自哪里」,
|
||
/// `flavor`(明暗 / 跟随系统)仍由 `self.appearance.flavor()` 决定。
|
||
///
|
||
/// 色卡档(③)刻意继续走 `Scheme::baseline`:`resolve_source` 的色卡档返回的是
|
||
/// `DEFAULT_CARD`(天蓝卡)方案,与令牌 baseline 逐色不同,应用它会把**全部既有验收图的
|
||
/// 像素**改掉。这条偏离登记在 `pages::appearance` 的偏离表 #4。
|
||
pub(in crate::app) fn rebuild_ui(&mut self) {
|
||
// 面板 α(D36)在这里注入 `Ui`:全仓所有表面色都从 `Ui::layer` 出去,所以这是**唯一**
|
||
// 生效点(外壳 / 三页卡片 / 外观分组卡片 / 空态图标块自动同源,调用点没有机会各处自乘)。
|
||
let panel_alpha = self.appearance.effective_panel_alpha();
|
||
// 三选一规则只在 `theme::tokens` 里写一遍:出图 harness 也调它(两头各抄一份迟早
|
||
// 出现「图上有强调色、产品里没有」,而那种偏差在图上看不出来)。
|
||
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!(
|
||
"[appearance] palette={} → Ui::from_scheme(取色方案接管 49 个角色)",
|
||
self.appearance.palette_level()
|
||
);
|
||
}
|
||
crate::theme::Colors::Board => {
|
||
// 非法值:**保持出厂板**,但错行留在设置页(不静默),日志里也留一条。
|
||
if let Some(error) = self.appearance.accent.error() {
|
||
diag!("[appearance] accent 非法({error:?})→ 出厂板,错行见设置页");
|
||
}
|
||
}
|
||
}
|
||
self.ui = ui.with_panel_alpha(panel_alpha);
|
||
}
|
||
|
||
/// 按设置文件里的主题模式切一次外观;`--theme` 显式给了就不动。
|
||
///
|
||
/// **截图通道直接跳过**:`--screenshot` 的主题唯一取值口是 `Options::capture_flavor`
|
||
/// (D30 可复现),读本机 `launcher.json` 会让同一份代码在两台机器上出不同的图。
|
||
///
|
||
/// `ThemeMode::Auto` 不在这里处理:那要看系统主题(`iced::system::theme()` 的结果在
|
||
/// `Message::SystemTheme` 里落地)。
|
||
pub(in crate::app) fn apply_config_theme(&mut self) {
|
||
if self.has_screenshot() || self.options.theme.is_some() {
|
||
return;
|
||
}
|
||
let mode = self.bridge.load().1;
|
||
self.theme_mode = mode;
|
||
// M229:设置页那一行画的是这个字段(唯一写者见 [`App::set_theme_mode`])。
|
||
self.settings.language_appearance.theme = mode;
|
||
match mode {
|
||
ThemeMode::Light => self.set_flavor(SchemeFlavor::Light),
|
||
ThemeMode::Dark => self.set_flavor(SchemeFlavor::Dark),
|
||
ThemeMode::Auto => {}
|
||
}
|
||
}
|
||
|
||
/// 换主题档(M229:设置页「通用 → 主题」→ `pages::Message::Theme`)。
|
||
///
|
||
/// 四件事:①改 `theme_mode`(唯一真相)②同步设置页那一行的档位 ③重建 `Ui`(色板换了,
|
||
/// `Theme::name` 与 49 个角色一起变,iced 才会重算缓存)④落盘 `appearance.theme_mode`。
|
||
/// `Auto` 自己没有颜色,要等系统回答——所以返回 `iced::system::theme()` 那个 Task,
|
||
/// 由既有的 `Message::SystemTheme` 分支落地(它只在 `theme_mode == Auto` 时吃这条消息)。
|
||
pub(in crate::app) fn set_theme_mode(&mut self, mode: ThemeMode) -> Task<Message> {
|
||
self.settings.language_appearance.theme = mode;
|
||
if mode == self.theme_mode {
|
||
return Task::none();
|
||
}
|
||
self.theme_mode = mode;
|
||
|
||
let prefs = self.appearance_prefs.clone();
|
||
self.bridge.persist(&prefs, mode);
|
||
match self.bridge.path() {
|
||
Some(path) => diag!("[theme] mode={} 已写回 {}", mode.as_str(), path.display()),
|
||
None => diag!(
|
||
"[theme] mode={} 未落盘(数据根解析不出来,内存态运行):重启会回到旧值",
|
||
mode.as_str()
|
||
),
|
||
}
|
||
|
||
match mode {
|
||
ThemeMode::Light => {
|
||
self.set_flavor(SchemeFlavor::Light);
|
||
self.after_theme_switch();
|
||
Task::none()
|
||
}
|
||
ThemeMode::Dark => {
|
||
self.set_flavor(SchemeFlavor::Dark);
|
||
self.after_theme_switch();
|
||
Task::none()
|
||
}
|
||
// 跟随系统:当前先按已有深浅画,紧接着由系统主题回答覆盖
|
||
// (`set_flavor` 有「同档不重建」的保护,所以这里不必先猜一次)。
|
||
ThemeMode::Auto => iced::system::theme().map(Message::SystemTheme),
|
||
}
|
||
}
|
||
|
||
/// 换档之后重建视图模型 + 记一行取证日志(色板名就是证据:`ink` / `silver`)。
|
||
fn after_theme_switch(&mut self) {
|
||
self.view_dirty = true;
|
||
self.refresh_view();
|
||
diag!(
|
||
"[theme] mode={} board={} Theme::name={}",
|
||
self.theme_mode().as_str(),
|
||
self.board_name(),
|
||
self.ui.name()
|
||
);
|
||
}
|
||
|
||
/// 当前主题档(设置页那一行的选中态来自 `settings.groups`,这里是取证与测试的读数口)。
|
||
pub(in crate::app) fn theme_mode(&self) -> ThemeMode {
|
||
self.theme_mode
|
||
}
|
||
|
||
/// 当前色板名(日志与测试用:`ink` / `silver`;取色方案接管时仍报它所属深浅的那块)。
|
||
pub(in crate::app) fn board_name(&self) -> &'static str {
|
||
self.ui.board().name()
|
||
}
|
||
|
||
/// 当前令牌求值器(测试与截图日志用)。
|
||
pub(in crate::app) fn ui(&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,
|
||
"强调色只换颜色来源,不改明暗"
|
||
);
|
||
}
|
||
}
|