用户定案(D16):右侧常驻可折叠、允许拆分成独立页、中栗宽度可调(磁铁吸附)。 本 mission 只做壳:三档状态 + 布局数学 + 一份会话视图;真数据仍归 M103,一行 presenter 都不调。 - 右栗三档:Collapsed(48) / Expanded(280~360,默认 320,锚=令牌 ai_panel_width) / Detached(Route::Agent); 会话、草稿、未读在切档时一条不丢(拆出去再收回,话说了一半还在) - 布局数学:中栗 = 窗口 − 右栗 − 拖拽缝(8),下限 472(= 行宽上限 424 + 两侧页边距); 右栗一展开,页面按更窄的宽度重新排版(收缩而不是溢出);拖拽磁铁吸附到 280/320/360,超上限夹紧 - 独立页与右栗共用 conversation::view 这一份实现(输入=纯数据,输出=agent::Message) - 控件:多行输入框(Enter 发送 / Shift+Enter 换行,走 text_editor::key_binding 的 modifiers 分流)、 发送禁用态、流式中断、未读点 + 可读计数、⌄ 收起 / ⇥ 拆出 / ⇤ 收回;Esc 收栗、Tab 焦点环、Enter 激活 - 文案全部进 crates/i18n 的新域 catalog/agent.rs(+ golden 追加 20 行),零硬编码中文 - 取证:ULL_AGENT_RAIL / ULL_AGENT_FIXTURE / ULL_AGENT_WIDTH(截图走产品 bin,标题栏与三键可见) - 消息不走 pages::Message(外壳级视图),走 app::Message::Agent(..) → app/update/agent.rs - 删掉 M68 骨架回落(shell::view + shell/fallback.rs):Route::Agent 现在有真页面 门禁:fmt 0 / clippy(workspace,-D warnings) 0 / pages_demo 0 / app-iced lib 418 条全绿
98 lines
4.5 KiB
Rust
98 lines
4.5 KiB
Rust
//! 应用消息:单一大枚举。
|
||
//!
|
||
//! 纪律:本枚举是**唯一**的消息类型,`view` 产出的所有回调都必须落到这里;组件库与页面层
|
||
//! 各自用自己的 `Message`(组件契约 §2 的泛型纪律),由 `view` 做一次 `.map` 接上。
|
||
//!
|
||
//! ## 三类输入(M90)
|
||
//!
|
||
//! | 来源 | 变体 | 落点 |
|
||
//! |---|---|---|
|
||
//! | 窗口/框架 | `Boot` / `WindowReady` / `Window` / `Frame` / `Captured` / 三个 caption | [`super::update`] 直接处理 |
|
||
//! | **时间** | `Frame`(忙碌,33 ms 闸门)+ `Heartbeat`(空闲,1 Hz) | presenter 的 TICK 节拍:`backend.tick` + `drain_events` + `apply` |
|
||
//! | **页面** | `Page(pages::Message)` | 导航 → `AppState::navigate`;启动/停止 → `AppCommand` |
|
||
|
||
use iced::{window, window::Direction};
|
||
|
||
use unlucky_presenter::router::Route;
|
||
|
||
use crate::pages;
|
||
use crate::shell::NavItem;
|
||
|
||
use super::components;
|
||
|
||
/// 消息(单一大枚举)。
|
||
#[derive(Debug, Clone)]
|
||
pub enum Message {
|
||
/// 启动即发一次(`iced::application` 的 boot 闭包)。
|
||
Boot,
|
||
/// `window::latest()` 的结果。
|
||
WindowReady(Option<window::Id>),
|
||
/// 系统主题探测结果(只可能在**交互模式**出现,截图模式必有主题)。
|
||
SystemTheme(iced::theme::Mode),
|
||
/// 一帧画完。**只在需要 33 ms 节拍时订阅**:截图通道 / 有 Busy-Running 实例 / 有未过期
|
||
/// toast / 派发后的 3 s 观察窗(见 `subscription::needs_frames`)。
|
||
Frame,
|
||
/// **空闲态**的 1 Hz 心跳:`Task::perform(sleep(1s))` 的自续链,用来替代空闲时的帧循环
|
||
/// (M90 r3 / P2-2)。它保证 `theme::config_poll_due` 仍按「30 × 33 ms ≈ 1 秒」的
|
||
/// presenter 语义跑,外部 CLI/AI 改配置后主题 1 秒内生效。
|
||
Heartbeat,
|
||
/// 窗口事件(M90:`Resized` 决定布局档位,600 / 840 两条断点靠它生效)。
|
||
Window(window::Event),
|
||
/// 窗口句柄(M93:`window::run` 探测出的 raw handle;`None` = 拿不到,整窗 α 无法下发)。
|
||
WindowHandle(Option<isize>),
|
||
/// 页面消息(导航 / 启动 / 停止 / 占位动作)。
|
||
Page(pages::Message),
|
||
/// **AI 助手栏 / 独立页**的消息(M104:三档状态、输入框、拖拽、键盘)。
|
||
///
|
||
/// 为什么不走 `pages::Message`:助手栏是**外壳级视图**(它挂在任何页面右边),不是某一页的
|
||
/// 消息;塞进页面枚举还要动 `app/update/page_messages.rs` 的穷尽 match(那个文件正被未合的
|
||
/// M101 改),绕开它就少一处 rebase 面。落点是 `app/update/agent.rs`。
|
||
Agent(crate::shell::agent::Message),
|
||
/// 外壳导航轨点选(`src/shell/nav.rs` 的回调仍然发这一条;收到就翻成同一个路由跳转)。
|
||
///
|
||
/// M90 起产品界面用 `pages` 里的真 `NavigationRail`,这条变体是给**外壳回退路径**
|
||
/// (`Route::Agent`)留的——`shell` 不在本 mission 的 scope 里,不能删它的消息。
|
||
Select(NavItem),
|
||
/// 按下标题栏 → 拖窗。
|
||
DragWindow,
|
||
/// 边缘热区:开始缩放(方向由坐标决定)。
|
||
ResizeStart(Direction),
|
||
/// 最小化。
|
||
Minimize,
|
||
/// 最大化/还原。
|
||
ToggleMaximize,
|
||
/// 关窗退出。
|
||
Close,
|
||
/// 截图回来了。
|
||
Captured(window::Screenshot),
|
||
/// gallery(组件库 harness)里的演示交互。
|
||
Demo(components::gallery::DemoMessage),
|
||
}
|
||
|
||
/// 外壳导航项 → 路由(导航项只在一级页之间有对应;AI 项对应 [`Route::Agent`])。
|
||
///
|
||
/// 这是 `NavItem → Route` 的**唯一**转换点:外壳(`src/shell/nav.rs`)与页面导航轨
|
||
/// (`pages::NAV`)最终都落到同一个 `Route`,所以两条入口不会各走一套页面语义。
|
||
pub(super) fn route_of(nav: NavItem) -> Route {
|
||
match nav {
|
||
NavItem::Instances => Route::Instances,
|
||
NavItem::Download => Route::Download,
|
||
NavItem::Logs => Route::Logs,
|
||
NavItem::Settings => Route::Settings,
|
||
NavItem::Ai => Route::Agent,
|
||
}
|
||
}
|
||
|
||
/// 路由 → 外壳导航项([`route_of`] 的逆;外壳 rail 与 caption 键都按它高亮)。
|
||
///
|
||
/// `Route::Detail(_)` 高亮「实例」(与 `Route::nav()` 同源)。
|
||
pub(super) fn nav_item_of(route: &Route) -> NavItem {
|
||
match route {
|
||
Route::Instances | Route::Detail(_) => NavItem::Instances,
|
||
Route::Download => NavItem::Download,
|
||
Route::Logs => NavItem::Logs,
|
||
Route::Settings => NavItem::Settings,
|
||
Route::Agent => NavItem::Ai,
|
||
}
|
||
}
|