用户实测四条抱怨逐条对应: - 「没有自动检测本机 Java」:首屏自动扫一次盘(托管根 + JAVA_HOME + PATH + 标准安装位置 + 注册表),之后由「立即扫描」手动触发;扫描在后台线程里跑,带 done/total 进度与取消 - 「显示 Temurin 21.0.4 不知道是不是硬编码」:演示行与绊线测试一起删掉,列表来自真读盘 - 「没一个能改」:默认内存(MB,512~65536,范围与 schema 同源)与 JVM 参数可真改、真落盘、读回 - 「不能手动指定 Java 路径 / 不能改默认版本」:手动添加走严格校验(真跑 java -version), 设为默认写 java.selected_path 并给出「为什么选它」(当前实例的版本门槛 + 每行满足与否) core:java::manual(严格探测 + 厂商识别 + 名单持久化)、java::staged(候选收集 + 带进度与 取消的扫描)、java::proc(带超时且可打断的子进程执行:卡住的 java -version 会被 kill)。 i18n:catalog/settings/java.rs 55 条中英 + 金表按装配顺序插入。 复审 r1(p2-5items)全修: - P2-1 「设为默认」不再清脏:只写 java.selected_path,内存/JVM 草稿一个字节都不动 - P2-2 错误文案全部走 i18n(三档「写不进去」/「路径太长」/「名单写不进去」各自一个 key), 不再把 Rust 枚举名当人话 - P2-3 手动名单落盘有正面断言(加一条 → 从盘上新读一份 → 在) - P2-4 版本号显示到小版本(17.0.13 / 17.0.9 可区分),解析不出来时给原始版本串 - P2-5 每候选 8 秒超时并 kill、取消打断正在跑的探测、扫描槽挂在 State 上、线程活着期间 不许起第二个(界面显示「正在取消…」) 复审 r2(p2-2items)全修: - P2-1 空态不再指向不存在的「Java 页」,改成真实存在的 `ull java install 21` 命令行入口 - P2-2 `auto_scan_due` 增加 `!report.cancelled`:取消过一次之后本进程不再自动扫, 配置文件再变(存内存 / 改主题)也不会把取消无声撤销;手动「立即扫描」不受影响 - 顺带:proc 的 kill+wait 补「子进程真死了」的回归守卫(标记文件从未出现)、 core 的 Schema 错误改成页面自己的 key(不再把中文 Display 直出给英文界面)、 默认态 chip 用新的 `Chip::badge`(不再挂一条永远收不到的消息)
97 lines
4.7 KiB
Rust
97 lines
4.7 KiB
Rust
//! 订阅:**按需**帧节拍(presenter 的 TICK)+ 窗口尺寸事件。
|
||
//!
|
||
//! ## 为什么要「按需」
|
||
//!
|
||
//! iced_winit 在处理完任何一条消息后都会 `request_redraw()`,而 `window::frames()` 监听的是
|
||
//! `RedrawRequested` —— 于是「订阅帧 → 收 Frame → update → 再触发重绘」是一条**自持循环**:
|
||
//! 只要订阅着,窗口就永远在重绘(M90 r1 就是这样,CPU 空转)。
|
||
//!
|
||
//! 现在只在**真的需要 33 ms 节拍**的时候订阅([`App::needs_frames`]):
|
||
//!
|
||
//! | 条件 | 为什么需要 33 ms 节拍 |
|
||
//! |---|---|
|
||
//! | 截图通道 | 第 3 帧要落盘 |
|
||
//! | 有 `Busy` / `Running` 实例 | 启动进度、日志尾随都靠 `backend.tick` 推进 |
|
||
//! | 有未过期的 toast | toast 按毫秒倒计时 |
|
||
//! | 刚派发过指令(3 s 观察窗) | 真 host 的异步事件要靠节拍抽出来 |
|
||
//!
|
||
//! 空闲态**一条帧消息都不来** ⇒ 窗口不重绘、`refresh_view` 不跑(配合 `view_dirty`)。
|
||
//!
|
||
//! ## 空闲态的时钟从哪来(M90 r3 / P2-2)
|
||
//!
|
||
//! r2 把空闲的帧订阅掐掉之后,`config_poll_due` 就再也没被调用过 —— 外部 CLI / AI 工具改
|
||
//! `launcher.json` 后主题**永不生效**(不是「等下次交互」,是「等下一次 dispatch」)。这与
|
||
//! D4「AI 能读改 ULL 配置」直接冲突。
|
||
//!
|
||
//! 所以空闲态换成一个 **1 Hz 的自续心跳**([`crate::app::message::Message::Heartbeat`],
|
||
//! 由 `Task::perform(sleep(1s))` 驱动,见 `update::heartbeat`):每秒醒一次、推进一拍
|
||
//! presenter(内部按 33 ms 折算 30 拍,于是 `config_poll_due` 仍是「约 1 秒比对一次 mtime」)。
|
||
//!
|
||
//! 为什么不用 `window::frames()` + 1 s 闸门来实现这个低频时钟:帧订阅**本身就是**自持重绘循环
|
||
//! (iced_winit 处理完任何消息都 `request_redraw()`),闸门只能省掉 `advance()` 里的活,省不掉
|
||
//! 整窗重绘——实测软件渲染约 **500 帧/秒**。心跳是「每秒一帧」,差三个数量级。
|
||
//!
|
||
//! ## 已知代价(都登记,不藏)
|
||
//!
|
||
//! 1. **忙碌态仍是自持帧循环**:`Running`/`Busy` 一天不结束,就一天按帧刷(软件渲染实测约
|
||
//! 970 帧/秒,refreshes 不增长 = 纯像素空转)。功能上需要 tick 来发现游戏退出,但「整段
|
||
//! 运行期近千帧/秒重绘」是真实开销;要收口得让 `Running` 走更慢的时钟(follow-up)。
|
||
//! 2. 心跳用 `thread::sleep` 占线程池一个线程 1 秒;1 Hz 下可接受,但不是优雅解法(正解仍是
|
||
//! `iced::time::every` + tokio/smol,Cargo.toml 在 scope 外,已报 finding)。
|
||
|
||
use std::time::Instant;
|
||
|
||
use iced::{window, Subscription};
|
||
|
||
use super::message::Message;
|
||
use super::state::App;
|
||
|
||
impl App {
|
||
/// 本次进程的订阅表。
|
||
pub(super) fn subscription(&self) -> Subscription<Message> {
|
||
let frames = if self.needs_frames() {
|
||
window::frames().map(|_| Message::Frame)
|
||
} else {
|
||
Subscription::none()
|
||
};
|
||
|
||
// 断点 600 / 840 要跟着真窗口宽度走;截图模式的窗口尺寸由参数钉死,不吃 resize。
|
||
let events = if self.has_screenshot() {
|
||
Subscription::none()
|
||
} else {
|
||
window::events().map(|(_, event)| Message::Window(event))
|
||
};
|
||
|
||
// 键盘(M104):`Esc` 收栗 / `Tab` 走焦点环 / `Enter` 激活焦点键。
|
||
//
|
||
// 为什么订阅整条键盘流而不是自己挑键:`iced::keyboard::listen()` 是唯一的全窗键盘入口,
|
||
// 挑键与「什么状态下该做什么」的解释权放在 `app/update/agent.rs`(一眼看完),
|
||
// 订阅表只负责搬运。截图通道不开它——D30 要可复现,键盘事件一个都不该进图。
|
||
let keys = if self.has_screenshot() {
|
||
Subscription::none()
|
||
} else {
|
||
iced::keyboard::listen()
|
||
.map(|event| Message::Agent(crate::shell::agent::Message::Key(event)))
|
||
};
|
||
|
||
Subscription::batch([frames, events, keys])
|
||
}
|
||
|
||
/// 是否订阅帧事件(= 截图模式要帧数闸门)。
|
||
pub(super) fn wants_frames(&self) -> bool {
|
||
self.has_screenshot()
|
||
}
|
||
|
||
/// 现在要不要**时钟**(见模块文档的表)。
|
||
pub(super) fn needs_frames(&self) -> bool {
|
||
self.has_screenshot()
|
||
|| !self.state.toasts.is_empty()
|
||
|| !self.state.running().is_empty()
|
||
// M175:Java 扫描进行中(进度 3/7 要动起来,否则用户只看到「正在扫描」卡着)。
|
||
|| self.settings.java.scanning.is_some()
|
||
|| self
|
||
.watch_until
|
||
.is_some_and(|deadline| Instant::now() < deadline)
|
||
}
|
||
}
|