use std::sync::OnceLock;
use std::time::{Duration, Instant};
use gpui::prelude::*;
use gpui::{canvas, div, fill, px, App, Bounds, IntoElement, Pixels, SharedString, Window};
use crate::devtools::Probed;
use crate::frameclock::{request_frame, FrameKind};
use crate::markdown::Markdown;
use crate::theme::{theme, Size};
const BLINK_MS: u64 = 900;
fn animation_start() -> Instant {
static START: OnceLock<Instant> = OnceLock::new();
*START.get_or_init(Instant::now)
}
fn request_next_toggle(window: &mut Window, cx: &mut App, after_ms: u64) {
request_frame(
FrameKind::Caret,
Duration::from_millis(after_ms.max(1)),
window,
cx,
);
}
#[derive(IntoElement)]
pub struct AIStreamingText {
text: SharedString,
size: Size,
caret: bool,
}
impl AIStreamingText {
pub fn new(text: impl Into<SharedString>) -> Self {
AIStreamingText {
text: text.into(),
size: Size::Sm,
caret: true,
}
}
pub fn size(mut self, size: Size) -> Self {
self.size = size;
self
}
pub fn caret(mut self, caret: bool) -> Self {
self.caret = caret;
self
}
}
impl RenderOnce for AIStreamingText {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let t = theme(cx);
let font = t.font_size(self.size);
let caret_color = t.text().hsla();
let caret = canvas(
|_, _, _| (),
move |bounds: Bounds<Pixels>, _, window, cx| {
if !bounds.intersects(&window.content_mask().bounds) {
return;
}
let elapsed = animation_start().elapsed().as_millis() as u64 % BLINK_MS;
let half = BLINK_MS / 2;
if elapsed < half {
window.paint_quad(fill(bounds, caret_color));
request_next_toggle(window, cx, half - elapsed);
} else {
request_next_toggle(window, cx, BLINK_MS - elapsed);
}
},
)
.w(px(font * 0.5))
.h(px(font * 1.1));
div()
.flex()
.flex_col()
.w_full()
.child(Markdown::new(self.text).size(self.size))
.when(self.caret, |column| {
column.child(div().flex().items_center().h(px(font * 1.3)).child(caret))
})
.probe("AIStreamingText")
}
}