Skip to main content

agg_gui/
animation.rs

1//! Thread-local repaint-request signals.
2//!
3//! Two independent channels feed the host's event loop:
4//!
5//! 1. **Immediate** — [`request_tick`] / [`wants_tick`].  Any widget whose
6//!    state just changed calls `request_tick()`; the next iteration of the
7//!    host loop paints a frame and clears the flag.  This is the "mark
8//!    dirty" path: input handlers, hover transitions, tweens mid-animation,
9//!    drag movement, continuous capture widgets.
10//!
11//! 2. **Scheduled** — [`request_repaint_after`] / [`next_repaint_at`].  A
12//!    widget that needs a redraw *at a future time* (text-cursor blink,
13//!    tooltip delay) calls `request_repaint_after(Duration)`; the host's
14//!    loop goes to sleep with `ControlFlow::WaitUntil(that_instant)` and
15//!    paints when the deadline fires.  Successive calls keep the EARLIEST
16//!    deadline.
17//!
18//! The host loop paints iff `wants_tick() || now >= next_repaint_at()`.
19//! Between paints it idles; no frames are drawn while nothing has changed.
20
21use std::cell::Cell;
22use std::time::Duration;
23use web_time::Instant;
24
25std::thread_local! {
26    static NEEDS_TICK:      Cell<bool>            = Cell::new(false);
27    static NEXT_REPAINT_AT: Cell<Option<Instant>> = Cell::new(None);
28}
29
30/// Request that the host schedule another paint as soon as possible.  Safe to
31/// call any number of times in a frame.  Typically called from `Widget::paint`
32/// while a time-based animation is in progress, from input handlers whose
33/// widget state changed, or from anywhere that mutates visual state.
34pub fn request_tick() {
35    NEEDS_TICK.with(|c| c.set(true));
36}
37
38/// Non-destructive read.  Hosts call this after painting to decide control-flow
39/// for the next loop iteration.
40pub fn wants_tick() -> bool {
41    NEEDS_TICK.with(|c| c.get())
42}
43
44/// Reset the per-frame repaint flags.  The `App::paint` entry point calls
45/// this before delegating to the root widget so each frame starts fresh —
46/// widgets that still need a redraw (animation in flight, focus blink, etc.)
47/// must re-arm during their paint, otherwise the loop goes idle.
48pub fn clear_tick() {
49    NEEDS_TICK.with(|c| c.set(false));
50    NEXT_REPAINT_AT.with(|c| c.set(None));
51}
52
53/// Schedule a future paint.  Keeps the EARLIEST pending deadline, so multiple
54/// widgets asking for different delays will all be served by the soonest one
55/// (each widget re-arms its own deadline on the next paint anyway).
56pub fn request_repaint_after(delay: Duration) {
57    let when = Instant::now() + delay;
58    NEXT_REPAINT_AT.with(|c| {
59        match c.get() {
60            Some(existing) if existing <= when => {}
61            _                                  => c.set(Some(when)),
62        }
63    });
64}
65
66/// Read-and-clear the scheduled repaint deadline.  The host reads this after
67/// painting so the next frame's scheduled wake is determined entirely by what
68/// the fresh paint registered (e.g. a text field re-arms the 500 ms blink
69/// each frame while it remains focused; losing focus means no re-arm and the
70/// loop goes idle).
71pub fn take_next_repaint() -> Option<Instant> {
72    NEXT_REPAINT_AT.with(|c| c.replace(None))
73}
74
75// ── Tween ────────────────────────────────────────────────────────────────────
76//
77// Small reusable time-based interpolator for widgets that want a smooth
78// transition between two scalar states (hover ↔ dormant, off ↔ on, etc.).
79// Ease-out cubic; reversal preserves the current value so rapid toggles
80// don't snap.  Requests an animation tick automatically while in flight.
81
82/// Smooth scalar tween between `0.0` and `1.0` (or any pair of values the
83/// caller interprets).  Drives animations such as the scroll-bar hover
84/// expansion and toggle-switch on/off slide.
85#[derive(Clone, Copy)]
86pub struct Tween {
87    current:     f64,
88    start_value: f64,
89    target:      f64,
90    start_time:  Option<Instant>,
91    duration:    f64,
92}
93
94impl Tween {
95    /// New tween that starts at `initial` with the same value as its target
96    /// (no animation in flight).
97    pub const fn new(initial: f64, duration_secs: f64) -> Self {
98        Self {
99            current:     initial,
100            start_value: initial,
101            target:      initial,
102            start_time:  None,
103            duration:    duration_secs,
104        }
105    }
106
107    /// Update the target.  If it differs from the current target, re-anchors
108    /// the animation at the current interpolated value so reversals are smooth.
109    pub fn set_target(&mut self, new_target: f64) {
110        if (self.target - new_target).abs() > 1e-9 {
111            self.start_value = self.current;
112            self.target      = new_target;
113            self.start_time  = Some(Instant::now());
114        }
115    }
116
117    /// Advance the animation based on elapsed wall time and return the new
118    /// interpolated value.  Ease-out cubic.  While in flight this also calls
119    /// [`request_tick`] so the host keeps painting frames until completion.
120    pub fn tick(&mut self) -> f64 {
121        if let Some(start) = self.start_time {
122            let elapsed = start.elapsed().as_secs_f64();
123            let p = (elapsed / self.duration).min(1.0);
124            let eased = 1.0 - (1.0 - p).powi(3);
125            self.current = self.start_value + (self.target - self.start_value) * eased;
126            if p >= 1.0 {
127                self.current    = self.target;
128                self.start_time = None;
129            } else {
130                request_tick();
131            }
132        }
133        self.current
134    }
135
136    /// Current interpolated value without advancing.
137    pub fn value(&self) -> f64 { self.current }
138}
139
140impl Default for Tween {
141    fn default() -> Self { Self::new(0.0, 0.12) }
142}