fix(ai): tighten empty chat cards

This commit is contained in:
Fini 2026-05-30 00:56:28 +08:00
parent c0dada1a59
commit dec0c94710
3 changed files with 68 additions and 37 deletions

View file

@ -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()));
}
}

View file

@ -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();

View file

@ -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,
),
);
}