guise-ui 1.4.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
//! Choreography across a list: the same motion, offset per element.
//!
//! One element is one clip, so staggering is not a timeline feature here —
//! it is a function from an index to a delay, which you fold into each
//! element's own [`Motion`](super::Motion). That keeps the N elements
//! independent (a list can grow or reorder mid-flight without restarting
//! anything) and makes the whole thing a pure calculation you can unit-test.
//!
//! ```ignore
//! let rise = Stagger::new(40.0).from(StaggerFrom::Center);
//! for (i, row) in rows.iter().enumerate() {
//!     Animated::new(("row", i))
//!         .motion(Motion::enter(TransitionKind::SlideUp).delay(rise.at(i, rows.len())))
//!         .child(row)
//! }
//! ```

use super::Easing;

/// Which element goes first.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StaggerFrom {
    #[default]
    First,
    Last,
    Center,
    /// A specific index leads and the rest ripple out from it.
    Index(usize),
}

/// Restrict a grid stagger to one axis.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StaggerAxis {
    X,
    Y,
}

/// Index-to-delay (or index-to-value) mapping for a list or a grid.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Stagger {
    /// Milliseconds between neighbours.
    pub step: f32,
    /// Milliseconds added to everyone.
    pub start: f32,
    pub from: StaggerFrom,
    /// Columns and rows, when the elements are laid out as a grid.
    pub grid: Option<(usize, usize)>,
    /// With a grid, measure distance along one axis only.
    pub axis: Option<StaggerAxis>,
    /// Reshape the spacing — `Easing::In(Curve::Quad)` bunches the early
    /// elements together and spreads the late ones out.
    pub ease: Option<Easing>,
    pub reversed: bool,
}

impl Stagger {
    /// `step` milliseconds between neighbours.
    pub fn new(step: f32) -> Self {
        Stagger {
            step,
            start: 0.0,
            from: StaggerFrom::First,
            grid: None,
            axis: None,
            ease: None,
            reversed: false,
        }
    }

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

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

    /// Treat the indices as a `columns × rows` grid in row-major order.
    pub fn grid(mut self, columns: usize, rows: usize) -> Self {
        self.grid = Some((columns.max(1), rows.max(1)));
        self
    }

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

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

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

    /// The delay for element `index` of `total`, in milliseconds.
    pub fn at(&self, index: usize, total: usize) -> f32 {
        self.start + self.weight(index, total) * self.furthest(total) * self.step
    }

    /// Spread a value instead of a delay: element 0 gets `from`, the
    /// furthest gets `to`, everyone else lands in between. anime.js's
    /// `stagger([a, b])`.
    pub fn value(&self, index: usize, total: usize, from: f32, to: f32) -> f32 {
        from + (to - from) * self.weight(index, total)
    }

    /// How long until the last element has started.
    pub fn span(&self, total: usize) -> f32 {
        self.start + self.furthest(total) * self.step
    }

    /// 0..=1: how far this index is from the leading one.
    fn weight(&self, index: usize, total: usize) -> f32 {
        let furthest = self.furthest(total);
        if furthest <= 0.0 {
            return 0.0;
        }
        let mut t = (self.distance(index, total) / furthest).clamp(0.0, 1.0);
        if self.reversed {
            t = 1.0 - t;
        }
        match self.ease {
            Some(easing) => easing.apply(t),
            None => t,
        }
    }

    /// Distance from the leading element, in element-widths.
    fn distance(&self, index: usize, total: usize) -> f32 {
        match self.grid {
            Some((columns, _)) => {
                let (x, y) = ((index % columns) as f32, (index / columns) as f32);
                let (ox, oy) = self.grid_origin(total);
                match self.axis {
                    Some(StaggerAxis::X) => (x - ox).abs(),
                    Some(StaggerAxis::Y) => (y - oy).abs(),
                    None => ((x - ox).powi(2) + (y - oy).powi(2)).sqrt(),
                }
            }
            None => {
                let last = total.saturating_sub(1) as f32;
                let origin = match self.from {
                    StaggerFrom::First => 0.0,
                    StaggerFrom::Last => last,
                    StaggerFrom::Center => last / 2.0,
                    StaggerFrom::Index(i) => i as f32,
                };
                (index as f32 - origin).abs()
            }
        }
    }

