kael_ui 0.2.0

Professional shadcn-inspired UI component library for Kael. 100+ accessible components for building beautiful, performant desktop applications.
//! Pre-configured celebration particle burst.

use kael::{prelude::FluentBuilder as _, *};
use std::time::Duration;

const DEFAULT_CONFETTI_COLORS: [u32; 6] =
    [0xFF6B6B, 0x4ECDC4, 0x45B7D1, 0xFFA07A, 0x98D8C8, 0xF7DC6F];

#[derive(Clone)]
pub struct ConfettiParticle {
    pub position: Point<f32>,
    pub velocity: Point<f32>,
    pub rotation_speed: f32,
    pub size: f32,
    pub color: Hsla,
    pub age: f32,
    pub lifetime: f32,
}

pub struct ConfettiState {
    is_active: bool,
    particles: Vec<ConfettiParticle>,
    particle_count: usize,
    colors: Vec<Hsla>,
    gravity: f32,
    origin: Point<f32>,
    spread: f32,
}

impl ConfettiState {
    pub fn new(_cx: &mut Context<Self>) -> Self {
        Self {
            is_active: false,
            particles: Vec::new(),
            particle_count: 80,
            colors: DEFAULT_CONFETTI_COLORS
                .iter()
                .map(|&c| {
                    let color: Rgba = rgb(c);
                    Hsla::from(color)
                })
                .collect(),
            gravity: 120.0,
            origin: Point { x: 0.5, y: 0.5 },
            spread: 300.0,
        }
    }

    pub fn set_particle_count(&mut self, count: usize) {
        self.particle_count = count;
    }

    pub fn set_colors(&mut self, colors: Vec<Hsla>) {
        if !colors.is_empty() {
            self.colors = colors;
        }
    }

    pub fn set_gravity(&mut self, gravity: f32) {
        self.gravity = gravity;
    }

    pub fn set_origin(&mut self, origin: Point<f32>) {
        self.origin = origin;
    }

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

    pub fn burst(&mut self, cx: &mut Context<Self>) {
        self.particles.clear();
        self.is_active = true;

        let count = self.particle_count;
        let color_count = self.colors.len().max(1);

        for i in 0..count {
            let seed = i as u32;
            let angle = pseudo_random_f32(seed) * std::f32::consts::TAU;
            let speed = self.spread * (0.3 + pseudo_random_f32(seed + 3) * 0.7);

            let vx = angle.cos() * speed;
            let vy = angle.sin() * speed - self.spread * 0.5;

            let color_idx =
                (pseudo_random_f32(seed + 7) * color_count as f32) as usize % color_count;
            let particle_size = 4.0 + pseudo_random_f32(seed + 11) * 6.0;
            let rotation_spd = (pseudo_random_f32(seed + 17) - 0.5) * 10.0;
            let lifetime = 1.5 + pseudo_random_f32(seed + 23) * 1.5;

            self.particles.push(ConfettiParticle {
                position: Point {
                    x: self.origin.x,
                    y: self.origin.y,
                },
                velocity: Point { x: vx, y: vy },
                rotation_speed: rotation_spd,
                size: particle_size,
                color: self.colors[color_idx],
                age: 0.0,
                lifetime,
            });
        }

        self.schedule_tick(cx);
        cx.notify();
    }

    fn update_particles(&mut self, dt: f32) {
        let gravity = self.gravity;

        for particle in &mut self.particles {
            particle.age += dt;
            particle.velocity.y += gravity * dt;
            particle.velocity.x *= 0.99;
            particle.position.x += particle.velocity.x * dt;
            particle.position.y += particle.velocity.y * dt;
        }

        self.particles.retain(|p| p.age < p.lifetime);

        if self.particles.is_empty() {
            self.is_active = false;
        }
    }

    pub fn particles(&self) -> &[ConfettiParticle] {
        &self.particles
    }

