主线六条合入后,四个被我碰过的文件越过 400 行;按模块拆开,只搬不改: - `presenter/host/mod.rs` 421→366:账户刷新/皮肤三段(refresh_accounts / attach_skins / request_skins)搬进 `host/accounts/refresh.rs`(`impl CoreHost`,pub(in crate::host)) - `presenter/state.rs` 410→392:`TileCell` + `grid_positions` 搬进 `state/tiles.rs`, `state.rs` 用 `pub use` 再导出,老路径不变 - `app-iced/app/state.rs` 401→394:账户页重建那一段搬进 `app/view/account_page.rs` 的 `App::rebuild_account_page` - `app-iced/app/update.rs` 407→388:`ULL_START_ROUTE` 取值表搬进 `app/update/boot_env.rs` (纯函数、无状态)
395 lines
21 KiB
Rust
395 lines
21 KiB
Rust
//! 应用状态:**只放字段与构造**,不放消息分支(那是 [`super::update`] 的事)。
|
||
//!
|
||
//! ## presenter 三件(`crates/presenter/src/lib.rs` crate 文档 §4)
|
||
//!
|
||
//! | 字段 | 谁给 | 干什么 |
|
||
//! |---|---|---|
|
||
//! | `backend: Box<dyn Backend>` | [`build_backend`](unlucky_presenter::backend::build_backend) | 派发 [`AppCommand`](unlucky_presenter::dto::AppCommand)、收 [`AppEvent`](unlucky_presenter::dto::AppEvent) |
|
||
//! | `bridge: SettingsBridge` | [`SettingsBridge::open`](unlucky_presenter::settings_bridge::SettingsBridge::open) | 读一次外观/主题,之后每 N 拍问「文件变了吗」 |
|
||
//! | `state: AppState` | [`AppState::new`](unlucky_presenter::state::AppState::new) | **唯一**的状态写入口(`apply` / `navigate`) |
|
||
//!
|
||
//! `view: pages::Model` 是**展示转换的缓存**:presenter 快照 → 页面要画的东西。
|
||
//! 它在**状态真的变了**时重建([`App::view_dirty`] 门控,M90 r2/P2-1 起),不是每拍重建——
|
||
//! 重建要把 N 个实例的名字/版本串克隆一遍,30 Hz 空转没有意义。缓存成字段是为了让
|
||
//! `view(&self)` 能只借 `&self`:iced 的 `view` 签名不允许返回一棵借用局部变量的树。
|
||
|
||
use std::time::Instant;
|
||
|
||
use iced::window;
|
||
|
||
use unlucky_design::color::SchemeFlavor;
|
||
use unlucky_presenter::backend::{build_backend, config_path};
|
||
use unlucky_presenter::dto::Backend;
|
||
use unlucky_presenter::prefs::ThemeMode;
|
||
use unlucky_presenter::settings_bridge::SettingsBridge;
|
||
use unlucky_presenter::state::AppState;
|
||
use unlucky_presenter::toast::ToastKind;
|
||
|
||
use crate::cli::Options;
|
||
use crate::pages;
|
||
use crate::theme::{Families, Ui};
|
||
use crate::ui::lang::LangState;
|
||
|
||
use super::WINDOW_SIZE;
|
||
|
||
/// presenter 的 TICK 节拍(33 ms ≈ 30 fps)——**忙碌态**的节拍。
|
||
///
|
||
/// 与 Slint 版的 `TICK_MS` 同值:`backend.tick(..)` 的时间推进、toast 过期、
|
||
/// `theme::config_poll_due` 的计数都按这个节拍走,两个 UI 版的时间语义因此一致。
|
||
pub(super) const TICK_MS: u64 = 33;
|
||
|
||
/// [`TICK_MS`] 的 `Duration` 形态。
|
||
pub(super) const TICK: std::time::Duration = std::time::Duration::from_millis(TICK_MS);
|
||
|
||
/// **空闲态**的低频心跳(1 Hz,M90 r3 / P2-2)。
|
||
///
|
||
/// 空闲时不能订阅 `window::frames()`:那条订阅会自持重绘循环(实测软件渲染约 500 帧/秒),
|
||
/// 而空闲时唯一需要的时钟是「偶发一次」——外部 CLI / AI 工具改 `launcher.json` 后的热切
|
||
/// (presenter 的 `theme::config_poll_due` 语义是 30 × 33 ms ≈ 1 秒一拍)。
|
||
///
|
||
/// 实现用的是 `Task::perform(async { sleep(1s) })` 的自续链:[`App::heartbeat`]。它每秒只唤醒
|
||
/// 一次、只触发**一帧**重绘,而 `frames()` 是每秒几百帧。为什么不用 `iced::time::every`:
|
||
/// 它挂在 tokio/smol 后端下,本 crate 的执行器是 thread-pool(Cargo.toml 不在 scope)。
|
||
pub(super) const IDLE_TICK: std::time::Duration = std::time::Duration::from_secs(1);
|
||
|
||
/// 派发指令后保持帧订阅的观察窗(见 [`App::watch_until`])。
|
||
///
|
||
/// 3 秒够真 host 把「命令已受理」的第一条事件交回来;之后由「有 Busy/Running 实例」这个条件续订。
|
||
pub(super) const WATCH_WINDOW: std::time::Duration = std::time::Duration::from_secs(3);
|
||
|
||
mod boot_probe;
|
||
|
||
use boot_probe::app_options;
|
||
pub(crate) use boot_probe::{log_first_frame, STARTED};
|
||
|
||
/// 应用状态:UI 状态 + presenter 三件 + 展示转换缓存。
|
||
pub struct App {
|
||
/// 启动参数(截图/主题/画面开关)。
|
||
pub(super) options: Options,
|
||
/// 字体族(切主题时重建 [`Ui`] 要用)。
|
||
pub(super) families: Families,
|
||
/// 令牌求值器(浅/深两套)。
|
||
pub(super) ui: Ui,
|
||
/// 后端通道(真 host 或 mock)。
|
||
pub(super) backend: Box<dyn Backend>,
|
||
/// 设置持久化桥。
|
||
pub(super) bridge: SettingsBridge,
|
||
/// presenter 的全局状态(唯一写入口是 `apply`)。
|
||
pub(super) state: AppState,
|
||
/// 已推进的拍数(`theme::config_poll_due` 的输入)。单位是 **33 ms 一拍**,不是调用次数。
|
||
pub(super) ticks: u64,
|
||
/// 下一次「比对配置文件 mtime」的 tick 号(见 `update::advance` 的注释)。
|
||
pub(super) config_due_at: u64,
|
||
/// 设置文件里的主题模式(`auto` / `light` / `dark`)。
|
||
///
|
||
/// 用来定优先级:`--theme` > 配置文件 > 系统主题。没有它,`Message::SystemTheme` 会无条件
|
||
/// 盖掉配置文件的显式主题(M90 r3 修)。
|
||
pub(super) theme_mode: ThemeMode,
|
||
/// 界面语言(M106):用户档位 + 探测结论 + 目录选择。见 [`crate::ui::lang`]。
|
||
///
|
||
/// 为什么不在 `Ui` 里:`theme.rs` 是令牌求值器(颜色/字号),而语言是**运行期状态**——
|
||
/// 随设置变、要落盘、要重建视图模型(切主题只重建 Style)。
|
||
pub(super) lang: LangState,
|
||
/// 上一次推进节拍的时刻(节拍由帧事件驱动,按真实流逝时间累加)。
|
||
pub(super) last_tick: Instant,
|
||
/// 窗口宽度(布局档位的判据;交互模式跟着 resize 事件走)。
|
||
pub(super) width: f32,
|
||
/// 展示转换缓存(presenter 快照 → 页面视图模型)。
|
||
pub(super) view: pages::Model,
|
||
/// 视图模型是否过期(P2-1):只有 `apply` / `navigate` / resize / 主题这类**真的改了状态**
|
||
/// 的事情才置位,`advance()` 每拍只在置位时重建模型(否则 30 Hz × N 实例的白克隆)。
|
||
pub(super) view_dirty: bool,
|
||
/// 「刚派发过指令,等后端回事件」的观察窗(P2-1)。
|
||
///
|
||
/// 空闲态**不订阅帧事件**(只有 1 Hz 的 `heartbeat`),所以派发之后必须有这段短窗口让
|
||
/// 33 ms 节拍跑起来,否则真 host 的异步事件永远抽不出来。
|
||
///
|
||
/// 空闲态不订阅帧事件(不点亮窗口重绘),但派发之后必须有一段短窗口保持节拍,
|
||
/// 否则真 host 的异步事件永远抽不出来(`LaunchInstance` 之后状态还是 Idle ⇒ 帧订阅永不开启)。
|
||
pub(super) watch_until: Option<Instant>,
|
||
/// 窗口句柄(`window::latest()` 之后才有)。
|
||
pub(super) window: Option<window::Id>,
|
||
/// 已画的帧数(截图闸门用)。
|
||
pub(super) frames: u32,
|
||
/// 视图模型重建次数(P2-1 的可观测计数:空闲态 100 拍里必须是 0 增长)。
|
||
pub(super) refreshes: u64,
|
||
/// 首屏快照到位后是否已经自动选中过第一行(list-detail 的桌面惯例,见 `update::auto_select`)。
|
||
pub(super) auto_selected: bool,
|
||
/// 窗口高度(背景图平铺网格的视口判据;宽度在 `width`)。
|
||
pub(super) height: f32,
|
||
/// 外观运行期状态(M93):面板 α / 背景图 / 配色来源 / 诊断。
|
||
pub(super) appearance: pages::appearance::State,
|
||
/// 设置文件里那 8 个「UI 写入」外观字段的最近快照(写回时只改我们真改了的字段,
|
||
/// 强调色这类本 mission 不管的字段原样保留)。
|
||
pub(super) appearance_prefs: unlucky_presenter::prefs::AppearancePrefs,
|
||
/// 窗口句柄(`Message::WindowHandle` 之后才有;非 Windows 平台恒 `None`)。
|
||
pub(super) hwnd: Option<isize>,
|
||
/// 设置页各组自己的运行期状态(M101 r2 的**状态漏斗**)。
|
||
///
|
||
/// 所有者在 `pages/settings/state.rs` 的 `Groups`:加/改某一组的字段只改那一组自己的文件,
|
||
/// 本字段与 `app/view.rs` 都不用动(`settings::States` 只看 `&Groups`,不看任何一组的字段)。
|
||
/// 放在字段列表**末尾**:后续 mission 若也要往这里加字段,冲突面最小。
|
||
pub(super) settings: pages::settings::Groups,
|
||
/// AI 助手栏的 UI 状态(M104):右栗三档 + 展开宽度 + 会话/草稿/未读。
|
||
///
|
||
/// 挂在 `App` 上而不是全局:这棵树在 `view(&self)` 里只读、在 `update(&mut self)` 里改,
|
||
/// 状态跟着 App 走才能 per-app、可测(tower 明确否掉了 `OnceLock<Mutex<..>>`)。
|
||
pub(super) agent: crate::shell::agent::AgentUi,
|
||
/// 下载页的安装草稿(M105:选中版本 + 加载器 + 加载器版本 + 实例名)。
|
||
///
|
||
/// 为什么住在 `App` 而不是页面模型:`refresh_view` 每次事件都把 `self.view` 整棵重建,
|
||
/// 页面自己存不住「用户正在填但还没确认」的东西;这与分类筛选(`view.download.category`)
|
||
/// 是同一个理由、同一个做法。
|
||
pub(super) download_draft: pages::InstallDraft,
|
||
/// 已经提示过的取消次数(M105):`AppState::install_cancelled` 是「发生过几次取消」,
|
||
/// 这里是「其中几次已经弹过提示」。两者不等 ⇒ 弹一次 i18n 提示。
|
||
pub(super) install_cancelled_seen: u64,
|
||
/// 内容库页的页面内选择(M109):内容类型 / 选中项目 / 目标实例 / 关键词草稿。
|
||
///
|
||
/// 为什么在 `App` 而不是 presenter:这四样是「用户在这一屏上点了什么」,重建快照不该冲掉;
|
||
/// 而检索条件与结果(会跨页存活、要被事件写)在 `AppState::content`。放进 `view` 模型也
|
||
/// 不行——`refresh_view` 每次都用快照重建模型,选择会被冲回默认值。
|
||
pub(super) content: pages::content::PageState,
|
||
/// 账户页的**表单状态**(三条轨道的输入串;密码只活在这一次请求里)。
|
||
pub(super) account_form: pages::account::Form,
|
||
/// 账户页视图模型(与 `view` 同一条「状态变了才重建」的门控)。
|
||
pub(super) account: pages::account::Page,
|
||
/// 皮肤/头像解码缓存(M114:同一张皮肤只解一次)。
|
||
pub(super) avatars: pages::account::avatar::Cache,
|
||
}
|
||
|
||
impl App {
|
||
/// 生产构造:按启动参数选后端、打开设置桥。
|
||
///
|
||
/// **截图通道强制 mock**:出图要可复现(D30),真 host 会按本机盘上的实例清单画面,
|
||
/// 同一份代码在两台机器上出不了同一张图。交互模式走真 host(数据根解析不出来时
|
||
/// [`build_backend`] 自己退回 mock)。
|
||
pub(super) fn new(options: Options, families: Families, flavor: SchemeFlavor) -> Self {
|
||
let backend = build_backend(&app_options(&options));
|
||
// P2-2:设置桥必须与后端指向**同一个数据根**。早先这里无条件 `SettingsBridge::open()`
|
||
// (按便携配置解析),于是 `--data-dir X` 会出现「实例来自 X、主题/设置读写默认根」的一进程
|
||
// 两个数据根。Slint 版是同一条口径:`app/src/ui_bind.rs` 的
|
||
// `Some(root) => SettingsBridge::with_path(config_path(root))`。
|
||
let bridge = match &options.data_dir {
|
||
Some(root) => SettingsBridge::with_path(config_path(root)),
|
||
None => SettingsBridge::open(),
|
||
};
|
||
Self::with_presenter(options, families, flavor, backend, bridge)
|
||
}
|
||
|
||
/// 注入式构造(单测用:后端与桥都由调用方给,**不碰磁盘**)。
|
||
pub(super) fn with_presenter(
|
||
options: Options,
|
||
families: Families,
|
||
flavor: SchemeFlavor,
|
||
backend: Box<dyn Backend>,
|
||
bridge: SettingsBridge,
|
||
) -> Self {
|
||
let width = WINDOW_SIZE.width;
|
||
let height = WINDOW_SIZE.height;
|
||
let theme_mode = bridge.load().1;
|
||
// 外观两半分开读:`load()` 给 8 个 UI 写入字段,`load_base()` 给 3 个只读下层键
|
||
// (整窗 α / 标题栏三态 / 配色来源)。M93 之前这两个键在 app 侧**根本读不到**。
|
||
//
|
||
// 截图通道**不读用户配置**(D30:同一份代码在两台机器上要出同一张图):外观一律取出厂值,
|
||
// 矩阵需要的值由 `ULL_APPEARANCE_*` 显式灌进来。
|
||
let (appearance_prefs, appearance_base) = if options.screenshot.is_some() {
|
||
(
|
||
unlucky_presenter::prefs::AppearancePrefs::factory(),
|
||
unlucky_presenter::prefs::AppearanceBase::factory(),
|
||
)
|
||
} else {
|
||
(bridge.load().0, bridge.load_base())
|
||
};
|
||
// 面板 α / 玻璃的降级判据要的是**实际**后端,不是 `--renderer` 的请求值(复审 r1 P2-2):
|
||
// 默认 `Auto` + 无独显时请求是 Auto、实际是 tiny-skia,只看请求值就会漏掉 D36 的强制项。
|
||
let backend_tier = super::backend::effective_backend(options.backend);
|
||
let mut appearance = pages::appearance::State::new(
|
||
&appearance_prefs,
|
||
&appearance_base,
|
||
backend_tier,
|
||
flavor,
|
||
);
|
||
// 取证开关(D30):`ULL_SETTINGS_GROUP=<下标>` 直达设置页的某一组(出图要看见语言行)。
|
||
if let Some(index) =
|
||
crate::ui::lang::screenshot_settings_group(options.screenshot.is_some())
|
||
{
|
||
appearance.selected_group = index;
|
||
}
|
||
// 界面语言(M106):**M92 已经探测对了,问题是一直没人用**——`pages::Model.lang` 此前在
|
||
// `view::model` 里被写死成 `Lang::En`,于是中文系统上开出来的是全英文界面。
|
||
let lang = LangState::boot(&bridge, options.screenshot.is_some());
|
||
let mut view = pages::Model::new(
|
||
pages::Screen::Instances,
|
||
width,
|
||
Some(pages::Screen::Instances.nav()),
|
||
);
|
||
view.lang = lang.lang();
|
||
|
||
let mut app = Self {
|
||
ui: Ui::new(flavor, families),
|
||
families,
|
||
options,
|
||
backend,
|
||
bridge,
|
||
state: AppState::new(),
|
||
ticks: 0,
|
||
config_due_at: unlucky_presenter::theme::CONFIG_POLL_TICKS,
|
||
theme_mode,
|
||
lang,
|
||
last_tick: Instant::now(),
|
||
width,
|
||
view,
|
||
view_dirty: false,
|
||
watch_until: None,
|
||
window: None,
|
||
frames: 0,
|
||
refreshes: 0,
|
||
auto_selected: false,
|
||
height,
|
||
appearance,
|
||
appearance_prefs,
|
||
hwnd: None,
|
||
settings: pages::settings::Groups::default(),
|
||
// 取证开关 `ULL_AGENT_*`(三态矩阵 / 空会话)在这里落地;不设变量 = 出厂态
|
||
agent: crate::shell::agent::demo::from_env().unwrap_or_default(),
|
||
// 下载页安装草稿(M105):出厂态 = 没选中、原版、两个空串
|
||
download_draft: pages::InstallDraft::default(),
|
||
install_cancelled_seen: 0,
|
||
content: pages::content::PageState::default(),
|
||
account_form: pages::account::Form::new(),
|
||
account: pages::account::Page::build(
|
||
&AppState::new(),
|
||
&pages::account::Form::new(),
|
||
&mut pages::account::avatar::Cache::default(),
|
||
unlucky_i18n::Lang::En,
|
||
width,
|
||
),
|
||
avatars: pages::account::avatar::Cache::default(),
|
||
};
|
||
// 配色来源档可能已经指向一张背景图:那时 `Ui` 必须由解析出的方案构造,而不是 baseline。
|
||
app.rebuild_ui();
|
||
// 两行日志:①档位 + 探测结论 + 生效语言 ②中文策略(D28 缺字体不许静默)。
|
||
// 下一个人据此分得清「探测错了」还是「探测对了但没用上」。
|
||
app.lang.log();
|
||
app.lang.log_cjk();
|
||
app
|
||
}
|
||
|
||
/// 本次进程是否走截图通道。
|
||
pub(super) fn has_screenshot(&self) -> bool {
|
||
self.options.screenshot.is_some()
|
||
}
|
||
|
||
/// 切主题:重建 [`Ui`](= 重建 Style),**不动任何 UI 状态**。
|
||
///
|
||
/// M93 起 `Ui` 有两个来源:配色来源取到了图(①②级)就用解析出的方案,否则用令牌 baseline。
|
||
/// 所以这里改叫「重算 `Ui`」:先让外观状态记住新深浅(配色解析要它),再重建。
|
||
pub(super) 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();
|
||
eprintln!(
|
||
"[theme] {} -> {} (Theme::name 变化触发缓存重算)",
|
||
previous,
|
||
self.ui.name()
|
||
);
|
||
}
|
||
|
||
/// 按「配色来源是否取色成功」重建 [`Ui`]。
|
||
///
|
||
/// 色卡档(③)刻意继续走 `Scheme::baseline`:`resolve_source` 的色卡档返回的是
|
||
/// `DEFAULT_CARD`(天蓝卡)方案,与令牌 baseline 逐色不同,应用它会把**全部既有验收图的
|
||
/// 像素**改掉。这条偏离登记在 `pages::appearance` 的偏离表 #4。
|
||
pub(super) fn rebuild_ui(&mut self) {
|
||
// 面板 α(D36)在这里注入 `Ui`:全仓所有表面色都从 `Ui::layer` 出去,所以这是**唯一**
|
||
// 生效点(外壳 / 三页卡片 / 外观分组卡片 / 空态图标块自动同源,调用点没有机会各处自乘)。
|
||
let panel_alpha = self.appearance.effective_panel_alpha();
|
||
self.ui = match self.appearance.derived_scheme() {
|
||
Some(scheme) => {
|
||
eprintln!(
|
||
"[appearance] palette={} → Ui::from_scheme(取色方案接管 49 个角色)",
|
||
self.appearance.palette_level()
|
||
);
|
||
Ui::from_scheme(*scheme, self.families)
|
||
}
|
||
None => Ui::new(self.appearance.flavor(), self.families),
|
||
}
|
||
.with_panel_alpha(panel_alpha);
|
||
}
|
||
|
||
/// 把 presenter 的快照转换成页面视图模型(**展示转换在 UI 侧做**)。
|
||
///
|
||
/// 代价是「克隆 N 个实例的名字/版本串」,所以只在 [`App::view_dirty`] 置位时调用(P2-1)。
|
||
pub(super) fn refresh_view(&mut self) {
|
||
// **不变式先对齐再算宽度**(M104 / 复审 r1 P2-1):`Route::Agent` ⟺ `RailState::Detached`。
|
||
// 每一次路由变化都会走到这里,所以它是「档位 ↔ 路由」的唯一点;放在算宽度之前,
|
||
// 是为了让模型拿到的 `content_width` 与页面头部读到的档位来自同一份状态。
|
||
self.sync_agent_route();
|
||
|
||
// M105:取消安装的提示语在**这里**弹(`refresh_view` 是事件 apply 之后必经的那一处)。
|
||
// 文案由页面层求(`pages::cancelled_toast`),presenter 只发信号 ——
|
||
// 否则会出现「选项区英文、取消弹窗中文」的同屏两语(复审 P2-2)。
|
||
if self.state.install_cancelled != self.install_cancelled_seen {
|
||
self.install_cancelled_seen = self.state.install_cancelled;
|
||
// M106:提示语跟着**生效语言**走(M105 留的 `view::ui_lang()` 占位已删——它恒英文,
|
||
// 中文界面会弹出英文提示,那正是 M105 r1 复审 P2-2 要堵的同屏两语)。
|
||
let text = pages::cancelled_toast(self.lang());
|
||
self.state.toasts.push(ToastKind::Info, text);
|
||
}
|
||
|
||
// 分类是**页面自己持有的过滤条件**(真 host 丢弃 `ListVersions` 的分类参数):
|
||
// 重建视图模型时带回去,否则每次事件都会把 chip 打回「全部」(复审 P2-1)。
|
||
let category = self.view.download.category;
|
||
// 草稿同理(M105):它也是页面自己持有的输入,重建模型时带回去,
|
||
// 否则每来一条事件(连安装进度都算)用户填的加载器就被抹回「原版」。
|
||
let draft = self.download_draft.clone();
|
||
// 右栏占掉的宽度先扣掉:面板展开时页面按更窄的宽度排版(「收缩而不是溢出」,M104)
|
||
let width = self.agent.content_width(self.width);
|
||
// M106:语言形参跟着 `LangState` 走(`draft` 那一位是 M105 的安装草稿)。
|
||
self.view = super::view::model(
|
||
&self.state,
|
||
width,
|
||
self.options.force_state,
|
||
category,
|
||
&draft,
|
||
self.lang(),
|
||
&self.content,
|
||
);
|
||
// 账户页(M114):与 `view` 同一次门控重建(实现见 `view/account_page.rs`,
|
||
// 皮肤解码走缓存,所以重复重建不重解图)。
|
||
self.rebuild_account_page(width);
|
||
self.view_dirty = false;
|
||
self.refreshes += 1;
|
||
}
|
||
|
||
/// 按设置文件里的主题模式切一次外观;`--theme` 显式给了就不动。
|
||
///
|
||
/// **截图通道直接跳过**:`--screenshot` 的主题唯一取值口是 `Options::capture_flavor`
|
||
/// (D30 可复现),读本机 `launcher.json` 会让同一份代码在两台机器上出不同的图。
|
||
///
|
||
/// `ThemeMode::Auto` 不在这里处理:那要看系统主题(`iced::system::theme()` 的结果在
|
||
/// `Message::SystemTheme` 里落地)。
|
||
pub(super) 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;
|
||
match mode {
|
||
ThemeMode::Light => self.set_flavor(SchemeFlavor::Light),
|
||
ThemeMode::Dark => self.set_flavor(SchemeFlavor::Dark),
|
||
ThemeMode::Auto => {}
|
||
}
|
||
}
|
||
|
||
/// 当前令牌求值器(测试与截图日志用)。
|
||
pub(super) fn ui(&self) -> &Ui {
|
||
&self.ui
|
||
}
|
||
}
|