    fn grid_origin(&self, total: usize) -> (f32, f32) {
        let (columns, rows) = self.grid.unwrap_or((1, 1));
        let rows = rows.max(total.div_ceil(columns));
        let (last_x, last_y) = ((columns - 1) as f32, (rows.saturating_sub(1)) as f32);
        match self.from {
            StaggerFrom::First => (0.0, 0.0),
            StaggerFrom::Last => (last_x, last_y),
            StaggerFrom::Center => (last_x / 2.0, last_y / 2.0),
            StaggerFrom::Index(i) => ((i % columns) as f32, (i / columns) as f32),
        }
    }

    /// The largest distance any index reaches — what normalizes the weight
    /// so `ease` and `value` have a fixed range to work in.
    fn furthest(&self, total: usize) -> f32 {
        (0..total)
            .map(|i| self.distance(i, total))
            .fold(0.0_f32, f32::max)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::anim::ease::Curve;

    #[test]
    fn a_plain_stagger_steps_one_by_one() {
        let stagger = Stagger::new(50.0);
        assert_eq!(stagger.at(0, 4), 0.0);
        assert_eq!(stagger.at(1, 4), 50.0);
        assert_eq!(stagger.at(3, 4), 150.0);
        assert_eq!(stagger.span(4), 150.0);
    }

    #[test]
    fn start_shifts_everyone() {
        let stagger = Stagger::new(50.0).start(100.0);
        assert_eq!(stagger.at(0, 4), 100.0);
        assert_eq!(stagger.at(2, 4), 200.0);
    }

    #[test]
    fn from_last_reverses_the_order() {
        let stagger = Stagger::new(50.0).from(StaggerFrom::Last);
        assert_eq!(stagger.at(3, 4), 0.0);
        assert_eq!(stagger.at(0, 4), 150.0);
    }

    #[test]
    fn from_center_ripples_outward() {
        let stagger = Stagger::new(50.0).from(StaggerFrom::Center);
        // Five elements: the middle leads, the ends arrive together.
        assert_eq!(stagger.at(2, 5), 0.0);
        assert_eq!(stagger.at(0, 5), stagger.at(4, 5));
        assert!(stagger.at(1, 5) < stagger.at(0, 5));
    }

    #[test]
    fn a_named_index_leads() {
        let stagger = Stagger::new(10.0).from(StaggerFrom::Index(2));
        assert_eq!(stagger.at(2, 5), 0.0);
        assert_eq!(stagger.at(4, 5), 20.0);
    }

    #[test]
    fn a_grid_measures_in_two_dimensions() {
        let stagger = Stagger::new(100.0).grid(3, 2);
        // Row-major 3x2. Corner-to-corner is sqrt(2^2 + 1^2).
        assert_eq!(stagger.at(0, 6), 0.0);
        let far = stagger.at(5, 6);
        assert!((far - 100.0 * 5.0_f32.sqrt()).abs() < 1e-3, "{far}");
        // Same column, next row: distance 1.
        assert!((stagger.at(3, 6) - 100.0).abs() < 1e-3);
    }

    #[test]
    fn an_axis_flattens_the_grid_to_one_direction() {
        let stagger = Stagger::new(100.0).grid(3, 2).axis(StaggerAxis::Y);
        assert_eq!(stagger.at(0, 6), stagger.at(2, 6), "same row, same delay");
        assert!(stagger.at(3, 6) > stagger.at(0, 6));
    }

    #[test]
    fn reversed_flips_the_weights() {
        let plain = Stagger::new(50.0);
        let flipped = Stagger::new(50.0).reversed(true);
        assert_eq!(flipped.at(0, 4), plain.at(3, 4));
        assert_eq!(flipped.at(3, 4), plain.at(0, 4));
    }

    #[test]
    fn easing_reshapes_the_spacing_without_moving_the_ends() {
        let eased = Stagger::new(50.0).ease(Easing::In(Curve::Quad));
        assert_eq!(eased.at(0, 5), 0.0);
        assert_eq!(eased.at(4, 5), 200.0);
        // Quadratic in: the early elements bunch up.
        assert!(eased.at(1, 5) < 50.0);
    }

    #[test]
    fn values_spread_across_a_range() {
        let stagger = Stagger::new(0.0);
        assert_eq!(stagger.value(0, 5, -100.0, 100.0), -100.0);
        assert_eq!(stagger.value(4, 5, -100.0, 100.0), 100.0);
        assert_eq!(stagger.value(2, 5, -100.0, 100.0), 0.0);
    }

    #[test]
    fn a_single_element_never_waits() {
        let stagger = Stagger::new(50.0).from(StaggerFrom::Center);
        assert_eq!(stagger.at(0, 1), 0.0);
        assert_eq!(stagger.span(1), 0.0);
        assert_eq!(stagger.at(0, 0), 0.0);
    }
}