guise-ui 1.5.3

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
//! `Animator` — a clip with a clock you can drive.
//!
//! gpui's own `with_animation` is fire-and-forget: it starts when the element
//! first lays out and there is no handle to pause, reverse, or scrub it. That
//! is the right thing for an entrance and useless for anything a user
//! controls, so `Animator` keeps the playback state in an entity instead.
//!
//! The clock is an *anchor*, not a tick: `time` is where the playhead was at
//! `anchor`, and everything else is derived from `Instant::now()`. Nothing
//! mutates per frame, so a paused animation costs nothing, seeking is one
//! assignment, and sampling is pure enough to unit-test without a window.
//!
//! Frames come from [`Animator::frame`], which also asks the window for the
//! next one while the clip is still running — that is the whole repaint loop.

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

use gpui::{Context, EventEmitter, Task, Window};

use super::{Clip, Frame};

/// The ends of a playback run. There is no per-frame event: your `render`
/// already runs every frame, and reading [`Animator::frame`] there is the
/// same thing without the plumbing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnimatorEvent {
  /// Playback started (or restarted) from a stopped state.
  Begin,
  /// The playhead reached the end — or the start, when running reversed.
  Complete,
}

/// A clip plus a playhead.
pub struct Animator {
  clip: Clip,
  /// Playhead position in ms, correct as of `anchor`.
  time: f32,
  /// `Some` while running: the wall clock that `time` was measured at.
  anchor: Option<Instant>,
  speed: f32,
  reversed: bool,
  /// Bumped on every state change so a stale completion timer gives up.
  epoch: usize,
  completion: Option<Task<()>>,
}

impl EventEmitter<AnimatorEvent> for Animator {}

impl Animator {
  pub fn new(clip: impl Into<Clip>, _cx: &mut Context<Self>) -> Self {
    Animator {
      clip: clip.into(),
      time: 0.0,
      anchor: None,
      speed: 1.0,
      reversed: false,
      epoch: 0,
      completion: None,
    }
  }

  /// Start playing as soon as it is created.
  pub fn autoplay(mut self, cx: &mut Context<Self>) -> Self {
    self.play(cx);
    self
  }

  /// Run the clip backwards from the start.
  pub fn reversed(mut self, reversed: bool) -> Self {
    self.reversed = reversed;
    self.time = if reversed { self.total_ms() } else { 0.0 };
    self
  }

  pub fn clip(&self) -> &Clip {
    &self.clip
  }

  fn total_ms(&self) -> f32 {
    self.clip.total_ms()
  }

  /// Where the playhead is right now, in milliseconds.
  pub fn time(&self) -> f32 {
    self.time_at(Instant::now())
  }

  /// Where the playhead would be at `now`. The pure form — tests drive
  /// this instead of sleeping.
  pub fn time_at(&self, now: Instant) -> f32 {
    let Some(anchor) = self.anchor else {
      return self.time;
    };
    let elapsed = now.saturating_duration_since(anchor).as_secs_f32() * 1000.0 * self.speed;
    let raw = if self.reversed {
      self.time - elapsed
    } else {
      self.time + elapsed
    };
    raw.clamp(0.0, self.total_ms())
  }

  /// 0..=1 through the clip. Endless clips report their position within
  /// the current pass.
  pub fn progress(&self) -> f32 {
    self.clip.sample(self.time()).progress
  }

  /// Whether the clock is running. A clip that has played to its end
  /// reports `false` even before anything cleans up.
  pub fn is_playing(&self) -> bool {
    self.anchor.is_some() && !self.is_settled_at(Instant::now())
  }

  /// Whether the playhead has run out of road in the direction it is going.
  fn is_settled_at(&self, now: Instant) -> bool {
    if self.clip.is_endless() || self.speed <= 0.0 {
      return false;
    }
    let time = self.time_at(now);
    if self.reversed {
      time <= 0.0
    } else {
      time >= self.total_ms()
    }
  }

  /// The values for this instant, and a request for the next frame while
  /// the clip is still moving. Call it from `render`.
  pub fn frame(&self, window: &mut Window) -> Frame {
    let now = Instant::now();
    if self.anchor.is_some() && !self.is_settled_at(now) {
      window.request_animation_frame();
    }
    self.clip.sample(self.time_at(now))
  }

