adabraka-ui 0.3.9

A comprehensive, professional UI component library for GPUI inspired by shadcn/ui. 80+ accessible components for building beautiful desktop applications.
use gpui::{prelude::FluentBuilder as _, *};
use std::time::Duration;

use crate::theme::use_theme;

pub struct TypeWriterState {
    full_text: SharedString,
    visible_count: usize,
    is_typing: bool,
    _cursor_visible: bool,
    version: usize,
    speed: Duration,
    on_complete: Option<Box<dyn Fn(&mut App)>>,
}

impl TypeWriterState {
    pub fn new(text: impl Into<SharedString>) -> Self {
        Self {
            full_text: text.into(),
            visible_count: 0,
            is_typing: false,
            _cursor_visible: true,
            version: 0,
            speed: Duration::from_millis(50),
            on_complete: None,
        }
    }

    pub fn with_speed(mut self, speed: Duration) -> Self {
        self.speed = speed;
        self
    }

    pub fn start(&mut self, cx: &mut Context<Self>) {
        self.visible_count = 0;
        self.is_typing = true;
        self.version += 1;
        let version = self.version;
        let total_chars = self.full_text.chars().count();
        let speed = self.speed;

        cx.spawn(async move |this, cx| {
            for i in 1..=total_chars {
                cx.background_executor().timer(speed).await;
                let should_stop = this
                    .update(cx, |state, cx| {
                        if state.version != version {
                            return true;
                        }
                        state.visible_count = i;
                        cx.notify();
                        false
                    })
                    .unwrap_or(true);

                if should_stop {
                    return;
                }
            }

            _ = this.update(cx, |state, cx| {
                if state.version != version {
                    return;
                }
                state.is_typing = false;
                if let Some(cb) = state.on_complete.take() {
                    cb(cx);
                }
                cx.notify();
            });
        })
        .detach();

        cx.notify();
    }

    pub fn reset(&mut self, cx: &mut Context<Self>) {
        self.version += 1;
        self.visible_count = 0;
        self.is_typing = false;
        cx.notify();
    }

    pub fn set_text(&mut self, text: impl Into<SharedString>, cx: &mut Context<Self>) {
        self.version += 1;
        self.full_text = text.into();
        self.visible_count = 0;
        self.is_typing = false;
        cx.notify();
    }

    pub fn on_complete(mut self, handler: impl Fn(&mut App) + 'static) -> Self {
        self.on_complete = Some(Box::new(handler));
        self
    }

    pub fn visible_text(&self) -> &str {
        let byte_end = self
            .full_text
            .char_indices()
            .nth(self.visible_count)
            .map(|(i, _)| i)
            .unwrap_or(self.full_text.len());
        &self.full_text[..byte_end]
    }

    pub fn is_typing(&self) -> bool {
        self.is_typing
    }

    pub fn is_complete(&self) -> bool {
        self.visible_count >= self.full_text.chars().count()
    }
}

#[derive(IntoElement)]
pub struct TypeWriter {
    id: ElementId,
    base: Div,
    state: Entity<TypeWriterState>,
    show_cursor: bool,
    cursor_char: char,
}

impl TypeWriter {
    pub fn new(id: impl Into<ElementId>, state: Entity<TypeWriterState>) -> Self {
        Self {
            id: id.into(),
            base: div(),
            state,
            show_cursor: true,
            cursor_char: '|',
        }
    }

    pub fn cursor(mut self, show: bool) -> Self {
        self.show_cursor = show;
        self
    }

    pub fn cursor_char(mut self, ch: char) -> Self {
        self.cursor_char = ch;
        self
    }
}

impl Styled for TypeWriter {
    fn style(&mut self) -> &mut StyleRefinement {
        self.base.style()
    }
}

impl RenderOnce for TypeWriter {
    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
        let theme = use_theme();
        let state = self.state.read(cx);
        let text = String::from(state.visible_text());
        let is_typing = state.is_typing();
        let cursor_str = SharedString::from(String::from(self.cursor_char));

        self.base
            .flex()
            .flex_row()
            .text_color(theme.tokens.foreground)
            .child(text)
            .when(
                self.show_cursor && (is_typing || !state.is_complete()),
                |el| {
                    el.child(
                        div().id(self.id).child(cursor_str).with_animation(
                            "cursor-blink",
                            Animation::new(Duration::from_millis(530))
                                .repeat()
                                .with_easing(gpui::linear),
                            |el, delta| {
                                if delta < 0.5 {
                                    el.opacity(1.0)
                                } else {
                                    el.opacity(0.0)
                                }
                            },
                        ),
                    )
                },
            )
    }
}