Skip to main content

crabshell_ui/
animation.rs

1//! Animations: values that move toward a target over a [`Transition`],
2//! and the clock the platform advances only while one is running, so a
3//! shell where nothing moves wakes for nothing.
4
5use std::cell::{Cell, RefCell};
6use std::panic::{self, AssertUnwindSafe};
7use std::rc::Rc;
8use std::time::{Duration, Instant};
9
10use crabshell_reactive::{ReadSignal, Signal, untrack};
11
12use crate::cx::Cx;
13use crate::geometry::Point;
14use crate::input::panic_message;
15use crate::style::Color;
16
17/// How an animation moves from its start to its end: what fraction of
18/// the way it has gone at each fraction of its time.
19#[derive(Clone, Copy, Debug, Default, PartialEq)]
20pub enum Easing {
21  /// The same speed all the way.
22  Linear,
23  /// Starts slow.
24  InQuad,
25  /// Ends slow.
26  OutQuad,
27  /// Starts and ends slow.
28  InOutQuad,
29  /// Starts slower.
30  InCubic,
31  /// Ends slower. The default: it settles gently, as most interface
32  /// motion should.
33  #[default]
34  OutCubic,
35  /// Starts and ends slower.
36  InOutCubic,
37  /// Starts slow, along a quarter sine wave.
38  InSine,
39  /// Ends slow, along a quarter sine wave.
40  OutSine,
41  /// Starts and ends slow, along half a sine wave.
42  InOutSine,
43  /// Starts slower than a cubic.
44  InQuart,
45  /// Ends slower than a cubic.
46  OutQuart,
47  /// Starts and ends slower than a cubic.
48  InOutQuart,
49  /// Starts slower still.
50  InQuint,
51  /// Ends slower still.
52  OutQuint,
53  /// Starts and ends slower still.
54  InOutQuint,
55  /// Barely moves, then rushes in: each tenth of the time doubles it.
56  InExpo,
57  /// Rushes out, then barely moves.
58  OutExpo,
59  /// Barely moves at both ends.
60  InOutExpo,
61  /// Starts slow along a quarter circle.
62  InCirc,
63  /// Ends slow along a quarter circle.
64  OutCirc,
65  /// Starts and ends slow along quarter circles.
66  InOutCirc,
67  /// Pulls back about 10% behind the start, then goes.
68  InBack,
69  /// Runs about 10% past the end, then settles back.
70  OutBack,
71  /// Pulls back at the start and runs past the end.
72  InOutBack,
73  /// A cubic Bézier from (0, 0) to (1, 1) through the control points
74  /// `(x1, y1)` and `(x2, y2)`, as CSS's `cubic-bezier`. The x values are
75  /// kept within 0 to 1.
76  Bezier(f32, f32, f32, f32),
77  /// Cubic Bézier segments joined end to end, as Qt's
78  /// `Easing.BezierSpline`. Each is `[c1x, c1y, c2x, c2y, end_x, end_y]`:
79  /// two control points and an end, starting from the previous end, the
80  /// first from (0, 0). The last must end at (1, 1), and the ends' x
81  /// values must never go back. A spline that breaks either rule, is
82  /// empty, or holds a number that isn't finite moves linearly instead.
83  /// A control point's x is kept within its segment.
84  ///
85  /// Material 3's emphasized curve:
86  ///
87  /// ```
88  /// use crabshell_ui::{Easing, Transition};
89  ///
90  /// const EMPHASIZED: Easing = Easing::Spline(&[
91  ///     [0.05, 0.0, 0.133333, 0.06, 0.166666, 0.4],
92  ///     [0.208333, 0.82, 0.25, 1.0, 1.0, 1.0],
93  /// ]);
94  /// const ENTER: Transition = Transition::new(500).easing(EMPHASIZED);
95  /// ```
96  Spline(&'static [[f32; 6]]),
97}
98
99impl Easing {
100  /// How far along the animation is, from 0 to 1 (Back, a Bézier or a
101  /// spline may overshoot either end), when `t` of its time has passed.
102  /// `t` is kept within 0 to 1.
103  pub fn value_at(self, t: f32) -> f32 {
104    use std::f32::consts::PI;
105    // Back's overshoot, the usual constant: about 10%.
106    const C1: f32 = 1.70158;
107    const C2: f32 = C1 * 1.525;
108    let t = if t.is_nan() { 1.0 } else { t.clamp(0.0, 1.0) };
109    match self {
110      Easing::Linear => t,
111      Easing::InQuad => t * t,
112      Easing::OutQuad => 1.0 - (1.0 - t) * (1.0 - t),
113      Easing::InOutQuad if t < 0.5 => 2.0 * t * t,
114      Easing::InOutQuad => 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0,
115      Easing::InCubic => t * t * t,
116      Easing::OutCubic => 1.0 - (1.0 - t).powi(3),
117      Easing::InOutCubic if t < 0.5 => 4.0 * t * t * t,
118      Easing::InOutCubic => 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0,
119      Easing::InSine => 1.0 - (t * PI / 2.0).cos(),
120      Easing::OutSine => (t * PI / 2.0).sin(),
121      Easing::InOutSine => (1.0 - (PI * t).cos()) / 2.0,
122      Easing::InQuart => t.powi(4),
123      Easing::OutQuart => 1.0 - (1.0 - t).powi(4),
124      Easing::InOutQuart if t < 0.5 => 8.0 * t.powi(4),
125      Easing::InOutQuart => 1.0 - (-2.0 * t + 2.0).powi(4) / 2.0,
126      Easing::InQuint => t.powi(5),
127      Easing::OutQuint => 1.0 - (1.0 - t).powi(5),
128      Easing::InOutQuint if t < 0.5 => 16.0 * t.powi(5),
129      Easing::InOutQuint => 1.0 - (-2.0 * t + 2.0).powi(5) / 2.0,
130      // The formulas miss their ends by 2^-10, so the ends are exact.
131      Easing::InExpo | Easing::OutExpo | Easing::InOutExpo if t == 0.0 || t == 1.0 => t,
132      Easing::InExpo => 2_f32.powf(10.0 * t - 10.0),
133      Easing::OutExpo => 1.0 - 2_f32.powf(-10.0 * t),
134      Easing::InOutExpo if t < 0.5 => 2_f32.powf(20.0 * t - 10.0) / 2.0,
135      Easing::InOutExpo => (2.0 - 2_f32.powf(-20.0 * t + 10.0)) / 2.0,
136      Easing::InCirc => 1.0 - (1.0 - t * t).sqrt(),
137      Easing::OutCirc => (1.0 - (t - 1.0).powi(2)).sqrt(),
138      Easing::InOutCirc if t < 0.5 => (1.0 - (1.0 - (2.0 * t).powi(2)).sqrt()) / 2.0,
139      Easing::InOutCirc => ((1.0 - (-2.0 * t + 2.0).powi(2)).sqrt() + 1.0) / 2.0,
140      Easing::InBack => (C1 + 1.0) * t.powi(3) - C1 * t * t,
141      Easing::OutBack => 1.0 + (C1 + 1.0) * (t - 1.0).powi(3) + C1 * (t - 1.0).powi(2),
142      Easing::InOutBack if t < 0.5 => (2.0 * t).powi(2) * ((C2 + 1.0) * 2.0 * t - C2) / 2.0,
143      Easing::InOutBack => {
144        ((2.0 * t - 2.0).powi(2) * ((C2 + 1.0) * (2.0 * t - 2.0) + C2) + 2.0) / 2.0
145      }
146      Easing::Bezier(x1, y1, x2, y2) => bezier(t, (0.0, 0.0), [x1, y1, x2, y2, 1.0, 1.0]),
147      Easing::Spline(segments) => spline(t, segments),
148    }
149  }
150}
151
152/// A spline's y where its x is `x`, or `x` itself for a spline that
153/// breaks the rules [`Easing::Spline`] gives.
154fn spline(x: f32, segments: &[[f32; 6]]) -> f32 {
155  let mut end = 0.0;
156  let valid = segments.iter().all(|segment| {
157    let forward = segment[4] >= end;
158    end = segment[4];
159    forward && segment.iter().all(|n| n.is_finite())
160  }) && segments
161    .last()
162    .is_some_and(|last| last[4] == 1.0 && last[5] == 1.0);
163  if !valid {
164    return x;
165  }
166  let mut start = (0.0, 0.0);
167  for segment in segments {
168    if x <= segment[4] {
169      return bezier(x, start, *segment);
170    }
171    start = (segment[4], segment[5]);
172  }
173  1.0
174}
175
176/// The y of a cubic Bézier from `start` through the control points and
177/// to the end in `segment` (laid out as in [`Easing::Spline`]) at the
178/// point whose x is `x`, found by bisection: x grows with the curve's
179/// parameter once both control x values are kept between the ends.
180fn bezier(x: f32, (x0, y0): (f32, f32), segment: [f32; 6]) -> f32 {
181  let [x1, y1, x2, y2, x3, y3] = segment;
182  let (x1, x2) = (x1.clamp(x0, x3), x2.clamp(x0, x3));
183  let at = |p0: f32, p1: f32, p2: f32, p3: f32, s: f32| {
184    let r = 1.0 - s;
185    r * r * r * p0 + 3.0 * r * r * s * p1 + 3.0 * r * s * s * p2 + s * s * s * p3
186  };
187  let (mut low, mut high) = (0.0_f32, 1.0_f32);
188  for _ in 0..32 {
189    let middle = (low + high) / 2.0;
190    if at(x0, x1, x2, x3, middle) < x {
191      low = middle;
192    } else {
193      high = middle;
194    }
195  }
196  at(y0, y1, y2, y3, (low + high) / 2.0)
197}
198
199/// How long an animation takes, how it moves, and how long it waits
200/// first. Copy it into styles and constants like a color.
201#[derive(Clone, Copy, Debug, PartialEq)]
202pub struct Transition {
203  /// The time it takes, in milliseconds. 0 jumps straight to the end.
204  pub ms: u32,
205  /// How it moves.
206  pub easing: Easing,
207  /// How long it waits before it moves, in milliseconds.
208  pub delay: u32,
209  /// Whether a [`Cx::cycle`] comes back each round instead of starting
210  /// over. Nothing else reads it.
211  pub alternate: bool,
212}
213
214impl Transition {
215  /// A transition of `ms` milliseconds, easing out, with no delay.
216  pub const fn new(ms: u32) -> Transition {
217    Transition {
218      ms,
219      easing: Easing::OutCubic,
220      delay: 0,
221      alternate: false,
222    }
223  }
224
225  /// The same transition moving as `easing` says.
226  pub const fn easing(self, easing: Easing) -> Transition {
227    Transition { easing, ..self }
228  }
229
230  /// The same transition waiting `ms` milliseconds before it moves.
231  pub const fn delay(self, ms: u32) -> Transition {
232    Transition { delay: ms, ..self }
233  }
234
235  /// The same transition coming back after each round of a
236  /// [`Cx::cycle`], so it goes 0 to 1 to 0, each way taking `ms`.
237  pub const fn alternate(self) -> Transition {
238    Transition {
239      alternate: true,
240      ..self
241    }
242  }
243
244  /// Whether it takes no time at all, so the new value shows at once.
245  pub(crate) fn is_instant(self) -> bool {
246    self.ms == 0 && self.delay == 0
247  }
248
249  /// Whether an animation that started at `start` is still waiting out
250  /// its delay at `now`, so nothing it moves has changed yet.
251  pub(crate) fn waiting(self, start: Instant, now: Instant) -> bool {
252    now.saturating_duration_since(start) < Duration::from_millis(self.delay.into())
253  }
254
255  /// How far along an animation that started at `start` is at `now`,
256  /// eased, and whether it has ended. It stays at 0 through the delay.
257  pub(crate) fn progress(self, start: Instant, now: Instant) -> (f32, bool) {
258    let elapsed = now.saturating_duration_since(start).as_secs_f32() * 1000.0 - self.delay as f32;
259    if elapsed < 0.0 {
260      return (0.0, false);
261    }
262    if self.ms == 0 {
263      return (1.0, true);
264    }
265    let t = elapsed / self.ms as f32;
266    if t >= 1.0 {
267      (1.0, true)
268    } else {
269      (self.easing.value_at(t), false)
270    }
271  }
272}
273
274/// The spring a [`Cx::spring`] value follows its target on, as Qt's
275/// `SpringAnimation`. Time is in seconds and distance in the value's own
276/// units. The target pulls the value with `stiffness` times the distance,
277/// per second squared, and friction slows it as `damping` says, as a
278/// ratio: 1 comes to rest as fast as it can without passing the target,
279/// less overshoots and bounces, more creeps in. The default, 700 and 0.9,
280/// is Material 3's default spatial spring; its expressive one is 380 and
281/// 0.8.
282#[derive(Clone, Copy, Debug, PartialEq)]
283pub struct Spring {
284  /// How hard the target pulls, per second squared: higher is faster.
285  pub stiffness: f32,
286  /// The damping ratio: 1 never passes the target, less bounces.
287  pub damping: f32,
288}
289
290impl Spring {
291  /// A spring of `stiffness` and `damping` ratio.
292  pub const fn new(stiffness: f32, damping: f32) -> Spring {
293    Spring { stiffness, damping }
294  }
295
296  /// Whether it can move anything: a stiffness above 0, a damping of 0
297  /// or more, both finite.
298  fn is_valid(self) -> bool {
299    self.stiffness.is_finite()
300      && self.stiffness > 0.0
301      && self.damping.is_finite()
302      && self.damping >= 0.0
303  }
304
305  /// Moves the value `x`, with speed `v`, toward `goal` over `seconds`,
306  /// in implicit Euler steps of at most a millisecond, which stay stable
307  /// whatever the spring and the step.
308  fn advance(self, x: &mut f64, v: &mut f64, goal: f64, seconds: f64) {
309    let k = f64::from(self.stiffness);
310    let c = 2.0 * f64::from(self.damping) * k.sqrt();
311    let steps = (seconds * 1000.0).ceil().max(1.0);
312    let dt = seconds / steps;
313    for _ in 0..steps as u32 {
314      *v = (*v - dt * k * (*x - goal)) / (1.0 + dt * c + dt * dt * k);
315      *x += dt * *v;
316    }
317  }
318}
319
320impl Default for Spring {
321  fn default() -> Spring {
322    Spring::new(700.0, 0.9)
323  }
324}
325
326/// The most time one tick moves a spring: a frame that comes late moves
327/// it this far and no more, so a stall can't cost a long run of steps.
328const SPRING_MAX_STEP: f64 = 0.25;
329
330/// The distance from its target a spring counts as at rest within, and the
331/// speed per second below which it counts as still.
332const SPRING_REST: (f64, f64) = (0.001, 0.01);
333
334/// A value an animation can move between two of: `lerp` gives the value
335/// `t` of the way from `self` to `to`, where `t` may stray a little past 0
336/// or 1 for an easing that overshoots.
337pub trait Animate: Clone + PartialEq + 'static {
338  /// The value `t` of the way from `self` to `to`.
339  fn lerp(&self, to: &Self, t: f32) -> Self;
340}
341
342impl Animate for f32 {
343  fn lerp(&self, to: &f32, t: f32) -> f32 {
344    self + (to - self) * t
345  }
346}
347
348impl Animate for Point {
349  fn lerp(&self, to: &Point, t: f32) -> Point {
350    Point::new(self.x.lerp(&to.x, t), self.y.lerp(&to.y, t))
351  }
352}
353
354impl Animate for Color {
355  fn lerp(&self, to: &Color, t: f32) -> Color {
356    let channel = |a: u8, b: u8| {
357      f32::from(a)
358        .lerp(&f32::from(b), t)
359        .round()
360        .clamp(0.0, 255.0) as u8
361    };
362    Color::rgba(
363      channel(self.r, to.r),
364      channel(self.g, to.g),
365      channel(self.b, to.b),
366      channel(self.a, to.a),
367    )
368  }
369}
370
371/// One running animation: advanced to a time, it says whether it still
372/// runs.
373type Step = Box<dyn FnMut(Instant) -> bool>;
374
375thread_local! {
376  /// Everything is on one thread, so the clock is too. Each animation is
377  /// named for log lines.
378  static RUNNING: RefCell<Vec<(Rc<str>, Step)>> = const { RefCell::new(Vec::new()) };
379  /// When the clock last ticked.
380  static LAST_TICK: Cell<Option<Instant>> = const { Cell::new(None) };
381}
382
383/// Starts `step`, named `label` for log lines, at the next
384/// [`advance_animations`].
385pub(crate) fn start(label: Rc<str>, step: Step) {
386  RUNNING.with(|running| running.borrow_mut().push((label, step)));
387}
388
389/// When an animation starting now should count from: the clock's last
390/// tick while it runs, so one that replaces another moves at the very
391/// next tick, or `None` when it is still, so the first tick is time 0.
392pub(crate) fn start_time() -> Option<Instant> {
393  if animating() {
394    LAST_TICK.with(Cell::get)
395  } else {
396    None
397  }
398}
399
400/// Whether an animation is running, so the platform should advance the
401/// clock soon. `false` means nothing moves and nothing needs waking.
402pub fn animating() -> bool {
403  RUNNING.with(|running| !running.borrow().is_empty())
404}
405
406/// Advances every running animation to `now`, writing their values, and
407/// returns whether any still runs. Each one starts at the first call after
408/// it began, so `now` there is its time 0. One that panics is logged and
409/// dropped.
410pub fn advance_animations(now: Instant) -> bool {
411  LAST_TICK.with(|last| last.set(Some(now)));
412  let steps = RUNNING.with(|running| std::mem::take(&mut *running.borrow_mut()));
413  let mut kept = Vec::with_capacity(steps.len());
414  for (label, mut step) in steps {
415    match panic::catch_unwind(AssertUnwindSafe(|| step(now))) {
416      Ok(true) => kept.push((label, step)),
417      Ok(false) => drop_isolated(&label, step),
418      Err(payload) => {
419        tracing::error!(
420          "animation {label} panicked and stops: {}",
421          panic_message(payload.as_ref())
422        );
423        drop_isolated(&label, step);
424      }
425    }
426  }
427  RUNNING.with(|running| {
428    // Animations started while these ran go after them.
429    let mut running = running.borrow_mut();
430    kept.append(&mut running);
431    *running = kept;
432    if running.is_empty() {
433      // The clock stops, so the next animation counts from its own first
434      // tick, not this one.
435      LAST_TICK.with(|last| last.set(None));
436    }
437    !running.is_empty()
438  })
439}
440
441/// Drops a finished step, and the user's values it holds, isolated.
442fn drop_isolated(label: &str, step: Step) {
443  if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(move || drop(step))) {
444    tracing::error!(
445      "dropping animation {label} panicked: {}",
446      panic_message(payload.as_ref())
447    );
448  }
449}
450
451/// Runs `f`, a config's closure for the animation `label`, and logs a
452/// panic as that animation's, giving `None`.
453fn guarded<R>(label: &str, f: impl FnOnce() -> R) -> Option<R> {
454  match panic::catch_unwind(AssertUnwindSafe(f)) {
455    Ok(value) => Some(value),
456    Err(payload) => {
457      tracing::error!(
458        "animation {label} panicked and stops: {}",
459        panic_message(payload.as_ref())
460      );
461      None
462    }
463  }
464}
465
466/// Sets `signal` to `value` only when that changes it, since a set always
467/// tells its readers.
468fn set_changed<T: PartialEq + 'static>(signal: Signal<T>, value: T) {
469  if untrack(|| signal.try_with(|old| *old != value)) == Some(true) {
470    signal.try_set(value);
471  }
472}
473
474/// A value moving toward its target, from [`Cx::animation`]: a `Copy`
475/// handle to its value and to whether it moves.
476pub struct Animation<T: 'static> {
477  value: ReadSignal<T>,
478  running: ReadSignal<bool>,
479}
480
481impl<T> Clone for Animation<T> {
482  fn clone(&self) -> Self {
483    *self
484  }
485}
486
487impl<T> Copy for Animation<T> {}
488
489impl<T> Animation<T> {
490  /// The value, as [`Cx::animate`] gives it.
491  pub fn value(&self) -> ReadSignal<T> {
492    self.value
493  }
494
495  /// `true` from the moment a new target starts it, through its delay,
496  /// until it settles there. It turns `false` once per animation, so an
497  /// effect on it is the place for what follows.
498  pub fn running(&self) -> ReadSignal<bool> {
499    self.running
500  }
501}
502
503impl Cx {
504  /// A value that follows `target`, moving to each new one over
505  /// `transition` instead of jumping. It starts at `target`'s first value.
506  /// A new target while it moves starts a new animation from where it is.
507  /// The animation goes when this context's owner does. It is
508  /// [`Cx::animation`]'s value.
509  ///
510  /// ```
511  /// use crabshell_ui::{Cx, Transition};
512  ///
513  /// fn slide(cx: &Cx) {
514  ///     let open = cx.signal(false);
515  ///     let offset = cx.animate(move || if open.get() { 0.0 } else { -200.0 }, Transition::new(200));
516  ///     // Read `offset.get()` in a binding, such as a style's width.
517  /// }
518  /// ```
519  #[track_caller]
520  pub fn animate<T: Animate>(
521    &self,
522    target: impl FnMut() -> T + 'static,
523    transition: Transition,
524  ) -> ReadSignal<T> {
525    self.follow("cx.animate", target, transition).value
526  }
527
528  /// A value that follows `target` as [`Cx::animate`]'s does, and whether
529  /// it moves. A panic in `target` is logged with this call's place, and
530  /// the value stays where it was.
531  ///
532  /// ```
533  /// use crabshell_ui::{Cx, Transition};
534  ///
535  /// fn slide_in_then_focus(cx: &Cx) {
536  ///     let open = cx.signal(false);
537  ///     let ready = cx.signal(false);
538  ///     let slide = cx.animation(move || if open.get() { 0.0 } else { -200.0 }, Transition::new(200));
539  ///     cx.effect(move || {
540  ///         if open.get() && !slide.running().get() {
541  ///             ready.set(true);
542  ///         }
543  ///     });
544  /// }
545  /// ```
546  #[track_caller]
547  pub fn animation<T: Animate>(
548    &self,
549    target: impl FnMut() -> T + 'static,
550    transition: Transition,
551  ) -> Animation<T> {
552    self.follow("cx.animation", target, transition)
553  }
554
555  /// [`Cx::animation`], logged as `what` at the caller's place.
556  #[track_caller]
557  fn follow<T: Animate>(
558    &self,
559    what: &str,
560    mut target: impl FnMut() -> T + 'static,
561    transition: Transition,
562  ) -> Animation<T> {
563    let label: Rc<str> = format!("{what} at {}", std::panic::Location::caller()).into();
564    let first = untrack(&mut target);
565    let value = self.signal(first.clone());
566    let running = self.signal(false);
567    let mut last = first;
568    // Bumped by each new target, so the animation it replaces stops.
569    let generation = Rc::new(Cell::new(0_u64));
570    let mut broken = false;
571    self.effect(move || {
572      if broken {
573        return;
574      }
575      let Some(to) = guarded(&label, &mut target) else {
576        broken = true;
577        generation.set(generation.get() + 1);
578        set_changed(running, false);
579        return;
580      };
581      if to == last {
582        return;
583      }
584      last = to.clone();
585      let Some(from) = untrack(|| value.try_get()) else {
586        return;
587      };
588      let current = generation.get() + 1;
589      generation.set(current);
590      // No time to take: the new value shows in this flush.
591      if transition.is_instant() {
592        value.try_set(to);
593        set_changed(running, false);
594        return;
595      }
596      set_changed(running, true);
597      let generation = Rc::clone(&generation);
598      let mut started = start_time();
599      let step_label = Rc::clone(&label);
600      start(
601        Rc::clone(&label),
602        Box::new(move |now| {
603          if generation.get() != current {
604            return false;
605          }
606          let begun = *started.get_or_insert(now);
607          if transition.waiting(begun, now) {
608            // The value is still `from`; setting it would rerun its readers.
609            return true;
610          }
611          let (t, ended) = transition.progress(begun, now);
612          // `lerp` and `clone` may be a config's own; a panic ends the
613          // animation with `running` false, so whatever waits on it runs.
614          let Some(next) = guarded(&step_label, || {
615            if ended { to.clone() } else { from.lerp(&to, t) }
616          }) else {
617            running.try_set(false);
618            return false;
619          };
620          // A value whose owner is gone stops its animation.
621          let kept = value.try_set(next);
622          if ended {
623            running.try_set(false);
624          }
625          kept && !ended
626        }),
627      );
628    });
629    Animation {
630      value: value.read_only(),
631      running: running.read_only(),
632    }
633  }
634
635  /// Progress from 0 to 1, eased, repeating every `transition.ms` while
636  /// `running` gives `true`, for spinners and pulses. With
637  /// [`Transition::alternate`] it comes back, 0 to 1 to 0, each way taking
638  /// `ms`. It waits out the delay once, at the start. When `running` turns
639  /// `false`, the round it is in runs to its end, then it rests at 0 and
640  /// the clock stops; `true` again before then keeps it going, and after
641  /// it begins at 0. A 0 ms cycle never moves. A panic in `running` is
642  /// logged with this call's place and ends the cycle after its round.
643  ///
644  /// ```
645  /// use crabshell_ui::{Cx, Easing, Transition};
646  ///
647  /// const SPIN: Transition = Transition::new(1000).easing(Easing::Linear);
648  /// const PULSE: Transition = Transition::new(600).easing(Easing::InOutSine).alternate();
649  ///
650  /// fn busy(cx: &Cx) {
651  ///     let loading = cx.signal(true);
652  ///     let turn = cx.cycle(move || loading.get(), SPIN); // turns: turn.get() * 360.0
653  ///     let glow = cx.cycle(move || loading.get(), PULSE); // an opacity: 0.5 + glow.get() / 2.0
654  /// }
655  /// ```
656  #[track_caller]
657  pub fn cycle(
658    &self,
659    mut running: impl FnMut() -> bool + 'static,
660    transition: Transition,
661  ) -> ReadSignal<f32> {
662    let label: Rc<str> = format!("cx.cycle at {}", std::panic::Location::caller()).into();
663    let value = self.signal(0.0_f32);
664    // What `running` last gave, and whether a round is on the clock.
665    let wanted = Rc::new(Cell::new(false));
666    let ticking = Rc::new(Cell::new(false));
667    let mut broken = false;
668    self.effect(move || {
669      let on = !broken
670        && guarded(&label, &mut running).unwrap_or_else(|| {
671          broken = true;
672          false
673        });
674      wanted.set(on);
675      if !on || ticking.get() || transition.ms == 0 {
676        return;
677      }
678      ticking.set(true);
679      let (wanted, ticking) = (Rc::clone(&wanted), Rc::clone(&ticking));
680      let leg = f64::from(transition.ms);
681      let period = if transition.alternate { 2.0 * leg } else { leg };
682      let mut started = start_time();
683      // Round 0 is the delay, then 1, 2 and on.
684      let mut round = 0_u64;
685      start(
686        Rc::clone(&label),
687        Box::new(move |now| {
688          let begun = *started.get_or_insert(now);
689          let elapsed = now.saturating_duration_since(begun).as_secs_f64() * 1000.0
690            - f64::from(transition.delay);
691          let now_round = if elapsed < 0.0 {
692            0
693          } else {
694            (elapsed / period) as u64 + 1
695          };
696          let ended = !wanted.get() && (now_round == 0 || now_round > round);
697          round = now_round;
698          if ended {
699            ticking.set(false);
700            value.try_set(0.0);
701            return false;
702          }
703          let phase = elapsed.max(0.0) % period / leg;
704          let t = if phase <= 1.0 { phase } else { 2.0 - phase };
705          value.try_set(transition.easing.value_at(t as f32))
706        }),
707      );
708    });
709    value.read_only()
710  }
711
712  /// The time since `running` turned `true`, updated at each tick of the
713  /// animation clock while it stays true, as Qt's `FrameAnimation` gives.
714  /// Nothing ticks while it is false, and the time stays where it was
715  /// until the next run starts again from 0. A panic in `running` is
716  /// logged with this call's place and stops it.
717  ///
718  /// ```
719  /// use crabshell_ui::Cx;
720  ///
721  /// fn wave(cx: &Cx) {
722  ///     let shown = cx.signal(true);
723  ///     let time = cx.frames(move || shown.get());
724  ///     // A bar's height: 10.0 + 5.0 * (time.get().as_secs_f32() * 6.0).sin()
725  /// }
726  /// ```
727  #[track_caller]
728  pub fn frames(&self, mut running: impl FnMut() -> bool + 'static) -> ReadSignal<Duration> {
729    let label: Rc<str> = format!("cx.frames at {}", std::panic::Location::caller()).into();
730    let value = self.signal(Duration::ZERO);
731    let wanted = Rc::new(Cell::new(false));
732    let ticking = Rc::new(Cell::new(false));
733    let mut broken = false;
734    self.effect(move || {
735      let on = !broken
736        && guarded(&label, &mut running).unwrap_or_else(|| {
737          broken = true;
738          false
739        });
740      wanted.set(on);
741      if !on || ticking.get() {
742        return;
743      }
744      ticking.set(true);
745      let (wanted, ticking) = (Rc::clone(&wanted), Rc::clone(&ticking));
746      let mut started = start_time();
747      start(
748        Rc::clone(&label),
749        Box::new(move |now| {
750          if !wanted.get() {
751            ticking.set(false);
752            return false;
753          }
754          let begun = *started.get_or_insert(now);
755          value.try_set(now.saturating_duration_since(begun))
756        }),
757      );
758    });
759    value.read_only()
760  }
761
762  /// A value that follows `target` on `spring`, as Qt's
763  /// `SpringAnimation`: a new target while it moves keeps its speed, so a
764  /// value chasing a moving target never stops dead. It starts at
765  /// `target`'s first value and comes to rest exactly on the target, when
766  /// the clock stops. A target that isn't finite, or a spring that isn't
767  /// valid (see [`Spring`]), jumps there at once. A panic in `target` is
768  /// logged with this call's place, and the value stops where it is.
769  ///
770  /// ```
771  /// use crabshell_ui::{Cx, Spring};
772  ///
773  /// fn indicator(cx: &Cx) {
774  ///     let hovered = cx.signal(0_usize);
775  ///     let x = cx.spring(move || hovered.get() as f32 * 40.0, Spring::new(380.0, 0.8));
776  ///     // Read `x.get()` in a binding, such as a style's padding.
777  /// }
778  /// ```
779  #[track_caller]
780  pub fn spring(
781    &self,
782    mut target: impl FnMut() -> f32 + 'static,
783    spring: Spring,
784  ) -> ReadSignal<f32> {
785    let label: Rc<str> = format!("cx.spring at {}", std::panic::Location::caller()).into();
786    let first = untrack(&mut target);
787    let value = self.signal(first);
788    let mut last = first;
789    // Where the step on the clock heads, and whether there is one. A goal
790    // that isn't finite stops it.
791    let goal = Rc::new(Cell::new(first));
792    let ticking = Rc::new(Cell::new(false));
793    let mut broken = false;
794    self.effect(move || {
795      if broken {
796        return;
797      }
798      let Some(to) = guarded(&label, &mut target) else {
799        broken = true;
800        goal.set(f32::NAN);
801        return;
802      };
803      if to == last {
804        return;
805      }
806      last = to;
807      goal.set(to);
808      if !to.is_finite() || !spring.is_valid() {
809        value.try_set(to);
810        return;
811      }
812      if ticking.get() {
813        return;
814      }
815      let Some(from) = untrack(|| value.try_get()) else {
816        return;
817      };
818      ticking.set(true);
819      let (goal, ticking) = (Rc::clone(&goal), Rc::clone(&ticking));
820      let (mut x, mut v) = (f64::from(from), 0.0);
821      let mut previous = start_time();
822      start(
823        Rc::clone(&label),
824        Box::new(move |now| {
825          let to = f64::from(goal.get());
826          if !to.is_finite() {
827            ticking.set(false);
828            return false;
829          }
830          let seconds = previous.map_or(0.0, |previous| {
831            now
832              .saturating_duration_since(previous)
833              .as_secs_f64()
834              .min(SPRING_MAX_STEP)
835          });
836          previous = Some(now);
837          spring.advance(&mut x, &mut v, to, seconds);
838          let (near, slow) = SPRING_REST;
839          // A value that started out not finite lands at once.
840          let rest = !(x.is_finite() && v.is_finite()) || ((x - to).abs() < near && v.abs() < slow);
841          if rest {
842            x = to;
843          }
844          let kept = value.try_set(x as f32);
845          if rest || !kept {
846            ticking.set(false);
847          }
848          kept && !rest
849        }),
850      );
851    });
852    value.read_only()
853  }
854}