  /// The values at `now`, with no window and no repaint request.
  pub fn frame_at(&self, now: Instant) -> Frame {
    self.clip.sample(self.time_at(now))
  }

  pub fn play(&mut self, cx: &mut Context<Self>) {
    let now = Instant::now();
    if self.anchor.is_some() && !self.is_settled_at(now) {
      return;
    }
    // Replaying something that already finished starts it over, rather
    // than sitting on the end frame doing nothing.
    if self.is_settled_at(now) {
      self.time = if self.reversed { self.total_ms() } else { 0.0 };
    }
    self.anchor = Some(now);
    cx.emit(AnimatorEvent::Begin);
    self.reschedule(cx);
    cx.notify();
  }

  pub fn pause(&mut self, cx: &mut Context<Self>) {
    if self.anchor.is_none() {
      return;
    }
    self.time = self.time_at(Instant::now());
    self.anchor = None;
    self.reschedule(cx);
    cx.notify();
  }

  pub fn toggle(&mut self, cx: &mut Context<Self>) {
    if self.is_playing() {
      self.pause(cx);
    } else {
      self.play(cx);
    }
  }

  /// Back to the beginning, playing.
  pub fn restart(&mut self, cx: &mut Context<Self>) {
    self.time = if self.reversed { self.total_ms() } else { 0.0 };
    self.anchor = Some(Instant::now());
    cx.emit(AnimatorEvent::Begin);
    self.reschedule(cx);
    cx.notify();
  }

  /// Back to the beginning, stopped.
  pub fn stop(&mut self, cx: &mut Context<Self>) {
    self.time = if self.reversed { self.total_ms() } else { 0.0 };
    self.anchor = None;
    self.reschedule(cx);
    cx.notify();
  }

  /// Move the playhead. Keeps playing if it was playing.
  pub fn seek(&mut self, ms: f32, cx: &mut Context<Self>) {
    self.time = ms.clamp(0.0, self.total_ms());
    if self.anchor.is_some() {
      self.anchor = Some(Instant::now());
    }
    self.reschedule(cx);
    cx.notify();
  }

  /// Move the playhead by fraction of the clip, 0..=1. Endless clips have
  /// no fraction to scrub, so this does nothing for them.
  pub fn seek_progress(&mut self, progress: f32, cx: &mut Context<Self>) {
    let total = self.total_ms();
    if total.is_finite() {
      self.seek(total * progress.clamp(0.0, 1.0), cx);
    }
  }

  /// Flip direction, leaving the playhead where it is.
  pub fn reverse(&mut self, cx: &mut Context<Self>) {
    self.time = self.time_at(Instant::now());
    self.reversed = !self.reversed;
    if self.anchor.is_some() {
      self.anchor = Some(Instant::now());
    }
    self.reschedule(cx);
    cx.notify();
  }

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

  pub fn set_speed(&mut self, speed: f32, cx: &mut Context<Self>) {
    self.time = self.time_at(Instant::now());
    self.speed = speed.max(0.0);
    if self.anchor.is_some() {
      self.anchor = Some(Instant::now());
    }
    self.reschedule(cx);
    cx.notify();
  }

  pub fn speed(&self) -> f32 {
    self.speed
  }

  /// Arm (or disarm) the one timer that fires `Complete`.
  ///
  /// One timer for the whole run, not a tick: the frame values come from
  /// the clock, so the only thing that needs waking up is the event. An
  /// endless clip never arms one.
  fn reschedule(&mut self, cx: &mut Context<Self>) {
    self.epoch += 1;
    self.completion = None;
    if self.anchor.is_none() || self.speed <= 0.0 || self.clip.is_endless() {
      return;
    }
    let remaining = if self.reversed {
      self.time
    } else {
      self.total_ms() - self.time
    };
    if remaining <= 0.0 {
      return;
    }
    let epoch = self.epoch;
    let wait = Duration::from_secs_f32((remaining / self.speed / 1000.0).max(0.0));
    self.completion = Some(cx.spawn(async move |this, cx| {
      cx.background_executor().timer(wait).await;
      this
        .update(cx, |animator, cx| {
          if animator.epoch != epoch {
            return;
          }
          animator.time = if animator.reversed {
            0.0
          } else {
            animator.total_ms()
          };
          animator.anchor = None;
          animator.completion = None;
          cx.emit(AnimatorEvent::Complete);
          cx.notify();
        })
        .ok();
    }));
  }
}