kael_ui 0.2.0

Professional shadcn-inspired UI component library for Kael. 100+ accessible components for building beautiful, performant desktop applications.
use crate::animations::easings;
use kael::{prelude::FluentBuilder as _, *};
use std::time::Duration;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RevealMode {
    ByWord,
    ByLine,
    ByCharacter,
}

#[derive(IntoElement)]
pub struct TextReveal {
    id: ElementId,
    text: SharedString,
    mode: RevealMode,
    stagger: Duration,
    duration: Duration,
    easing: fn(f32) -> f32,
    text_size: Option<Pixels>,
    font_weight: Option<FontWeight>,
    text_color: Option<Hsla>,
    style: StyleRefinement,
}

impl TextReveal {
    pub fn new(id: impl Into<ElementId>, text: impl Into<SharedString>) -> Self {
        Self {
            id: id.into(),
            text: text.into(),
            mode: RevealMode::ByWord,
            stagger: Duration::from_millis(50),
            duration: Duration::from_millis(400),
            easing: easings::ease_out_cubic,
            text_size: None,
            font_weight: None,
            text_color: None,
            style: StyleRefinement::default(),
        }
    }

    pub fn mode(mut self, mode: RevealMode) -> Self {
        self.mode = mode;
        self
    }

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

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

    pub fn easing(mut self, easing: fn(f32) -> f32) -> Self {
        self.easing = easing;
        self
    }

    pub fn text_size(mut self, size: Pixels) -> Self {
        self.text_size = Some(size);
        self
    }

    pub fn font_weight(mut self, weight: FontWeight) -> Self {
        self.font_weight = Some(weight);
        self
    }

    pub fn text_color(mut self, color: Hsla) -> Self {
        self.text_color = Some(color);
        self
    }
}

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

impl RenderOnce for TextReveal {
    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
        let segments: Vec<String> = match self.mode {
            RevealMode::ByWord => self
                .text
                .split_whitespace()
                .map(|s| s.to_string())
                .collect(),
            RevealMode::ByLine => self.text.lines().map(|s| s.to_string()).collect(),
            RevealMode::ByCharacter => self.text.chars().map(|c| c.to_string()).collect(),
        };

        let is_line_mode = self.mode == RevealMode::ByLine;
        let mode = self.mode;
        let stagger = self.stagger;
        let duration = self.duration;
        let easing = self.easing;
        let user_style = self.style;

        let mut container = if is_line_mode {
            div().flex().flex_col()
        } else {
            div().flex().flex_row().flex_wrap()
        };

        container = container.map(|mut el| {
            el.style().refine(&user_style);
            el
        });

        if let Some(size) = self.text_size {
            container = container.text_size(size);
        }
        if let Some(weight) = self.font_weight {
            container = container.font_weight(weight);
        }
        if let Some(color) = self.text_color {
            container = container.text_color(color);
        }

        for (i, segment) in segments.into_iter().enumerate() {
            let seg_id: ElementId = ElementId::Name(format!("{}-seg-{}", self.id, i).into());
            let delay = stagger.as_millis() as u64 * i as u64;
            let total_duration = Duration::from_millis(duration.as_millis() as u64 + delay);

            let display_text: SharedString = if mode == RevealMode::ByWord && i > 0 {
                format!(" {}", segment).into()
            } else {
                segment.into()
            };

            let seg_el = div().id(seg_id.clone()).child(display_text).with_animation(
                seg_id,
                Animation::new(total_duration).with_easing(easing),
                move |el, delta| {
                    let seg_t = if total_duration.as_millis() > 0 {
                        let delay_fraction = delay as f32 / total_duration.as_millis() as f32;
                        ((delta - delay_fraction) / (1.0 - delay_fraction)).clamp(0.0, 1.0)
                    } else {
                        1.0
                    };

                    let offset = (1.0 - seg_t) * 8.0;
                    el.opacity(seg_t).mt(px(offset))
                },
            );

            container = container.child(seg_el);
        }

        container
    }
}