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
@@ -25,7 +25,7 @@
|
||||
//!
|
||||
//! 两条时钟不会同时干活:心跳在 `needs_frames()` 为真时**只续排、不推进**,忙碌结束自动接回。
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::Instant;
|
||||
|
||||
use iced::{window, Task};
|
||||
|
||||
@@ -33,19 +33,19 @@ use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_presenter::dto::AppCommand;
|
||||
use unlucky_presenter::prefs::ThemeMode;
|
||||
use unlucky_presenter::router::Route;
|
||||
use unlucky_presenter::theme::CONFIG_POLL_TICKS;
|
||||
|
||||
use crate::pages;
|
||||
use crate::renderer::Kind;
|
||||
|
||||
use super::message::{route_of, Message};
|
||||
use super::state::{App, IDLE_TICK, TICK, TICK_MS, WATCH_WINDOW};
|
||||
use super::state::{App, IDLE_TICK, TICK, WATCH_WINDOW};
|
||||
use super::CAPTURE_AT_FRAME;
|
||||
|
||||
mod account;
|
||||
mod agent;
|
||||
mod appearance;
|
||||
mod boot_env;
|
||||
mod clock;
|
||||
mod content;
|
||||
mod download;
|
||||
mod page_messages;
|
||||
@@ -249,117 +249,6 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
/// 推进一拍:后端消化时间 → 抽干事件 → 过期 toast → 偶发重读配置文件。
|
||||
///
|
||||
/// `elapsed` 是真实流逝时间(毫秒级抖动是正常的:`MockBackend` 的脚本按累计时长走)。
|
||||
pub(super) fn advance(&mut self, elapsed: Duration) {
|
||||
self.backend.tick(elapsed);
|
||||
self.pump();
|
||||
|
||||
self.state
|
||||
.toasts
|
||||
.expire(u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX));
|
||||
|
||||
// 设备码/授权码的倒计时(M114):等待态每秒都要重画一次,否则「剩 900s」永远不动,
|
||||
// 用户会以为卡死了——这正是任务里「不得只转圈」的那一条。
|
||||
if self.state.device_login.phase == unlucky_presenter::dto::DeviceLoginPhase::Waiting {
|
||||
self.view_dirty = true;
|
||||
}
|
||||
// `ticks` 的单位是**33 ms 的一拍**,不是「advance 被调了几次」:空闲心跳一次跨 1 s,
|
||||
// 要记 30 拍,这样 `theme::config_poll_due`(30 拍 ≈ 1 秒)在两种时钟下语义一致。
|
||||
let ticks = elapsed.as_millis().div_euclid(u128::from(TICK_MS));
|
||||
let ticks = u64::try_from(ticks).unwrap_or(u64::MAX).max(1);
|
||||
let bucket_before = self.ticks / 30;
|
||||
self.ticks += ticks;
|
||||
let bucket_after = self.ticks / 30;
|
||||
|
||||
// 外部(AI / CLI)改了配置文件:主题跟着换,其余外观项等玻璃/背景 mission。
|
||||
//
|
||||
// 判据用**跨越**而不是 presenter 的 `config_poll_due(ticks)`(整除):两个时钟的相位不同
|
||||
// (帧节拍按 33 ms 累加、空闲心跳按 30 拍累加),整除判定一旦错过一次就**再也命中不了**
|
||||
// ——实测 ticks=137 之后每拍 +30 永远不整除,配置轮询静默死掉。间隔真源仍是
|
||||
// `CONFIG_POLL_TICKS`(30 拍 × 33 ms ≈ 1 秒)。截图通道不轮询(D30 可复现)。
|
||||
let poll_due = self.ticks >= self.config_due_at;
|
||||
if poll_due {
|
||||
// 追到「当前 ticks 之后的第一个间隔点」,避免心跳把相位拖飞
|
||||
self.config_due_at = self.ticks + CONFIG_POLL_TICKS;
|
||||
}
|
||||
let reloaded =
|
||||
!self.has_screenshot() && poll_due && self.bridge.reload_if_changed().is_some();
|
||||
if reloaded {
|
||||
eprintln!("[settings] 配置文件变了,重读主题 + 外观");
|
||||
self.apply_config_theme();
|
||||
// M93:外观也热切(面板 α / 背景图 / 配色来源 / 整窗 α 都在里面)。
|
||||
self.reload_appearance();
|
||||
}
|
||||
|
||||
if self.view_dirty {
|
||||
self.refresh_view();
|
||||
}
|
||||
|
||||
// 取证开关(`ULL_TICK_TRACE=1`):每跨过 30 拍(≈1 秒)打一行。
|
||||
// 忙碌态由帧节拍跨桶(≈1 行/秒),空闲态由心跳一次 +30 跨桶(**也是 1 行/秒**)——
|
||||
// 两种时钟都用同一把尺子,所以 trace 的密度本身就是「时钟在跑」的证据。
|
||||
if bucket_after != bucket_before && std::env::var_os("ULL_TICK_TRACE").is_some() {
|
||||
eprintln!(
|
||||
"[tick] n={} frames={} refreshes={} clock={}",
|
||||
self.ticks,
|
||||
self.frames,
|
||||
self.refreshes,
|
||||
if self.needs_frames() {
|
||||
"frames"
|
||||
} else {
|
||||
"idle-1hz"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 派发一条指令并**立刻**抽干它带出来的事件(不本地回显,等价于 Slint 版的 `dispatch`)。
|
||||
pub(super) fn dispatch(&mut self, command: AppCommand) {
|
||||
self.backend.dispatch(command);
|
||||
// 观察窗:命令发出后 3 秒内保持帧订阅,等后端把第一条事件交回来
|
||||
// (真 host 是异步的,派发瞬间状态还没变;不设这个窗口,空闲态的帧订阅永远不会打开)。
|
||||
self.watch_until = Some(Instant::now() + WATCH_WINDOW);
|
||||
self.pump();
|
||||
}
|
||||
|
||||
/// 抽干事件队列并逐条 `apply`(**唯一**的状态写入口)。
|
||||
pub(super) fn pump(&mut self) {
|
||||
let events = self.backend.drain_events();
|
||||
if events.is_empty() {
|
||||
return;
|
||||
}
|
||||
for event in events {
|
||||
self.state.apply(event);
|
||||
}
|
||||
self.view_dirty = true;
|
||||
self.auto_select();
|
||||
self.refresh_view();
|
||||
}
|
||||
|
||||
/// 首屏快照到位后**自动选中第一行**(桌面 list-detail 的惯例:右栏不该开局就是空的)。
|
||||
///
|
||||
/// 只做一次:用户点「返回」回到列表后,再刷新实例列表也不会把他弹回详情页。
|
||||
/// 选中这件事走的是 `AppState::navigate`(路由栈),不是本地选中字段——于是「当前选中」
|
||||
/// 永远等于「路由栈顶」,用户点返回时栈语义自然成立。
|
||||
fn auto_select(&mut self) {
|
||||
if self.auto_selected || self.state.route() != &Route::Instances {
|
||||
return;
|
||||
}
|
||||
let Some(first) = self
|
||||
.state
|
||||
.tiles()
|
||||
.first()
|
||||
.map(|cell| cell.instance.id.clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
self.auto_selected = true;
|
||||
self.state.navigate(Route::Detail(first));
|
||||
self.view_dirty = true;
|
||||
}
|
||||
|
||||
/// 截图回来了:落盘 + 打一行取证日志,然后退出。
|
||||
fn on_captured(&mut self, screenshot: &window::Screenshot) -> Task<Message> {
|
||||
let Some(path) = self.options.screenshot.clone() else {
|
||||
|
||||
Reference in new issue
Block a user