fix(app-iced): M104 r2 —— Route::Agent ⟺ Detached 的不变式 + 复审 P3 六条
p2-1(唯一 p2):`Route::Agent` 与 `RailState::Detached` 之前没有绑定——从左侧导航第五项 (本 mission 点名的入口)进独立页时档位还是 Collapsed/Expanded,于是页头出现「Open assistant」 「Collapse/Pop out」、挂着不化的未读胶囊、Esc 也收不走页面(复审用产品 bin 复现)。 - **单一入口**:`App::sync_agent_route()`(update/agent.rs)在 `refresh_view` 开头调用, 两条对向规则:在独立页 ⇒ Detached(顺带清未读);离开独立页而档位还是 Detached ⇒ 收回右栗。 每一次路由变化都经过 refresh_view,所以导航 / ULL_START_ROUTE / detach / back 共用这一处。 - **结构性兜底**:`conversation::actions` 先按宿主分流,`Host::Page` 的动作恒为「Dock back」。 - **启动顺序**:`boot_agent()`(窗口尺寸 adopt + 拆出档直达独立页)必须在 `dispatch(RefreshAll)` 之前——那次 dispatch 会 pump→auto_select 把路由拉到实例详情,同步器随即把档位收回(实测踩到)。 - 测试:`entering_the_agent_route_detaches_the_rail`(走 `Message::Select(NavItem::Ai)` 真路径, 含未读清零 / Esc 离开 / 从页上点别的导航收回)、`the_page_navigation_entry_also_detaches`。 变异自证:把 `sync_agent_route` 改成 no-op ⇒ 3 红(含这两条)。 - 图 13/14:导航入口(折叠档带 2 条未读 / 展开档)进页后都是 Dock back、无未读胶囊、副标题单行。 P3-2 Enter/Shift+Enter 有测试了:分流抽成纯函数 `composer::binding_for`, `enter_sends_and_shift_enter_breaks_the_line` 覆盖「发送 / 换行 / 未聚焦不抢 / 其它键交回默认」; `AgentUi::edit` 里与 fallthrough 等价的冗余 Enter 分支删掉;三档测试改走 `agent.edit(...)`。 P3-4 自证式断言换字面量(320+8+852=1180 等);变异 RESIZE_GUTTER 8→0 ⇒ 6 红(含这条)。 P3-5 两条死文案真用上:8px 拖拽缝有了 tooltip(AgentResize + AgentWidthLabel「当前多宽」)。 P3-6 `on_boot` 的两块逻辑搬进 `update/agent.rs::boot_agent`:update.rs 372→383(+11,见报告逐行)。 P3-7 拖拽层补 on_press/on_exit:窗外松手不再僵住(窗内按下→清态;离窗→收在当前值)。 P3-9 顺手:只有几何/路由会变的消息才重建视图模型(每个字符、每次指针移动不再重建)。 门禁:fmt 0 / clippy(workspace,-D warnings) 0 / pages_demo 0 / 全文件 ≤400(最大 view.rs 373 之外 conversation.rs 383、update.rs 383)
This commit is contained in:
1 parent
8db1c5ab16
commit
9253684387
11 files changed
+495
-208
No files matched your search
@@ -0,0 +1,137 @@
|
||||
//! 拖拽与「缝」的判据(从 `tests.rs` 拆出来:那边加完复审 r1 的断言后顶到 436 行,
|
||||
//! 而 AGENTS.md §3-1 是 400 行硬线)。
|
||||
//!
|
||||
//! 拆的位置是「指针/几何」这一族:拖拽三条收尾路径、吸附宽度写回、缝的预算、缝与折叠条的标签。
|
||||
|
||||
use super::super::metrics::{layout_for, PRESETS, RESIZE_GUTTER};
|
||||
use super::super::state::{AgentUi, RailState};
|
||||
|
||||
use super::super::{docked, drag_overlay};
|
||||
use super::ui;
|
||||
|
||||
/// 拖拽层只在拖拽期间存在(否则它会吃掉整窗的点击)。
|
||||
#[test]
|
||||
fn the_drag_overlay_only_exists_while_dragging() {
|
||||
let mut agent = AgentUi::new();
|
||||
agent.expand();
|
||||
assert!(drag_overlay(&agent).is_none());
|
||||
|
||||
agent.drag_start();
|
||||
assert!(agent.is_dragging());
|
||||
assert!(drag_overlay(&agent).is_some());
|
||||
|
||||
agent.drag_end();
|
||||
assert!(drag_overlay(&agent).is_none());
|
||||
}
|
||||
|
||||
/// 拆出档右栗必须**不占地**(否则会话会在两个地方各画一份)。
|
||||
#[test]
|
||||
fn the_detached_state_frees_the_rail_slot() {
|
||||
let ui = ui();
|
||||
let mut agent = AgentUi::new();
|
||||
agent.detach();
|
||||
assert!(docked(&ui, &agent, 1180.0).is_none());
|
||||
assert_eq!(agent.rail_width(1180.0), 0.0);
|
||||
assert_eq!(agent.content_width(1180.0), 1180.0);
|
||||
}
|
||||
|
||||
/// 拖拽把「展开宽度」写进状态(松手后保有该宽度)。
|
||||
#[test]
|
||||
fn the_drag_writes_back_the_snapped_width() {
|
||||
let mut agent = AgentUi::new();
|
||||
agent.expand();
|
||||
agent.drag_start();
|
||||
agent.drag_to(1180.0, 1180.0 - 280.0);
|
||||
agent.drag_end();
|
||||
assert_eq!(agent.expanded(), 280.0);
|
||||
assert!(!agent.is_dragging());
|
||||
|
||||
// 没按下就收到的 DragTo(例如上一步松手后的残留)不该改宽度
|
||||
let before = agent.expanded();
|
||||
agent.drag_to(1180.0, 100.0);
|
||||
assert_eq!(agent.expanded(), before);
|
||||
|
||||
// 三档预设都能被拖到(1180 下上限 612 > 360)
|
||||
for preset in PRESETS {
|
||||
agent.drag_start();
|
||||
agent.drag_to(1180.0, 1180.0 - preset);
|
||||
agent.drag_end();
|
||||
assert_eq!(agent.expanded(), preset);
|
||||
}
|
||||
}
|
||||
|
||||
/// 面板与中栗之间的缝在布局里被真的留出来了。
|
||||
///
|
||||
/// **全用字面量**(复审 P3-4:早先写成 `rail + GUTTER == window − content`,两边同一组常量,
|
||||
/// 把 `RESIZE_GUTTER` 改成 0 照样绿 = 自证)。现在的算式是写死的:
|
||||
/// `320(面板)+ 8(缝)+ 852(中栗)= 1180`。
|
||||
#[test]
|
||||
fn the_resize_gutter_is_part_of_the_budget() {
|
||||
let mut agent = AgentUi::new();
|
||||
agent.expand();
|
||||
|
||||
assert_eq!(agent.rail_width(1180.0), 320.0, "1180 下展开档 = 令牌 320");
|
||||
assert_eq!(agent.content_width(1180.0), 852.0, "中栗 = 1180 − 320 − 8");
|
||||
assert_eq!(320.0 + 8.0 + 852.0, 1180.0, "三段之和必须回到窗口宽");
|
||||
assert_eq!(RESIZE_GUTTER, 8.0, "缝宽就是 8(`Space::X2`)");
|
||||
|
||||
// 折叠档 48 / 拆出档 0:这两档**没有**缝(中栗 = 1180 − 48 / 1180 − 0)
|
||||
let collapsed = layout_for(1180.0, RailState::Collapsed, 320.0);
|
||||
assert_eq!(
|
||||
(collapsed.rail, collapsed.gutter, collapsed.content),
|
||||
(48.0, 0.0, 1132.0)
|
||||
);
|
||||
let detached = layout_for(1180.0, RailState::Detached, 320.0);
|
||||
assert_eq!(
|
||||
(detached.rail, detached.gutter, detached.content),
|
||||
(0.0, 0.0, 1180.0)
|
||||
);
|
||||
}
|
||||
|
||||
/// 拖拽的三条收尾路径里,**能测的两条**(复审 P3-7)。
|
||||
///
|
||||
/// 真实鼠标在窗口外松手无法注入(这台机器连 `SetCursorPos` 都失败),所以这里只钉状态机:
|
||||
/// ①松手(`DragEnd`)之后指针再动不改宽度;②拖拽中「再按一次」(叠层的 `on_press`)能清态,
|
||||
/// 也就是上一条事件丢了的兜底。叠层本身挂了 `on_exit`(指针离窗 = 收在当前值),那条只能真窗口验。
|
||||
#[test]
|
||||
fn the_drag_recovers_when_a_release_is_missed() {
|
||||
let mut agent = AgentUi::new();
|
||||
agent.expand();
|
||||
|
||||
// ① 正常松手
|
||||
agent.drag_start();
|
||||
agent.drag_to(1180.0, 1180.0 - 360.0);
|
||||
agent.drag_end();
|
||||
assert_eq!(agent.expanded(), 360.0);
|
||||
agent.drag_to(1180.0, 900.0);
|
||||
assert_eq!(agent.expanded(), 360.0, "松手后指针再动不该改宽度");
|
||||
|
||||
// ② 拖拽中「再按一次」= 叠层的 on_press 兜底:清态,后续 DragTo 不再改宽度
|
||||
agent.drag_start();
|
||||
agent.drag_to(1180.0, 1180.0 - 280.0);
|
||||
assert_eq!(agent.expanded(), 280.0);
|
||||
agent.drag_end(); // ← 叠层 on_press → DragEnd
|
||||
assert!(
|
||||
!agent.is_dragging(),
|
||||
"再按一次必须能清态(窗内松手丢失时的兜底)"
|
||||
);
|
||||
agent.drag_to(1180.0, 100.0);
|
||||
assert_eq!(agent.expanded(), 280.0);
|
||||
assert!(drag_overlay(&agent).is_none(), "清态后叠层必须消失");
|
||||
}
|
||||
|
||||
/// 折叠条与拖拽缝都有可读文字(复审 P3-5:两条 key 以前在视图里一次没用)。
|
||||
///
|
||||
/// 抓得住:谁把 `AgentResize` / `AgentWidthLabel` 从视图里删掉(那样它们又变回空头支票)。
|
||||
/// 抓不住:tooltip 真弹出来长什么样(那要 hover 出图)。
|
||||
#[test]
|
||||
fn the_gutter_and_strip_are_labelled() {
|
||||
let hosts = include_str!("../mod.rs");
|
||||
for key in [
|
||||
"Key::AgentResize",
|
||||
"Key::AgentWidthLabel",
|
||||
"Key::AgentStripLabel",
|
||||
] {
|
||||
assert!(hosts.contains(key), "视图里没用上 {key}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user