    fn schedule_tick(&self, cx: &mut Context<Self>) {
        if !self.is_active {
            return;
        }

        cx.spawn(async |this, cx| {
            cx.background_executor()
                .timer(Duration::from_millis(16))
                .await;

            _ = this.update(cx, |state, cx| {
                if !state.is_active {
                    return;
                }

                let dt = 1.0 / 60.0;
                state.update_particles(dt);

                if state.is_active {
                    state.schedule_tick(cx);
                }

                cx.notify();
            });
        })
        .detach();
    }
}

impl Render for ConfettiState {
    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
        div()
    }
}

struct ConfettiPaintData {
    particles: Vec<ConfettiParticle>,
}

#[derive(IntoElement)]
pub struct Confetti {
    id: ElementId,
    state: Entity<ConfettiState>,
    style: StyleRefinement,
}

impl Confetti {
    pub fn new(id: impl Into<ElementId>, state: Entity<ConfettiState>) -> Self {
        Self {
            id: id.into(),
            state,
            style: StyleRefinement::default(),
        }
    }

    pub fn particle_count(self, count: usize, cx: &mut App) -> Self {
        self.state.update(cx, |s, _| s.particle_count = count);
        self
    }

    pub fn colors(self, colors: Vec<Hsla>, cx: &mut App) -> Self {
        self.state.update(cx, |s, _| s.set_colors(colors));
        self
    }

    pub fn gravity(self, gravity: f32, cx: &mut App) -> Self {
        self.state.update(cx, |s, _| s.gravity = gravity);
        self
    }
}

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

impl RenderOnce for Confetti {
    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
        let user_style = self.style;
        let state = self.state.read(cx);
        let paint_data = ConfettiPaintData {
            particles: state.particles().to_vec(),
        };

        div()
            .id(self.id)
            .relative()
            .size_full()
            .child(
                canvas_with_prepaint(
                    move |_bounds, _window, _cx| paint_data,
                    move |bounds, data, window, _cx| {
                        paint_confetti(bounds, &data, window);
                    },
                )
                .absolute()
                .inset_0()
                .size_full(),
            )
            .map(|this| {
                let mut el = this;
                el.style().refine(&user_style);
                el
            })
    }
}

fn paint_confetti(bounds: Bounds<Pixels>, data: &ConfettiPaintData, window: &mut Window) {
    if bounds.size.width <= px(0.0) || bounds.size.height <= px(0.0) {
        return;
    }

    let bw = bounds.size.width / px(1.0);
    let bh = bounds.size.height / px(1.0);

    for particle in &data.particles {
        let x = bounds.left() + px(particle.position.x * bw);
        let y = bounds.top() + px(particle.position.y * bh);
        let half = particle.size * 0.5;

        if x + px(half) < bounds.left()
            || x - px(half) > bounds.right()
            || y + px(half) < bounds.top()
            || y - px(half) > bounds.bottom()
        {
            continue;
        }

        let fade = 1.0 - (particle.age / particle.lifetime).clamp(0.0, 1.0);
        let alpha = particle.color.a * fade;

        let wobble = (particle.age * particle.rotation_speed).sin().abs();
        let w = particle.size * (0.5 + wobble * 0.5);
        let h = particle.size;

        window.paint_quad(PaintQuad {
            bounds: Bounds {
                origin: point(x - px(w * 0.5), y - px(h * 0.5)),
                size: kael::size(px(w), px(h)),
            },
            corner_radii: Corners::all(px(1.0)),
            background: hsla(particle.color.h, particle.color.s, particle.color.l, alpha).into(),
            border_widths: Edges::default(),
            border_color: transparent_black(),
            border_style: BorderStyle::default(),
            continuous_corners: false,
            transform: Default::default(),
            blend_mode: Default::default(),
        });
    }
}

fn pseudo_random_f32(seed: u32) -> f32 {
    let mut x = seed.wrapping_add(0x9E3779B9);
    x ^= x >> 16;
    x = x.wrapping_mul(0x45D9F3B);
    x ^= x >> 16;
    (x & 0xFFFF) as f32 / 65535.0
}