From a0787a579e20c467cc4a9cd53e87f4dd534a5ed1 Mon Sep 17 00:00:00 2001 From: Fini Date: Mon, 1 Jun 2026 07:12:12 +0800 Subject: [PATCH] fix(ai): maximize rust web chat panel --- crates/op-host-web/src/widget_host.rs | 58 ++-------------- .../src/widget_host/ai_chat_geometry.rs | 66 +++++++++++++++++++ .../src/widget_host/ai_chat_geometry_tests.rs | 27 ++++++++ 3 files changed, 97 insertions(+), 54 deletions(-) create mode 100644 crates/op-host-web/src/widget_host/ai_chat_geometry.rs create mode 100644 crates/op-host-web/src/widget_host/ai_chat_geometry_tests.rs diff --git a/crates/op-host-web/src/widget_host.rs b/crates/op-host-web/src/widget_host.rs index 495db869f..678ef573e 100644 --- a/crates/op-host-web/src/widget_host.rs +++ b/crates/op-host-web/src/widget_host.rs @@ -35,10 +35,8 @@ //! Functions that pull in `op_editor_ui::widgets::*` MUST live //! in this file (per spec §1.4). Phase B4 boundary check enforces. -use op_editor_core::ChatAnchor; use op_editor_ui::widgets::{ - LayoutCx, LocalePicker, Toolbar, TopBar, Widget, AI_CHAT_COLLAPSED_HEIGHT, - AI_CHAT_COLLAPSED_WIDTH, AI_CHAT_HEIGHT, AI_CHAT_WIDTH, LOCALE_PICKER_WIDTH, TOOLBAR_WIDTH, + LayoutCx, LocalePicker, Toolbar, TopBar, Widget, LOCALE_PICKER_WIDTH, TOOLBAR_WIDTH, TOP_BAR_HEIGHT, }; use op_editor_ui::{Point2D, Rect, Theme}; @@ -46,6 +44,9 @@ use op_editor_ui::{Point2D, Rect, Theme}; mod agent_settings_press; #[cfg(test)] mod agent_settings_press_tests; +mod ai_chat_geometry; +#[cfg(test)] +mod ai_chat_geometry_tests; mod boolean_ops; #[cfg(test)] mod boolean_toolbar_tests; @@ -719,14 +720,6 @@ impl WidgetHost { // `widget_host/keyboard.rs` — split out to keep this spine // file under the 800-line ceiling. - fn ai_chat_size(&self) -> (f32, f32) { - if self.editor_state.chat.collapsed { - (AI_CHAT_COLLAPSED_WIDTH, AI_CHAT_COLLAPSED_HEIGHT) - } else { - (AI_CHAT_WIDTH, AI_CHAT_HEIGHT) - } - } - pub(in crate::widget_host) fn locale_picker_rect(&self, viewport_w: f32) -> Rect { let top_bar_rect = Rect { origin: Point2D::new(0.0, 0.0), @@ -744,49 +737,6 @@ impl WidgetHost { } } - pub(in crate::widget_host) fn ai_chat_rect( - &self, - viewport_w: f32, - viewport_h: f32, - ) -> Option { - let (cx0, cy0, cw, ch) = self.canvas_region(viewport_w, viewport_h); - let (panel_w, panel_h) = self.ai_chat_size(); - if cw <= panel_w + AICHAT_INSET_LEFT + 16.0 || ch <= panel_h + 16.0 { - return None; - } - if let Some(d) = self.chat_drag { - return Some(Rect { - origin: Point2D::new(d.pos_x, d.pos_y), - size: Point2D::new(panel_w, panel_h), - }); - } - // `editor_state.chat.anchor` is op-editor-core's `ChatAnchor`; - // shell-core's is a structurally identical four-variant enum. - let (x, y) = match self.editor_state.chat.anchor { - op_editor_core::ChatAnchor::TopLeft => { - (cx0 + AICHAT_INSET_LEFT, cy0 + AICHAT_INSET_BOTTOM) - } - op_editor_core::ChatAnchor::TopRight => ( - cx0 + cw - panel_w - AICHAT_INSET_BOTTOM, - cy0 + AICHAT_INSET_BOTTOM, - ), - op_editor_core::ChatAnchor::BottomLeft => ( - cx0 + AICHAT_INSET_LEFT, - cy0 + ch - panel_h - AICHAT_INSET_BOTTOM, - ), - op_editor_core::ChatAnchor::BottomRight => ( - cx0 + cw - panel_w - AICHAT_INSET_BOTTOM, - cy0 + ch - panel_h - AICHAT_INSET_BOTTOM, - ), - }; - // `ChatAnchor` import kept for the `nearest` call in release. - let _ = ChatAnchor::TopLeft; - Some(Rect { - origin: Point2D::new(x, y), - size: Point2D::new(panel_w, panel_h), - }) - } - pub(in crate::widget_host) fn layer_panel_rect(&self, viewport_h: f32) -> Rect { Rect { origin: Point2D::new(0.0, TOP_BAR_HEIGHT), diff --git a/crates/op-host-web/src/widget_host/ai_chat_geometry.rs b/crates/op-host-web/src/widget_host/ai_chat_geometry.rs new file mode 100644 index 000000000..93799cfce --- /dev/null +++ b/crates/op-host-web/src/widget_host/ai_chat_geometry.rs @@ -0,0 +1,66 @@ +//! AI chat floating-panel geometry for the web host. + +use super::{WidgetHost, AICHAT_INSET_BOTTOM, AICHAT_INSET_LEFT}; +use op_editor_ui::widgets::{ + AI_CHAT_COLLAPSED_HEIGHT, AI_CHAT_COLLAPSED_WIDTH, AI_CHAT_HEIGHT, AI_CHAT_WIDTH, +}; +use op_editor_ui::{Point2D, Rect}; + +impl WidgetHost { + pub(in crate::widget_host) fn ai_chat_size(&self) -> (f32, f32) { + if self.editor_state.chat.collapsed { + (AI_CHAT_COLLAPSED_WIDTH, AI_CHAT_COLLAPSED_HEIGHT) + } else { + (AI_CHAT_WIDTH, AI_CHAT_HEIGHT) + } + } + + pub(in crate::widget_host) fn ai_chat_rect( + &self, + viewport_w: f32, + viewport_h: f32, + ) -> Option { + let (cx0, cy0, cw, ch) = self.canvas_region(viewport_w, viewport_h); + if self.editor_state.chat.maximized && !self.editor_state.chat.collapsed { + let inset = 12.0; + if cw <= inset * 2.0 + 16.0 || ch <= inset * 2.0 + 16.0 { + return None; + } + return Some(Rect { + origin: Point2D::new(cx0 + inset, cy0 + inset), + size: Point2D::new(cw - inset * 2.0, ch - inset * 2.0), + }); + } + let (panel_w, panel_h) = self.ai_chat_size(); + if cw <= panel_w + AICHAT_INSET_LEFT + 16.0 || ch <= panel_h + 16.0 { + return None; + } + if let Some(d) = self.chat_drag { + return Some(Rect { + origin: Point2D::new(d.pos_x, d.pos_y), + size: Point2D::new(panel_w, panel_h), + }); + } + let (x, y) = match self.editor_state.chat.anchor { + op_editor_core::ChatAnchor::TopLeft => { + (cx0 + AICHAT_INSET_LEFT, cy0 + AICHAT_INSET_BOTTOM) + } + op_editor_core::ChatAnchor::TopRight => ( + cx0 + cw - panel_w - AICHAT_INSET_BOTTOM, + cy0 + AICHAT_INSET_BOTTOM, + ), + op_editor_core::ChatAnchor::BottomLeft => ( + cx0 + AICHAT_INSET_LEFT, + cy0 + ch - panel_h - AICHAT_INSET_BOTTOM, + ), + op_editor_core::ChatAnchor::BottomRight => ( + cx0 + cw - panel_w - AICHAT_INSET_BOTTOM, + cy0 + ch - panel_h - AICHAT_INSET_BOTTOM, + ), + }; + Some(Rect { + origin: Point2D::new(x, y), + size: Point2D::new(panel_w, panel_h), + }) + } +} diff --git a/crates/op-host-web/src/widget_host/ai_chat_geometry_tests.rs b/crates/op-host-web/src/widget_host/ai_chat_geometry_tests.rs new file mode 100644 index 000000000..ce63e89b3 --- /dev/null +++ b/crates/op-host-web/src/widget_host/ai_chat_geometry_tests.rs @@ -0,0 +1,27 @@ +use super::WidgetHost; + +#[test] +fn ai_chat_maximize_click_expands_panel_geometry_like_ts() { + let mut host = WidgetHost::new(); + let viewport_w = 1200.0; + let viewport_h = 800.0; + let before = host + .ai_chat_rect(viewport_w, viewport_h) + .expect("chat panel visible"); + let x = before.origin.x + before.size.x - 16.0 - 50.0 + 9.0; + let y = before.origin.y + 17.0; + + assert!(host.apply_click(x, y, viewport_w, viewport_h)); + + let after = host + .ai_chat_rect(viewport_w, viewport_h) + .expect("maximized chat panel visible"); + let (cx0, cy0, cw, ch) = host.canvas_region(viewport_w, viewport_h); + assert!(host.editor_state.chat.maximized); + assert_eq!(after.origin.x, cx0 + 12.0); + assert_eq!(after.origin.y, cy0 + 12.0); + assert_eq!(after.size.x, cw - 24.0); + assert_eq!(after.size.y, ch - 24.0); + assert!(after.size.x > before.size.x); + assert!(after.size.y > before.size.y); +}