Files
Unlucky-Launcher/crates/app/src/callbacks.rs
T
AskaEth f159529936 feat(presenter): 新建 unlucky-presenter,把 host/状态/DTO/设置桥从 app 迁出(M85 第 1 块)
D26(iced 重写)的关键一次性动作:UI 框架无关的宿主层独立成 crate,
Slint 版(crates/app)与 iced 版(crates/app-iced)共用,Slint 退役后不再重写一遍。

- 新建 crates/presenter(package `unlucky-presenter` / lib `unlucky_presenter`),
  workspace 登记 + crate 文档头写清「这层为什么存在」、对外表面、UI 侧允许/禁止、
  iced 接线骨架、以及哪些东西留在了 crates/app 及为什么。
- `git mv`(保留文件历史)迁入:host/(25 文件)、dto/(11 文件)、state.rs(+tests)、
  prefs.rs、settings_bridge.rs、toast.rs、router.rs、dialogs.rs、wizard.rs、
  mock.rs、mock_data.rs、backend.rs。模块路径与公开 API 名称不变(`host::install` 等),
  内部实现零改动。
- 新增 `theme`:把原本混在 ui_bind/theme_runtime 里的**纯判据**(CONFIG_POLL_TICKS /
  SYSTEM_THEME_POLL_TICKS / TILE_REGENERATE_STEP / BackgroundKey / tile_viewport /
  system_theme_value / 两个 poll 判据)抽出来,附 5 个单测;app 侧改成调用它们。
- crates/app 改为消费者:删掉迁走的文件,`crate::host|state|dto|prefs|settings_bridge|
  toast|router|dialogs|wizard|mock|mock_data|backend` 全部改写为 `unlucky_presenter::*`
  (64 处),backend.rs 的 config_path/build_backend 提为 pub。
- crates/app-iced 只登记 unlucky-presenter 依赖,本 mission 不接线。
- prefs.rs 的 2 处 slint 文档引用改写;`grep -rn slint crates/presenter` 现在无输出。
2026-11-02 09:29:31 +08:00

