diff --git a/crates/op-editor-ui/src/widgets/ai_chat_panel.rs b/crates/op-editor-ui/src/widgets/ai_chat_panel.rs index 42ea703e9..669d49dc7 100644 --- a/crates/op-editor-ui/src/widgets/ai_chat_panel.rs +++ b/crates/op-editor-ui/src/widgets/ai_chat_panel.rs @@ -2,7 +2,7 @@ use crate::theme::Theme; use crate::widgets::ai_chat_panel_controls::{ attachment_row_hit, paint_attachment_row, ATTACHMENT_ROW_HEIGHT, }; -use crate::widgets::ai_chat_panel_paint::paint_examples; +use crate::widgets::ai_chat_panel_paint::{example_card_rects, paint_examples}; use crate::widgets::editor_state_ext::{theme_for, translate}; use crate::widgets::icons::{draw_icon, Icon}; use crate::widgets::{LayoutBox, LayoutCx, PaintCx, Widget, WidgetId}; @@ -373,20 +373,8 @@ impl<'a> AIChatPlaceholder<'a> { } if self.state.messages.is_empty() { // Examples grid hit-test (only rendered when no messages). - let card_w = (rect.size.x - PAD * 2.0 - 8.0) / 2.0; - let card_h = 70.0; - let grid_y = rect.origin.y + HEADER_HEIGHT + 32.0; - for (i, ex) in self.examples.iter().enumerate() { - let col = (i % 2) as f32; - let row = (i / 2) as f32; - let card = Rect { - origin: Point2D::new( - rect.origin.x + PAD + col * (card_w + 8.0), - grid_y + row * (card_h + 8.0), - ), - size: Point2D::new(card_w, card_h), - }; - if rect_contains(card, point) { + for (card, ex) in example_card_rects(rect).iter().zip(self.examples.iter()) { + if rect_contains(*card, point) { return Some(AIChatHit::Example(ex.prompt.clone())); } } diff --git a/crates/op-editor-ui/src/widgets/ai_chat_panel/tests.rs b/crates/op-editor-ui/src/widgets/ai_chat_panel/tests.rs index e2631c0e9..cd46f6240 100644 --- a/crates/op-editor-ui/src/widgets/ai_chat_panel/tests.rs +++ b/crates/op-editor-ui/src/widgets/ai_chat_panel/tests.rs @@ -127,6 +127,21 @@ fn hit_test_resolves_first_example_when_empty() { } } +#[test] +fn hit_test_keeps_quick_action_card_height_compact() { + let s = EditorState::new(); + let panel = AIChatPlaceholder::from_editor(&s); + let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT); + let card_w = (AI_CHAT_WIDTH - PAD * 2.0 - 8.0) / 2.0; + let compact_card_h = 58.0; + let p = Point2D::new( + PAD + card_w / 2.0, + HEADER_HEIGHT + 32.0 + compact_card_h + 4.0, + ); + + assert_eq!(panel.hit_test(rect, p), Some(AIChatHit::DragHandle)); +} + #[test] fn hit_test_header_returns_drag_handle() { let s = EditorState::new(); diff --git a/crates/op-editor-ui/src/widgets/ai_chat_panel_paint.rs b/crates/op-editor-ui/src/widgets/ai_chat_panel_paint.rs index b363c8229..0d6ffe252 100644 --- a/crates/op-editor-ui/src/widgets/ai_chat_panel_paint.rs +++ b/crates/op-editor-ui/src/widgets/ai_chat_panel_paint.rs @@ -6,7 +6,31 @@ use super::ai_chat_panel::{to_jian_color, ExampleCard, HEADER_HEIGHT, PAD}; use crate::theme::Theme; use crate::widgets::PaintCx; -use crate::{Point2D, Rect, TextLayout}; +use crate::{Color, Point2D, Rect, TextLayout}; + +pub(crate) const EXAMPLE_CARD_GAP: f32 = 8.0; +pub(crate) const EXAMPLE_CARD_HEIGHT: f32 = 58.0; +const EXAMPLE_CARD_PAD: f32 = 12.0; + +pub(crate) fn example_card_rects(rect: Rect) -> [Rect; 4] { + let grid_y = rect.origin.y + HEADER_HEIGHT + 32.0; + let card_w = (rect.size.x - PAD * 2.0 - EXAMPLE_CARD_GAP) / 2.0; + std::array::from_fn(|i| { + let col = (i % 2) as f32; + let row = (i / 2) as f32; + Rect { + origin: Point2D::new( + rect.origin.x + PAD + col * (card_w + EXAMPLE_CARD_GAP), + grid_y + row * (EXAMPLE_CARD_HEIGHT + EXAMPLE_CARD_GAP), + ), + size: Point2D::new(card_w, EXAMPLE_CARD_HEIGHT), + } + }) +} + +fn with_alpha(color: Color, a: f32) -> Color { + Color { a, ..color } +} /// Paint the empty-state hint line + the 2×2 example-card grid. pub(crate) fn paint_examples( @@ -25,26 +49,16 @@ pub(crate) fn paint_examples( Point2D::new(0.0, 0.0), ); let hint_y = rect.origin.y + HEADER_HEIGHT + 16.0; + let hint_w = cx.backend.measure_text(hint_label, 12.0); cx.backend.draw_text( &hint, - Point2D::new(rect.origin.x + rect.size.x / 2.0 - 40.0, hint_y), + Point2D::new(rect.origin.x + (rect.size.x - hint_w) / 2.0, hint_y), ); - let grid_origin_y = hint_y + 16.0; - let card_w = (rect.size.x - PAD * 2.0 - 8.0) / 2.0; - let card_h = 70.0; - for (i, ex) in examples.iter().enumerate() { - let col = (i % 2) as f32; - let row = (i / 2) as f32; - let card = Rect { - origin: Point2D::new( - rect.origin.x + PAD + col * (card_w + 8.0), - grid_origin_y + row * (card_h + 8.0), - ), - size: Point2D::new(card_w, card_h), - }; - cx.backend.fill_round_rect(card, 8.0, theme.muted); - cx.backend.stroke_round_rect(card, 8.0, theme.border, 1.0); + let card_bg = with_alpha(theme.muted, 0.3); + for (card, ex) in example_card_rects(rect).iter().zip(examples.iter()) { + cx.backend.fill_round_rect(*card, 8.0, card_bg); + cx.backend.stroke_round_rect(*card, 8.0, theme.border, 1.0); let emoji_layout = TextLayout::single_run( ex.emoji, "system-ui", @@ -54,7 +68,10 @@ pub(crate) fn paint_examples( ); cx.backend.draw_text( &emoji_layout, - Point2D::new(card.origin.x + 12.0, card.origin.y + 22.0), + Point2D::new( + card.origin.x + EXAMPLE_CARD_PAD, + card.origin.y + EXAMPLE_CARD_PAD + 10.0, + ), ); let title_layout = TextLayout::single_run( &ex.title, @@ -65,7 +82,10 @@ pub(crate) fn paint_examples( ); cx.backend.draw_text( &title_layout, - Point2D::new(card.origin.x + 36.0, card.origin.y + 22.0), + Point2D::new( + card.origin.x + EXAMPLE_CARD_PAD + 20.0, + card.origin.y + EXAMPLE_CARD_PAD + 10.0, + ), ); let subtitle_layout = TextLayout::single_run( &ex.subtitle, @@ -76,14 +96,17 @@ pub(crate) fn paint_examples( ); cx.backend.draw_text( &subtitle_layout, - Point2D::new(card.origin.x + 36.0, card.origin.y + 42.0), + Point2D::new( + card.origin.x + EXAMPLE_CARD_PAD, + card.origin.y + EXAMPLE_CARD_PAD + 28.0, + ), ); } let tip = TextLayout::single_run( tip_label, "system-ui", 10.0, - to_jian_color(theme.muted_foreground), + to_jian_color(with_alpha(theme.muted_foreground, 0.5)), Point2D::new(0.0, 0.0), ); let tip_w = cx.backend.measure_text(tip_label, 10.0); @@ -91,7 +114,12 @@ pub(crate) fn paint_examples( &tip, Point2D::new( rect.origin.x + (rect.size.x - tip_w) / 2.0, - grid_origin_y + card_h * 2.0 + 8.0 * 2.0 + 22.0, + rect.origin.y + + HEADER_HEIGHT + + 32.0 + + EXAMPLE_CARD_HEIGHT * 2.0 + + EXAMPLE_CARD_GAP * 2.0 + + 22.0, ), ); }