guise-ui 1.5.0

A component library for gpui, Zed's GPU-accelerated UI framework: a themed palette, sizing tokens, 130+ composable components, a reactive state layer, and an in-app Safari-style inspector.
Documentation
//! `Skeleton` — an animated loading placeholder.

use std::sync::OnceLock;
use std::time::{Duration, Instant};

use gpui::prelude::*;
use gpui::{
    canvas, pulsating_between, px, quad, transparent_black, App, BorderStyle, Bounds, IntoElement,
    Pixels, Window,
};

use crate::devtools::Probed;
use crate::frameclock::{request_frame, FrameKind};
use crate::theme::{theme, ColorName, Size};

const FRAME_INTERVAL: Duration = Duration::from_millis(60);
const CYCLE_SECONDS: f32 = 1.1;

fn animation_start() -> Instant {
    static START: OnceLock<Instant> = OnceLock::new();
    *START.get_or_init(Instant::now)
}

fn request_next_frame(window: &mut Window, cx: &mut App) {
    request_frame(FrameKind::Continuous, FRAME_INTERVAL, window, cx);
}

/// A pulsing placeholder block.
#[derive(IntoElement)]
pub struct Skeleton {
    width: Option<f32>,
    height: f32,
    radius: Size,
    circle: bool,
}

impl Skeleton {
    pub fn new() -> Self {
        Skeleton {
            width: None,
            height: 16.0,
            radius: Size::Sm,
            circle: false,
        }
    }

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

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

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

    /// Render a circle of `size` (overrides width/height/radius).
    pub fn circle(mut self, size: f32) -> Self {
        self.circle = true;
        self.width = Some(size);
        self.height = size;
        self
    }
}

impl Default for Skeleton {
    fn default() -> Self {
        Skeleton::new()
    }
}

impl RenderOnce for Skeleton {
    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
        let t = theme(cx);
        let color = t
            .color(ColorName::Gray, if t.scheme.is_dark() { 7 } else { 2 })
            .hsla();
        let radius = if self.circle {
            self.height
        } else {
            t.radius(self.radius)
        };

        let mut block = canvas(
            |_, _, _| (),
            move |bounds: Bounds<Pixels>, _, window, cx| {
                if !bounds.intersects(&window.content_mask().bounds) {
                    return;
                }
                let cycle = (animation_start().elapsed().as_secs_f32() / CYCLE_SECONDS) % 1.0;
                let pulse = pulsating_between(0.4, 1.0);
                window.paint_quad(quad(
                    bounds,
                    px(radius),
                    color.opacity(pulse(cycle)),
                    px(0.0),
                    transparent_black(),
                    BorderStyle::default(),
                ));
                request_next_frame(window, cx);
            },
        )
        .h(px(self.height));
        block = match self.width {
            Some(width) => block.w(px(width)),
            None => block.w_full(),
        };
        block.probe("Skeleton")
    }
}