353 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 回调接线:把 `ui/main.slint` 暴露的每个回调翻译成「状态变更 + AppCommand + 同步」。
//!
//! 逐条列出而不做通用转发,是为了让「用户能做的每一件事」在这里一目了然;
//! 新增交互时只改这一处 + `.slint` 的 callback 声明。
use std::rc::Weak;
use slint::ComponentHandle;
use crate::conv;
use crate::ui_bind::Inner;
use unlucky_presenter::dto::AppCommand;
use unlucky_presenter::router::Route;
use unlucky_presenter::toast::ToastKind;
/// 接全部回调;闭包持弱引用,避免 `MainWindow` 与闭包互相持有导致窗口永不释放。
pub(crate) fn wire(weak: Weak<Inner>) {
let Some(inner) = weak.upgrade() else { return };
// 只在注册期间持有强引用;闭包内一律持弱引用(否则窗口与闭包互相持有、永不释放)。
let ui = &inner.ui;
wire_window(ui, &weak);
wire_navigation(ui, &weak);
wire_instances(ui, &weak);
wire_download(ui, &weak);
wire_logs(ui, &weak);
wire_settings(ui, &weak);
wire_overlays(ui, &weak);
}
fn wire_window(ui: &crate::MainWindow, weak: &Weak<Inner>) {
{
let weak = weak.clone();
ui.on_toggle_maximize(move || {
let Some(this) = weak.upgrade() else { return };
let window = this.ui.window();
window.set_maximized(!window.is_maximized());
});
}
{
let weak = weak.clone();
ui.on_minimize(move || {
if let Some(this) = weak.upgrade() {
this.ui.window().set_minimized(true);
}
});
}
{
let weak = weak.clone();
ui.on_request_close(move || {
if let Some(this) = weak.upgrade() {
let _ = this.ui.hide();
}
let _ = slint::quit_event_loop();
});
}
}
fn wire_navigation(ui: &crate::MainWindow, weak: &Weak<Inner>) {
{
let weak = weak.clone();
ui.on_navigate(move |route| {
let Some(this) = weak.upgrade() else { return };
if let Some(route) = conv::route_from(route) {
this.state.borrow_mut().navigate(route);
this.sync();
}
});
}
{
let weak = weak.clone();
ui.on_toggle_nav(move || {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().toggle_nav();
this.sync();
});
}
{
let weak = weak.clone();
ui.on_toggle_ai(move || {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().toggle_ai();
this.sync();
});
}
{
let weak = weak.clone();
ui.on_expand_ai(move || {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().navigate(Route::Agent);
this.sync();
});
}
{
let weak = weak.clone();
ui.on_dock_ai(move || {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().back();
this.sync();
});
}
{
let weak = weak.clone();
ui.on_toggle_yolo(move || {
let Some(this) = weak.upgrade() else { return };
let enabled = {
let mut state = this.state.borrow_mut();
state.ai.yolo = !state.ai.yolo;
state.ai.yolo
};
this.dispatch(AppCommand::SetYolo(enabled));
this.sync();
});
}
{
let weak = weak.clone();
ui.on_ai_send(move |text| {
let Some(this) = weak.upgrade() else { return };
let text = text.to_string();
if text.trim().is_empty() {
return;
}
// 不做本地回显:user 消息由 host 的 MessageAppended 事件带回(契约 §3)
this.state.borrow_mut().ai.draft.clear();
this.ui.set_ai_draft("".into());
this.ask_agent(&text);
this.sync();
});
}
{
let weak = weak.clone();
ui.on_ai_stop(move || {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().ai.finish_stream();
this.dispatch(AppCommand::StopAgent);
this.sync();
});
}
}
fn wire_instances(ui: &crate::MainWindow, weak: &Weak<Inner>) {
{
let weak = weak.clone();
ui.on_instance_launch(move |id| {
let Some(this) = weak.upgrade() else { return };
this.dispatch(AppCommand::LaunchInstance(id.to_string()));
this.sync();
});
}
{
let weak = weak.clone();
ui.on_instance_configure(move |id| {
let Some(this) = weak.upgrade() else { return };
// 齿轮键进实例详情(G4 占位页),实例级设置后续挂在该页 tab 上
this.state
.borrow_mut()
.navigate(Route::Detail(id.to_string()));
this.sync();
});
}
{
let weak = weak.clone();
ui.on_instance_stop(move |id| {
let Some(this) = weak.upgrade() else { return };
this.dispatch(AppCommand::StopInstance(id.to_string()));
this.sync();
});
}
{
let weak = weak.clone();
ui.on_instance_open(move |id| {
let Some(this) = weak.upgrade() else { return };
this.state
.borrow_mut()
.navigate(Route::Detail(id.to_string()));
this.sync();
});
}
{
let weak = weak.clone();
ui.on_new_instance(move || {
let Some(this) = weak.upgrade() else { return };
// G5:打开向导(版本 → 加载器 → 名字 → 确认 → 安装进度)
this.state.borrow_mut().wizard = unlucky_presenter::wizard::WizardState::open();
this.sync();
});
}
{
let weak = weak.clone();
ui.on_instance_view_changed(move |view| {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().prefs.instance_view = conv::instance_view_from(view);
this.sync();
});
}
{
let weak = weak.clone();
ui.on_tile_size_changed(move |size| {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().prefs.tile_size = conv::tile_size_from(size);
this.sync();
});
}
{
let weak = weak.clone();
ui.on_instance_query_changed(move |query| {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().instance_query = query.to_string();
this.sync();
});
}
}
fn wire_download(ui: &crate::MainWindow, weak: &Weak<Inner>) {
{
let weak = weak.clone();
ui.on_download_category_changed(move |category| {
let Some(this) = weak.upgrade() else { return };
let category = conv::version_category(category);
this.state.borrow_mut().prefs.download_category = category;
this.dispatch(AppCommand::ListVersions(category));
this.sync();
});
}
{
let weak = weak.clone();
ui.on_download_install(move |id| {
let Some(this) = weak.upgrade() else { return };
this.dispatch(AppCommand::InstallVersion(id.to_string()));
this.sync();
});
}
{
let weak = weak.clone();
ui.on_installed_only_changed(move |only| {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().installed_only = only;
this.sync();
});
}
{
let weak = weak.clone();
ui.on_download_query_changed(move |query| {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().download_query = query.to_string();
this.sync();
});
}
{
let weak = weak.clone();
ui.on_ancient_help(move || {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().toasts.push(
ToastKind::Info,
"远古版(classic/alpha/beta)需旧版 Java 与旧启动参数;完整说明页 P1 接入",
);
this.sync();
});
}
}
fn wire_logs(ui: &crate::MainWindow, weak: &Weak<Inner>) {
{
let weak = weak.clone();
ui.on_log_select(move |id| {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().selected_session = Some(id.to_string());
this.dispatch(AppCommand::OpenLogSession(id.to_string()));
this.sync();
});
}
{
let weak = weak.clone();
ui.on_log_ask_ai(move |id| {
let Some(this) = weak.upgrade() else { return };
let question = format!("帮我看下会话 {id} 为什么崩溃");
this.state.borrow_mut().navigate(Route::Agent);
this.ask_agent(&question);
this.sync();
});
}
{
let weak = weak.clone();
ui.on_log_apply_fix(move || {
let Some(this) = weak.upgrade() else { return };
this.state
.borrow_mut()
.toasts
.push(ToastKind::Warn, "一键修复在 P1 接 diagnose 的 fix_action");
this.sync();
});
}
}
fn wire_settings(ui: &crate::MainWindow, weak: &Weak<Inner>) {
// 主题三态 + 外观四件在 appearance_bind.rs(拆文件守 400 行)
crate::appearance_bind::wire(ui, weak);
{
let weak = weak.clone();
ui.on_yolo_changed(move |enabled| {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().ai.yolo = enabled;
this.dispatch(AppCommand::SetYolo(enabled));
this.sync();
});
}
{
let weak = weak.clone();
ui.on_mcp_changed(move |_enabled| {
let Some(this) = weak.upgrade() else { return };
this.state
.borrow_mut()
.toasts
.push(ToastKind::Info, "MCP server 开关在 P1 接 agent");
this.sync();
});
}
// P0 只保留入口:设置分组与 AI 端点暂不落状态(P1 接 setting schema)。
ui.on_settings_section_changed(|_section| {});
ui.on_ai_endpoint_changed(|_index| {});
}
fn wire_overlays(ui: &crate::MainWindow, weak: &Weak<Inner>) {
{
let weak = weak.clone();
ui.on_dialog_primary(move || close_current_dialog(&weak));
}
{
let weak = weak.clone();
ui.on_dialog_secondary(move || close_current_dialog(&weak));
}
{
let weak = weak.clone();
ui.on_dialog_dismissed(move || close_current_dialog(&weak));
}
{
let weak = weak.clone();
ui.on_toast_dismissed(move |id| {
let Some(this) = weak.upgrade() else { return };
if let Ok(id) = id.parse::<u64>() {
this.state.borrow_mut().toasts.dismiss(id);
this.sync();
}
});
}
}
fn close_current_dialog(weak: &Weak<Inner>) {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().dialogs.close_current();
this.sync();
}