diff --git a/crates/tinyclient/src/gui/anim.rs b/crates/tinyclient/src/gui/anim.rs index aab8a55..d45a5c7 100644 --- a/crates/tinyclient/src/gui/anim.rs +++ b/crates/tinyclient/src/gui/anim.rs @@ -2,18 +2,24 @@ //! draws next. A picture plays only while it is drawn, on screen, in the key window, and as the //! user's autoplay setting and the system's Reduce Motion allow. Nothing asks for a frame at the //! display's rate: one timer per window wakes it when the next frame of any playing picture is -//! due, so a 10 fps GIF draws the window 10 times a second and a paused one never. +//! due, so a 10 fps GIF draws the window 10 times a second and a paused one never. A file's +//! faster frames are skipped rather than drawn, and the timer wakes the window at most once +//! every [`FASTEST`], however many pictures play. The wake notifies only the views whose +//! pictures are due rather than refreshing the window, which would draw even a view embedded +//! with `.cached` again; the window's root alone is refreshed, as notifying it would also run +//! whatever observes it. //! //! Decoded frames are paid for from one of two [`Budget`]s, so pictures in messages and //! avatars cannot crowd each other out and neither grows without end. use super::app; use gpui::{ - canvas, img, prelude::*, px, AnyElement, App, Bounds, Corners, ImageId, ImageSource, MouseMoveEvent, ObjectFit, - Pixels, RenderImage, Task, Window, WindowId, + canvas, img, prelude::*, px, AnyElement, App, Bounds, Corners, EntityId, ImageId, ImageSource, MouseMoveEvent, + ObjectFit, Pixels, RenderImage, Task, Window, WindowId, }; use std::cell::RefCell; use std::collections::HashMap; +use std::hash::Hash; use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; @@ -169,8 +175,76 @@ struct Run { thread_local! { static RUNS: RefCell> = RefCell::new(HashMap::new()); - /// Each window's one pending wake, and when it fires. - static WAKES: RefCell)>> = RefCell::new(HashMap::new()); + /// Each window's one pending wake, when it fires, and the views it is for. + static WAKES: RefCell> = RefCell::new(HashMap::new()); +} + +/// The shortest time between two draws for moving pictures: about 30 a second. Many GIFs ask +/// for 20 ms frames; their time is kept and the frames between are skipped, so they play at +/// their speed for a third of the draws. +const FASTEST: Duration = Duration::from_millis(33); + +/// When to draw a playing picture again, `next` being when its next frame is due. +fn next_draw(next: Duration) -> Duration { + next.max(FASTEST) +} + +/// A window's timer: when it fires and for which views, and the task that waits for it. +struct Wake { + due: Due, + task: Option>, +} + +/// When a window's timer fires, and which views it draws: each view showing a playing picture +/// says when it wants drawing again, and the timer fires for the soonest. While no view is +/// `.cached`, any wake draws every view, so [`FASTEST`] holds for the window rather than each +/// view: two views on different beats would otherwise draw it at both. +struct Due { + at: HashMap, + /// When the timer is set for; `None` while it is idle. + timer: Option, + /// When it last fired for a view. + fired: Option, +} + +impl Due { + fn new() -> Self { + Due { at: HashMap::new(), timer: None, fired: None } + } + + /// Notes that `key` wants drawing at `at`, unless it already does sooner, and says when to + /// set the timer for if it must now fire sooner than it is set. + fn add(&mut self, key: K, at: Instant) -> Option { + self.at.entry(key).and_modify(|due| *due = (*due).min(at)).or_insert(at); + let next = self.next()?; + if self.timer.is_some_and(|timer| timer <= next) { + return None; + } + self.timer = Some(next); + self.timer + } + + /// The timer has fired at `now`: takes every key due, and says when to set it for the rest; + /// `None` leaves it idle until a key is added. + fn fire(&mut self, now: Instant) -> (Vec, Option) { + // A timer may fire a little early; what is due within a millisecond goes now. + let by = now + Duration::from_millis(1); + let due: Vec = self.at.iter().filter(|(_, at)| **at <= by).map(|(key, _)| *key).collect(); + for key in &due { + self.at.remove(key); + } + if !due.is_empty() { + self.fired = Some(now); + } + self.timer = self.next(); + (due, self.timer) + } + + /// When the soonest key is due, but no sooner than [`FASTEST`] after the last firing. + fn next(&self) -> Option { + let soonest = self.at.values().min().copied()?; + Some(self.fired.map_or(soonest, |fired| soonest.max(fired + FASTEST))) + } } /// The frame to show `played` into an animation of these delays, and how long until the next @@ -211,11 +285,17 @@ fn frame_now(bounds: Bounds, image: &Arc, play: Play, windo }; let allowed = by_hover.unwrap_or(play == Play::Asked || autoplay() != Autoplay::Never); let on_screen = window.content_mask().bounds.intersects(&bounds); - // The pointer arriving or leaving starts or stops it, so the window must hear of it. + // The view that draws it, which alone is drawn again for its next frame. + let view = window.current_view(); + // The pointer arriving or leaving starts or stops it, so its view must hear of it: once the + // window is let go, which is when its root can be told apart. if on_screen && by_hover.is_some() { - window.on_mouse_event(move |event: &MouseMoveEvent, _, window, _| { + let handle = window.window_handle(); + window.on_mouse_event(move |event: &MouseMoveEvent, _, _, cx| { if bounds.contains(&event.position) != hovered { - window.refresh(); + cx.defer(move |cx| { + let _ = handle.update(cx, |root, window, cx| redraw(view, root.entity_id(), window, cx)); + }); } }); } @@ -243,7 +323,7 @@ fn frame_now(bounds: Bounds, image: &Arc, play: Play, windo (frame, next.filter(|_| run.last.is_some())) }); if let Some(next) = next { - wake(now + next, window, cx); + wake(now + next_draw(next), view, window, cx); } frame } @@ -260,20 +340,59 @@ fn shot_frames() -> bool { false } -/// Has the window drawn at `at`, unless a wake already comes sooner. -fn wake(at: Instant, window: &mut Window, cx: &mut App) { +/// Has `view` drawn again at `at`, and the window's timer set sooner if it must be. +fn wake(at: Instant, view: EntityId, window: &mut Window, cx: &mut App) { let id = window.window_handle().window_id(); - // A wake already past has fired (or is firing) and is replaced; a later one is dropped, - // which cancels it: the draw this one causes schedules whatever is still due. - let sooner = WAKES.with(|wakes| wakes.borrow().get(&id).is_none_or(|(pending, _)| at < *pending || *pending <= Instant::now())); - if !sooner { - return; + let sooner = WAKES.with(|wakes| wakes.borrow_mut().entry(id).or_insert_with(|| Wake { due: Due::new(), task: None }).due.add(view, at)); + if let Some(at) = sooner { + arm(id, at, window, cx); } +} + +/// Sets the window's timer for `at`, replacing the one there: the later one is dropped, which +/// cancels it. +fn arm(id: WindowId, at: Instant, window: &mut Window, cx: &mut App) { let task = window.spawn(cx, async move |cx| { cx.background_executor().timer(at.saturating_duration_since(Instant::now())).await; - let _ = cx.update(|window, _| window.refresh()); + if cx.update_root(|root, window, cx| fire(id, root.entity_id(), window, cx)).is_err() { + // The window is gone, and with it what it was waiting for. + let gone = WAKES.with(|wakes| wakes.borrow_mut().remove(&id)); + drop(gone); + } }); - WAKES.with(|wakes| wakes.borrow_mut().insert(id, (at, task))); + // The one replaced is dropped once the map is let go, even when it is the one firing. + let replaced = WAKES.with(|wakes| wakes.borrow_mut().get_mut(&id).and_then(|wake| wake.task.replace(task))); + drop(replaced); +} + +/// The timer has fired: the views now due are drawn, and the timer is set for the rest. +fn fire(id: WindowId, root: EntityId, window: &mut Window, cx: &mut App) { + let (due, next, done) = WAKES.with(|wakes| { + let mut wakes = wakes.borrow_mut(); + let Some(wake) = wakes.get_mut(&id) else { return (Vec::new(), None, None) }; + let (due, next) = wake.due.fire(Instant::now()); + // Nothing left: the draws these views make set the timer again if they still play. They + // ask for no sooner than FASTEST after drawing, so forgetting this firing keeps the floor. + let done = if next.is_none() { wakes.remove(&id) } else { None }; + (due, next, done) + }); + drop(done); + for view in due { + redraw(view, root, window, cx); + } + if let Some(next) = next { + arm(id, next, window, cx); + } +} + +/// Draws `view` again. The window's root is drawn by refreshing the window, the same draw: +/// notifying it would also run whatever observes it, on every frame of a picture it paints. +fn redraw(view: EntityId, root: EntityId, window: &mut Window, cx: &mut App) { + if view == root { + window.refresh(); + } else { + cx.notify(view); + } } /// A decoded picture filling its parent: `img` for a still, a moving one painted frame by @@ -306,6 +425,91 @@ mod tests { assert_eq!(frame_at(&[ms(80)], None, ms(500)), (0, None)); } + #[test] + fn a_fast_animation_asks_no_sooner_than_fastest_and_keeps_its_time() { + let ms = Duration::from_millis; + assert_eq!(next_draw(ms(20)), FASTEST); + assert_eq!(next_draw(ms(1)), FASTEST); + assert_eq!(next_draw(ms(100)), ms(100)); + // Twenty 20 ms frames drawn every 33 ms: the frame shown follows the clock, skipping. + let delays = [ms(20); 20]; + assert_eq!(frame_at(&delays, None, FASTEST).0, 1); + assert_eq!(frame_at(&delays, None, FASTEST * 2).0, 3); + assert_eq!(frame_at(&delays, None, FASTEST * 3).0, 4); + } + + #[test] + fn a_wake_draws_only_the_views_that_are_due() { + let now = Instant::now(); + let ms = Duration::from_millis; + let mut due = Due::new(); + assert_eq!(due.add(1, now + ms(100)), Some(now + ms(100))); + assert_eq!(due.add(2, now + ms(40)), Some(now + ms(40))); + // A sooner ask for a view replaces its later one; a later one does not, and neither + // moves a timer already set sooner. + assert_eq!(due.add(1, now + ms(50)), None); + assert_eq!(due.add(2, now + ms(80)), None); + // Firing early draws nothing and keeps the timer. + assert_eq!(due.fire(now + ms(30)), (vec![], Some(now + ms(40)))); + // The next is held FASTEST after this one, though it is due sooner. + assert_eq!(due.fire(now + ms(40)), (vec![2], Some(now + ms(40) + FASTEST))); + assert_eq!(due.fire(now + ms(40) + FASTEST), (vec![1], None)); + // Idle until asked again. + assert_eq!(due.add(1, now + ms(200)), Some(now + ms(200))); + } + + /// Plays views on these beats for `playing`, as a window would: every draw has every view + /// ask for its next frame, none being `.cached`, and the timer fires when `Due` says. When it + /// started, each draw, and each ask. + fn play(beats: &[(u8, Duration)], playing: Duration) -> (Instant, Vec, Vec) { + let start = Instant::now(); + let (mut due, mut timer, mut now) = (Due::new(), None, start); + let (mut draws, mut asks) = (Vec::new(), Vec::new()); + for _ in 0..1000 { + draws.push(now); + if now - start < playing { + for &(view, beat) in beats { + let into = Duration::from_nanos(((now - start).as_nanos() % beat.as_nanos()) as u64); + let at = now + next_draw(beat - into); + asks.push(at); + timer = due.add(view, at).or(timer); + } + } + // A firing with nothing due draws nothing; none set is idle. + loop { + let Some(at) = timer else { return (start, draws, asks) }; + let (drawn, next) = due.fire(at); + (timer, now) = (next, at); + if !drawn.is_empty() { + break; + } + } + } + panic!("the timer never went idle"); + } + + #[test] + fn the_window_wakes_at_most_thirty_times_a_second_however_many_views_play() { + let ms = Duration::from_millis; + // Two views on beats that seldom meet: each asks no sooner than FASTEST after a draw, + // and the other's earlier ask must not draw the window in between. + let (start, draws, asks) = play(&[(1, ms(20)), (2, ms(45))], ms(1000)); + let gaps: Vec = draws.windows(2).map(|pair| pair[1] - pair[0]).collect(); + assert!(gaps.iter().all(|gap| *gap >= FASTEST), "{gaps:?}"); + let first_second = draws.iter().filter(|draw| **draw - start < ms(1000)).count(); + assert!((30..=31).contains(&first_second), "{first_second} draws in the first second"); + // No ask is lost: a draw meets each, no more than FASTEST late. + for at in &asks { + assert!(draws.iter().any(|draw| *draw + ms(1) >= *at && *draw <= *at + FASTEST), "{:?} unmet", *at - start); + } + // Once none plays, what was asked is drawn and the timer goes idle. + assert!(*draws.last().unwrap() - start <= ms(1000) + FASTEST * 2); + // A slow picture alone is drawn at its own pace. + let (_, draws, _) = play(&[(1, ms(100))], ms(1000)); + assert_eq!(draws.len(), 11); + assert!(draws.windows(2).all(|pair| pair[1] - pair[0] == ms(100))); + } + #[test] fn a_budget_takes_only_what_fits_and_gets_it_back() { let budget = Budget { held: AtomicUsize::new(0), most: 100 };