fix(ai): wrap chat quick action cards
This commit is contained in:
parent
c80491b4ed
commit
28dab69245
|
|
@ -225,18 +225,18 @@ fn hit_test_resolves_first_example_when_empty() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn hit_test_keeps_quick_action_card_height_compact() {
|
||||
let s = EditorState::new();
|
||||
fn hit_test_uses_taller_ts_quick_action_card_height() {
|
||||
let mut s = EditorState::new();
|
||||
seed_available_model(&mut s);
|
||||
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,
|
||||
);
|
||||
let p = Point2D::new(PAD + card_w / 2.0, HEADER_HEIGHT + 32.0 + 64.0);
|
||||
|
||||
assert_eq!(panel.hit_test(rect, p), Some(AIChatHit::DragHandle));
|
||||
match panel.hit_test(rect, p) {
|
||||
Some(AIChatHit::Example(prompt)) => assert_eq!(prompt, panel.examples[0].prompt),
|
||||
other => panic!("expected first example hit in taller TS-style card, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@ use crate::widgets::PaintCx;
|
|||
use crate::{Color, Point2D, Rect, TextLayout};
|
||||
|
||||
pub(crate) const EXAMPLE_CARD_GAP: f32 = 8.0;
|
||||
pub(crate) const EXAMPLE_CARD_HEIGHT: f32 = 58.0;
|
||||
pub(crate) const EXAMPLE_CARD_HEIGHT: f32 = 72.0;
|
||||
const EXAMPLE_CARD_PAD: f32 = 12.0;
|
||||
const EXAMPLE_TITLE_FONT: f32 = 12.0;
|
||||
const EXAMPLE_SUBTITLE_FONT: f32 = 10.0;
|
||||
const EXAMPLE_SUBTITLE_LINE_H: f32 = 12.0;
|
||||
|
||||
pub(crate) fn example_card_rects(rect: Rect) -> [Rect; 4] {
|
||||
let grid_y = rect.origin.y + HEADER_HEIGHT + 32.0;
|
||||
|
|
@ -32,6 +35,51 @@ fn with_alpha(color: Color, a: f32) -> Color {
|
|||
Color { a, ..color }
|
||||
}
|
||||
|
||||
fn wrapped_lines(
|
||||
cx: &mut PaintCx<'_>,
|
||||
value: &str,
|
||||
max_w: f32,
|
||||
size: f32,
|
||||
max_lines: usize,
|
||||
) -> Vec<String> {
|
||||
if max_lines == 0 || value.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let chars: Vec<char> = value.chars().collect();
|
||||
let mut lines = Vec::new();
|
||||
let mut idx = 0;
|
||||
while idx < chars.len() && lines.len() < max_lines {
|
||||
while idx < chars.len() && chars[idx].is_whitespace() {
|
||||
idx += 1;
|
||||
}
|
||||
let mut line = String::new();
|
||||
while idx < chars.len() {
|
||||
let candidate = format!("{}{}", line, chars[idx]);
|
||||
if !line.is_empty() && cx.backend.measure_text(&candidate, size) > max_w {
|
||||
break;
|
||||
}
|
||||
line.push(chars[idx]);
|
||||
idx += 1;
|
||||
}
|
||||
if line.is_empty() && idx < chars.len() {
|
||||
line.push(chars[idx]);
|
||||
idx += 1;
|
||||
}
|
||||
if !line.is_empty() {
|
||||
lines.push(line.trim_end().to_string());
|
||||
}
|
||||
}
|
||||
if idx < chars.len() {
|
||||
if let Some(last) = lines.last_mut() {
|
||||
while !last.is_empty() && cx.backend.measure_text(&format!("{last}..."), size) > max_w {
|
||||
last.pop();
|
||||
}
|
||||
last.push_str("...");
|
||||
}
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
/// Paint the floating chat panel shell. TS uses
|
||||
/// `rounded-xl border bg-card/95 shadow-2xl backdrop-blur-sm`;
|
||||
/// Skia here has no blur primitive, so we layer translucent rounded
|
||||
|
|
@ -123,34 +171,60 @@ pub(crate) fn paint_examples(
|
|||
card.origin.y + EXAMPLE_CARD_PAD + 10.0,
|
||||
),
|
||||
);
|
||||
let title_layout = TextLayout::single_run(
|
||||
let title_lines = wrapped_lines(
|
||||
cx,
|
||||
&ex.title,
|
||||
"system-ui",
|
||||
12.0,
|
||||
to_jian_color(title_color),
|
||||
Point2D::new(0.0, 0.0),
|
||||
card.size.x - EXAMPLE_CARD_PAD * 2.0 - 20.0,
|
||||
EXAMPLE_TITLE_FONT,
|
||||
2,
|
||||
);
|
||||
cx.backend.draw_text(
|
||||
&title_layout,
|
||||
Point2D::new(
|
||||
card.origin.x + EXAMPLE_CARD_PAD + 20.0,
|
||||
card.origin.y + EXAMPLE_CARD_PAD + 10.0,
|
||||
),
|
||||
);
|
||||
let subtitle_layout = TextLayout::single_run(
|
||||
for (line_index, line) in title_lines.iter().enumerate() {
|
||||
let title_layout = TextLayout::single_run(
|
||||
line,
|
||||
"system-ui",
|
||||
EXAMPLE_TITLE_FONT,
|
||||
to_jian_color(title_color),
|
||||
Point2D::new(0.0, 0.0),
|
||||
);
|
||||
cx.backend.draw_text(
|
||||
&title_layout,
|
||||
Point2D::new(
|
||||
card.origin.x + EXAMPLE_CARD_PAD + 20.0,
|
||||
card.origin.y
|
||||
+ EXAMPLE_CARD_PAD
|
||||
+ 10.0
|
||||
+ line_index as f32 * EXAMPLE_SUBTITLE_LINE_H,
|
||||
),
|
||||
);
|
||||
}
|
||||
let title_extra = title_lines.len().saturating_sub(1) as f32 * EXAMPLE_SUBTITLE_LINE_H;
|
||||
let subtitle_lines = wrapped_lines(
|
||||
cx,
|
||||
&ex.subtitle,
|
||||
"system-ui",
|
||||
11.0,
|
||||
to_jian_color(subtitle_color),
|
||||
Point2D::new(0.0, 0.0),
|
||||
);
|
||||
cx.backend.draw_text(
|
||||
&subtitle_layout,
|
||||
Point2D::new(
|
||||
card.origin.x + EXAMPLE_CARD_PAD,
|
||||
card.origin.y + EXAMPLE_CARD_PAD + 28.0,
|
||||
),
|
||||
card.size.x - EXAMPLE_CARD_PAD * 2.0,
|
||||
EXAMPLE_SUBTITLE_FONT,
|
||||
2,
|
||||
);
|
||||
for (line_index, line) in subtitle_lines.iter().enumerate() {
|
||||
let subtitle_layout = TextLayout::single_run(
|
||||
line,
|
||||
"system-ui",
|
||||
EXAMPLE_SUBTITLE_FONT,
|
||||
to_jian_color(subtitle_color),
|
||||
Point2D::new(0.0, 0.0),
|
||||
);
|
||||
cx.backend.draw_text(
|
||||
&subtitle_layout,
|
||||
Point2D::new(
|
||||
card.origin.x + EXAMPLE_CARD_PAD,
|
||||
card.origin.y
|
||||
+ EXAMPLE_CARD_PAD
|
||||
+ 29.0
|
||||
+ title_extra
|
||||
+ line_index as f32 * EXAMPLE_SUBTITLE_LINE_H,
|
||||
),
|
||||
);
|
||||
}
|
||||
cx.backend.restore();
|
||||
}
|
||||
let tip = TextLayout::single_run(
|
||||
|
|
@ -169,7 +243,7 @@ pub(crate) fn paint_examples(
|
|||
+ HEADER_HEIGHT
|
||||
+ 32.0
|
||||
+ EXAMPLE_CARD_HEIGHT * 2.0
|
||||
+ EXAMPLE_CARD_GAP * 2.0
|
||||
+ EXAMPLE_CARD_GAP
|
||||
+ 22.0,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue