refactor(app-iced): 拆 state.rs(400 行) 与 update.rs(399 行) 为功能子文件
两个文件都贴着 400 行硬线、零余量,M119 要往里加字段与一行 drain。**这一个提交只搬不改** (下一次提交才是改动),搬的位置按功能切: | 原位置 | 现在 | 内容 | |---|---|---| | `app/state.rs` | `state.rs`(172) | 只剩 crate 文档 + 常量 + `struct App` 的字段与注释 | | 同上 | `state/construct.rs` | 两个构造器 + 字段初始化表 | | 同上 | `state/refresh.rs` | `refresh_view`(快照 → 视图模型的唯一转换点) | | 同上 | `state/theme.rs` | `Ui` 重算 / 主题档 / `has_screenshot` / `ui()` | | `app/update.rs` | `update/clock.rs` | 推进一拍 / 派发 / 抽干事件 / 自动选中 | 一处**非字面**的改动:搬进子模块之后 `pub(super)` 的语义从「crate::app」变成「crate::app::state」, 所以这些方法改成 `pub(in crate::app)`——可见范围与搬家前逐字相同,只是写法得跟着模块路径走。 除此之外照抄原文(含原有注释与文档)。 搬完 `cargo check -p unlucky-app-iced --all-targets` 零 error 零 warning。
This commit is contained in:
1 parent
1b77a36d3d
commit
3c61a2397e
6 files changed
+441
-356
No files matched your search
@@ -0,0 +1,146 @@
|
||||
//! `App` 的两个构造器(从 `state.rs` 搬出来守 400 行硬线;M119 拆,纯搬不改)。
|
||||
//!
|
||||
//! | 构造器 | 谁用 | 碰不碰磁盘 |
|
||||
//! |---|---|---|
|
||||
//! | [`App::new`](super::App::new) | 生产(`main` → `boot`) | 按启动参数选后端 + 打开设置桥 |
|
||||
//! | [`App::with_presenter`](super::App::with_presenter) | 单测(后端与桥都由调用方给) | **不碰** |
|
||||
//!
|
||||
//! 两个都走 `with_presenter`:生产路径只是先造好后端与桥。**字段初始化表**也在这一个文件里
|
||||
//! (构造 = 字段表 + 三件外部依赖),加字段时只改这一处。
|
||||
|
||||
use iced::window;
|
||||
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_presenter::backend::{build_backend, config_path};
|
||||
use unlucky_presenter::dto::Backend;
|
||||
use unlucky_presenter::settings_bridge::SettingsBridge;
|
||||
use unlucky_presenter::state::AppState;
|
||||
|
||||
use crate::cli::Options;
|
||||
use crate::pages;
|
||||
use crate::theme::{Families, Ui};
|
||||
use crate::ui::lang::LangState;
|
||||
|
||||
use super::super::WINDOW_SIZE;
|
||||
use super::boot_probe::app_options;
|
||||
use super::App;
|
||||
|
||||
impl App {
|
||||
/// 生产构造:按启动参数选后端、打开设置桥。
|
||||
///
|
||||
/// **截图通道强制 mock**:出图要可复现(D30),真 host 会按本机盘上的实例清单画面,
|
||||
/// 同一份代码在两台机器上出不了同一张图。交互模式走真 host(数据根解析不出来时
|
||||
/// [`build_backend`] 自己退回 mock)。
|
||||
pub(in crate::app) 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(in crate::app) 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()` 给 4 个只读下层键
|
||||
// (整窗 α / 标题栏三态 / 配色来源 / 窗口圆角)。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::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: std::time::Instant::now(),
|
||||
width,
|
||||
view,
|
||||
view_dirty: false,
|
||||
watch_until: None,
|
||||
window: None::<window::Id>,
|
||||
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(),
|
||||
corner_applied: None,
|
||||
};
|
||||
// 配色来源档可能已经指向一张背景图:那时 `Ui` 必须由解析出的方案构造,而不是 baseline。
|
||||
app.rebuild_ui();
|
||||
// 两行日志:①档位 + 探测结论 + 生效语言 ②中文策略(D28 缺字体不许静默)。
|
||||
// 下一个人据此分得清「探测错了」还是「探测对了但没用上」。
|
||||
app.lang.log();
|
||||
app.lang.log_cjk();
|
||||
app
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//! 快照 → 视图模型的重建口径(从 `state.rs` 搬出来守 400 行硬线;M119 拆,纯搬不改)。
|
||||
//!
|
||||
//! 全仓只有 [`App::refresh_view`] 这一个「presenter 快照 → 页面要画的东西」的转换点,
|
||||
//! 由 [`App::view_dirty`](super::App) 门控(M90 r2/P2-1 起):只有真的改了状态的事情才置位,
|
||||
//! 30 Hz 空转不重建(重建要把 N 个实例的名字/版本串克隆一遍)。
|
||||
|
||||
use unlucky_presenter::toast::ToastKind;
|
||||
|
||||
use crate::pages;
|
||||
|
||||
use super::App;
|
||||
|
||||
impl App {
|
||||
/// 把 presenter 的快照转换成页面视图模型(**展示转换在 UI 侧做**)。
|
||||
///
|
||||
/// 代价是「克隆 N 个实例的名字/版本串」,所以只在 [`App::view_dirty`] 置位时调用(P2-1)。
|
||||
pub(in crate::app) 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::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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//! 主题与 `Ui` 的重算口径(从 `state.rs` 搬出来守 400 行硬线;M119 拆,纯搬不改)。
|
||||
//!
|
||||
//! 三件事在这里:①`Ui` 什么时候重建([`App::rebuild_ui`])②主题档怎么定
|
||||
//! ([`App::set_flavor`] / [`App::apply_config_theme`])③读当前求值器([`App::ui`])。
|
||||
//!
|
||||
//! 截图通道的判据 [`App::has_screenshot`] 也放在这儿:上面三条里两条都要「截图不读用户配置」。
|
||||
|
||||
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();
|
||||
eprintln!(
|
||||
"[theme] {} -> {} (Theme::name 变化触发缓存重算)",
|
||||
previous,
|
||||
self.ui.name()
|
||||
);
|
||||
}
|
||||
|
||||
/// 按「配色来源是否取色成功」重建 [`Ui`]。
|
||||
///
|
||||
/// 色卡档(③)刻意继续走 `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();
|
||||
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);
|
||||
}
|
||||
|
||||
/// 按设置文件里的主题模式切一次外观;`--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;
|
||||
match mode {
|
||||
ThemeMode::Light => self.set_flavor(SchemeFlavor::Light),
|
||||
ThemeMode::Dark => self.set_flavor(SchemeFlavor::Dark),
|
||||
ThemeMode::Auto => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前令牌求值器(测试与截图日志用)。
|
||||
pub(in crate::app) fn ui(&self) -> &Ui {
|
||||
&self.ui
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user