guise/ai/
streamingtext.rs1use std::sync::OnceLock;
18use std::time::{Duration, Instant};
19
20use gpui::prelude::*;
21use gpui::{canvas, div, fill, px, App, Bounds, IntoElement, Pixels, SharedString, Window};
22
23use crate::devtools::Probed;
24use crate::frameclock::{request_frame, FrameKind};
25use crate::markdown::Markdown;
26use crate::theme::{theme, Size};
27
28const BLINK_MS: u64 = 900;
30
31fn animation_start() -> Instant {
32 static START: OnceLock<Instant> = OnceLock::new();
33 *START.get_or_init(Instant::now)
34}
35
36fn request_next_toggle(window: &mut Window, cx: &mut App, after_ms: u64) {
37 request_frame(
38 FrameKind::Caret,
39 Duration::from_millis(after_ms.max(1)),
40 window,
41 cx,
42 );
43}
44
45#[derive(IntoElement)]
47pub struct AIStreamingText {
48 text: SharedString,
49 size: Size,
50 caret: bool,
51}
52
53impl AIStreamingText {
54 pub fn new(text: impl Into<SharedString>) -> Self {
55 AIStreamingText {
56 text: text.into(),
57 size: Size::Sm,
58 caret: true,
59 }
60 }
61
62 pub fn size(mut self, size: Size) -> Self {
63 self.size = size;
64 self
65 }
66
67 pub fn caret(mut self, caret: bool) -> Self {
70 self.caret = caret;
71 self
72 }
73}
74
75impl RenderOnce for AIStreamingText {
76 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
77 let t = theme(cx);
78 let font = t.font_size(self.size);
79 let caret_color = t.text().hsla();
80
81 let caret = canvas(
85 |_, _, _| (),
86 move |bounds: Bounds<Pixels>, _, window, cx| {
87 if !bounds.intersects(&window.content_mask().bounds) {
88 return;
89 }
90 let elapsed = animation_start().elapsed().as_millis() as u64 % BLINK_MS;
91 let half = BLINK_MS / 2;
92 if elapsed < half {
93 window.paint_quad(fill(bounds, caret_color));
94 request_next_toggle(window, cx, half - elapsed);
95 } else {
96 request_next_toggle(window, cx, BLINK_MS - elapsed);
97 }
98 },
99 )
100 .w(px(font * 0.5))
101 .h(px(font * 1.1));
102
103 div()
104 .flex()
105 .flex_col()
106 .w_full()
107 .child(Markdown::new(self.text).size(self.size))
108 .when(self.caret, |column| {
109 column.child(div().flex().items_center().h(px(font * 1.3)).child(caret))
110 })
111 .probe("AIStreamingText")
112 }
113}