Skip to main content

gpui_base/plot/
appear.rs

1//! The appear motion shared by every [`Plot`](super::Plot): how far its data
2//! marks have drawn in since the plot was first painted.
3//!
4//! This is behavior only. A plot decides what appearing looks like — a line
5//! revealed from the left, bars growing from zero — and a styled layer
6//! projects the timing through [`PlotMotion`](crate::PlotMotion).
7//!
8//! The appear lives in the plot's element state, so a plot that stops being
9//! painted — a row a virtual list scrolled away — forgets it and draws in again
10//! when it comes back. A [`PlotAppearScope`] around the list remembers which
11//! plots have finished, for as long as the scope itself is painted.
12use std::{cell::RefCell, collections::HashMap, panic::Location, rc::Rc};
13
14use gpui::{
15    AnyElement, App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement,
16    LayoutId, Pixels, Window,
17};
18
19use crate::{
20    Theme,
21    motion::{Easing, Presence},
22};
23
24/// How far a plot's data marks have appeared this frame, handed to
25/// [`Plot::appear`](super::Plot::appear).
26///
27/// The appear starts on the first frame a plot's id is painted and runs over
28/// the active [`PlotMotion`](crate::PlotMotion)'s appear. Base's default
29/// duration is zero, and reduced motion skips it, so a plot is then complete
30/// from its first frame.
31#[derive(Clone)]
32pub struct PlotAppear {
33    /// Linear time through the appear, from `0` to `1`.
34    time: f32,
35    easing: Easing,
36}
37
38impl PlotAppear {
39    /// A finished appear: every mark is complete.
40    pub fn complete() -> Self {
41        Self {
42            time: 1.,
43            easing: Easing::Linear,
44        }
45    }
46
47    /// How far the whole plot has appeared, from `0` to `1`, eased.
48    pub fn progress(&self) -> f32 {
49        // Charts read this per mark on every frame, long after the appear is
50        // done, so a finished appear skips sampling the curve.
51        if self.time >= 1. {
52            return 1.;
53        }
54        self.easing.sample(self.time)
55    }
56
57    /// Whether the appear is still running.
58    pub fn is_appearing(&self) -> bool {
59        self.time < 1.
60    }
61
62    /// How far mark `index` of `count` has appeared, from `0` to `1`, eased.
63    ///
64    /// The marks start one after another across the first `spread` of the
65    /// appear (`0..1`) and each runs for the rest of it, so the last mark
66    /// still finishes with the appear however many marks there are. A `spread`
67    /// of `0` moves every mark together.
68    pub fn staggered(&self, index: usize, count: usize, spread: f32) -> f32 {
69        if count <= 1 || self.time >= 1. {
70            return self.progress();
71        }
72        let spread = spread.clamp(0., 0.95);
73        let start = spread * index.min(count - 1) as f32 / (count - 1) as f32;
74        let time = ((self.time - start) / (1. - spread)).clamp(0., 1.);
75        self.easing.sample(time)
76    }
77}
78
79/// The element-state key of a plot's appear, within the plot's scope.
80const APPEAR: &str = "__plot-appear";
81
82/// The plots a [`PlotAppearScope`] has seen finish appearing, by global id,
83/// with the generation that finished.
84type Appeared = Rc<RefCell<HashMap<GlobalElementId, u64>>>;
85
86thread_local! {
87    /// The scopes being laid out or painted, innermost last. Only non-empty
88    /// while a [`PlotAppearScope`] is drawing its child.
89    static SCOPES: RefCell<Vec<Appeared>> = const { RefCell::new(Vec::new()) };
90}
91
92/// Remembers which plots inside it have finished appearing, so a plot that is
93/// painted again after a gap — a row a virtual list scrolled out of view and
94/// back — shows its data whole instead of drawing in again.
95///
96/// Wrap the list, or whatever region repaints its plots on and off, in one:
97///
98/// ```ignore
99/// PlotAppearScope::new(("transcript", conversation_id), list(state, render_row).flex_1())
100/// ```
101///
102/// A plot is remembered by its global element id and its
103/// [`Plot::appear_generation`](super::Plot::appear_generation), once its appear
104/// has finished; a new generation still replays it, and one taken away
105/// mid-appear draws in again from the start. The memory is the scope's own
106/// element state: it lasts while the scope is painted every frame and goes with
107/// it, so a view that is closed, or a scope whose id changes — name it after the
108/// content, such as a conversation — draws its plots in afresh. The innermost
109/// scope wins. Without one, every plot draws in each time it is painted anew.
110///
111/// The scope takes no part in layout: it hands on its child's layout, so a
112/// child that sizes itself, such as a `list`, keeps doing so.
113pub struct PlotAppearScope {
114    id: ElementId,
115    child: Option<AnyElement>,
116}
117
118impl PlotAppearScope {
119    /// Remember the appears of the plots in `child` under `id`, unique among
120    /// its siblings.
121    pub fn new(id: impl Into<ElementId>, child: impl IntoElement) -> Self {
122        Self {
123            id: id.into(),
124            child: Some(child.into_any_element()),
125        }
126    }
127
128    /// Run `f` with `appeared` as the innermost scope.
129    fn within<R>(appeared: &Appeared, f: impl FnOnce() -> R) -> R {
130        SCOPES.with_borrow_mut(|scopes| scopes.push(appeared.clone()));
131        let result = f();
132        SCOPES.with_borrow_mut(|scopes| scopes.pop());
133        result
134    }
135}
136
137impl IntoElement for PlotAppearScope {
138    type Element = Self;
139
140    fn into_element(self) -> Self::Element {
141        self
142    }
143}
144
145impl Element for PlotAppearScope {
146    type RequestLayoutState = (Option<AnyElement>, Appeared);
147    type PrepaintState = ();
148
149    fn id(&self) -> Option<ElementId> {
150        Some(self.id.clone())
151    }
152
153    fn source_location(&self) -> Option<&'static Location<'static>> {
154        None
155    }
156
157    fn request_layout(
158        &mut self,
159        global_id: Option<&GlobalElementId>,
160        _: Option<&InspectorElementId>,
161        window: &mut Window,
162        cx: &mut App,
163    ) -> (LayoutId, Self::RequestLayoutState) {
164        let appeared: Appeared = match global_id {
165            Some(global_id) => window.with_element_state(global_id, |appeared, _| {
166                let appeared: Appeared = appeared.unwrap_or_default();
167                (appeared.clone(), appeared)
168            }),
169            None => Appeared::default(),
170        };
171        let mut child = self.child.take();
172        let layout_id = Self::within(&appeared, || match child.as_mut() {
173            Some(child) => child.request_layout(window, cx),
174            None => window.request_layout(Default::default(), None, cx),
175        });
176        (layout_id, (child, appeared))
177    }
178
179    fn prepaint(
180        &mut self,
181        _: Option<&GlobalElementId>,
182        _: Option<&InspectorElementId>,
183        _: Bounds<Pixels>,
184        (child, appeared): &mut Self::RequestLayoutState,
185        window: &mut Window,
186        cx: &mut App,
187    ) -> Self::PrepaintState {
188        // A virtual list lays its rows out here, so their plots appear here.
189        if let Some(child) = child {
190            Self::within(appeared, || {
191                child.prepaint(window, cx);
192            });
193        }
194    }
195
196    fn paint(
197        &mut self,
198        _: Option<&GlobalElementId>,
199        _: Option<&InspectorElementId>,
200        _: Bounds<Pixels>,
201        (child, appeared): &mut Self::RequestLayoutState,
202        _: &mut Self::PrepaintState,
203        window: &mut Window,
204        cx: &mut App,
205    ) {
206        if let Some(child) = child {
207            Self::within(appeared, || child.paint(window, cx));
208        }
209    }
210}
211
212/// The innermost scope, if any, cloned out so no borrow outlives the lookup.
213fn current_scope() -> Option<Appeared> {
214    SCOPES.with_borrow(|scopes| scopes.last().cloned())
215}
216
217/// Sample the appear of the plot painting under the window's current element
218/// id, `global_id`. A new `generation` starts it over, and one the innermost
219/// [`PlotAppearScope`] saw finish is complete at once. Called by
220/// [`PlotElement`](super::PlotElement) within the plot's element scope on every
221/// frame, so it borrows the theme rather than cloning it and builds its key
222/// without allocating.
223pub(super) fn track_appear(
224    global_id: &GlobalElementId,
225    generation: u64,
226    window: &mut Window,
227    cx: &mut App,
228) -> PlotAppear {
229    let scope = current_scope();
230    if scope
231        .as_ref()
232        .is_some_and(|scope| scope.borrow().get(global_id) == Some(&generation))
233    {
234        return PlotAppear::complete();
235    }
236    let Some(policy) = cx
237        .try_global::<Theme>()
238        .map(|theme| theme.plot.motion().appear().clone())
239    else {
240        return PlotAppear::complete();
241    };
242    let easing = policy.curve().clone();
243    // Presence keeps the linear time so the marks can each ease over their
244    // own slice of it; see `PlotAppear::staggered`.
245    let sample = Presence::new((ElementId::Integer(generation), APPEAR), true)
246        .transition(policy.easing(Easing::Linear))
247        .sample(window, cx);
248    if sample.progress >= 1.
249        && let Some(scope) = scope
250    {
251        scope.borrow_mut().insert(global_id.clone(), generation);
252    }
253    PlotAppear {
254        time: sample.progress,
255        easing,
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use std::{cell::RefCell, rc::Rc, time::Duration};
262
263    use gpui::{
264        Bounds, Context, ElementId, IntoElement, ParentElement as _, Pixels, Render, Styled as _,
265        TestAppContext, WindowHandle, px, size,
266    };
267
268    use super::*;
269    use crate::{
270        PlotMotion, PlotTheme,
271        motion::Transition,
272        plot::{Plot, PlotElement},
273    };
274
275    /// A plot that records the appear progress it is handed each frame.
276    struct Recorder {
277        samples: Rc<RefCell<Vec<f32>>>,
278        generation: Option<u64>,
279    }
280
281    impl IntoElement for Recorder {
282        type Element = PlotElement<Self>;
283
284        fn into_element(self) -> Self::Element {
285            PlotElement::new(self)
286        }
287    }
288
289    impl Plot for Recorder {
290        fn paint(&mut self, _: Bounds<Pixels>, _: &mut Window, _: &mut App) {}
291
292        fn id(&self) -> Option<ElementId> {
293            Some("recorder".into())
294        }
295
296        // Appear motion rides on the id alone, without the interactive layer.
297        fn interactive(&self) -> bool {
298            false
299        }
300
301        fn appear(&mut self, appear: PlotAppear, _: &mut Window, _: &mut App) {
302            self.samples.borrow_mut().push(appear.progress());
303        }
304
305        fn appear_generation(&self) -> Option<u64> {
306            self.generation
307        }
308    }
309
310    struct RecorderView {
311        samples: Rc<RefCell<Vec<f32>>>,
312        generation: Option<u64>,
313    }
314
315    impl Render for RecorderView {
316        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
317            Recorder {
318                samples: self.samples.clone(),
319                generation: self.generation,
320            }
321        }
322    }
323
324    /// A recorder that can be taken out of the tree and put back, inside a
325    /// [`PlotAppearScope`] named `scope` or none.
326    struct ScopedView {
327        samples: Rc<RefCell<Vec<f32>>>,
328        scope: Option<usize>,
329        mounted: bool,
330        generation: u64,
331    }
332
333    impl Render for ScopedView {
334        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
335            let plot = self.mounted.then(|| Recorder {
336                samples: self.samples.clone(),
337                generation: Some(self.generation),
338            });
339            let body = gpui::div().size_full().children(plot);
340            match self.scope {
341                Some(scope) => PlotAppearScope::new(("scope", scope), body).into_any_element(),
342                None => body.into_any_element(),
343            }
344        }
345    }
346
347    fn set_theme(cx: &mut TestAppContext) {
348        cx.update(|cx| {
349            cx.set_global(Theme::default());
350            Theme::global_mut(cx).plot = PlotTheme::new().with_motion(
351                PlotMotion::default()
352                    .with_appear(Transition::new(Duration::from_millis(100)).ease(|t| t)),
353            );
354        });
355    }
356
357    fn open_scoped(
358        cx: &mut TestAppContext,
359        scope: Option<usize>,
360    ) -> (WindowHandle<ScopedView>, Rc<RefCell<Vec<f32>>>) {
361        set_theme(cx);
362        let samples = Rc::new(RefCell::new(Vec::new()));
363        let window = cx.open_window(size(px(100.), px(100.)), {
364            let samples = samples.clone();
365            move |_, _| ScopedView {
366                samples,
367                scope,
368                mounted: true,
369                generation: 0,
370            }
371        });
372        cx.run_until_parked();
373        (window, samples)
374    }
375
376    /// Change the view, then return the progress the plot was handed on the
377    /// frame that follows, if it was painted.
378    fn update_scoped(
379        window: WindowHandle<ScopedView>,
380        cx: &mut TestAppContext,
381        f: impl FnOnce(&mut ScopedView),
382    ) -> Option<f32> {
383        window
384            .update(cx, |view, _, cx| {
385                f(view);
386                view.samples.borrow_mut().clear();
387                cx.notify();
388            })
389            .unwrap();
390        cx.run_until_parked();
391        window
392            .update(cx, |view, _, _| view.samples.borrow().last().copied())
393            .unwrap()
394    }
395
396    fn finish_appear(window: WindowHandle<ScopedView>, cx: &mut TestAppContext) {
397        cx.executor().advance_clock(Duration::from_millis(100));
398        window
399            .update(cx, |_, window, cx| window.simulate_next_frame(cx))
400            .unwrap();
401        cx.run_until_parked();
402    }
403
404    fn open(
405        cx: &mut TestAppContext,
406        generation: Option<u64>,
407    ) -> (WindowHandle<RecorderView>, Rc<RefCell<Vec<f32>>>) {
408        set_theme(cx);
409        let samples = Rc::new(RefCell::new(Vec::new()));
410        let window = cx.open_window(size(px(100.), px(100.)), {
411            let samples = samples.clone();
412            move |_, _| RecorderView {
413                samples,
414                generation,
415            }
416        });
417        cx.run_until_parked();
418        (window, samples)
419    }
420
421    fn next_frame(window: WindowHandle<RecorderView>, cx: &mut TestAppContext) -> usize {
422        let frames = window
423            .update(cx, |_, window, cx| window.simulate_next_frame(cx))
424            .unwrap();
425        cx.run_until_parked();
426        frames
427    }
428
429    #[gpui::test]
430    fn test_plot_appears_once_over_the_theme_duration(cx: &mut TestAppContext) {
431        let (window, samples) = open(cx, Some(0));
432        assert_eq!(samples.borrow().last(), Some(&0.));
433
434        cx.executor().advance_clock(Duration::from_millis(50));
435        next_frame(window, cx);
436        assert_eq!(samples.borrow().last(), Some(&0.5));
437
438        cx.executor().advance_clock(Duration::from_millis(50));
439        next_frame(window, cx);
440        assert_eq!(samples.borrow().last(), Some(&1.));
441
442        // Once whole, the plot stops asking for frames and stays whole.
443        assert_eq!(next_frame(window, cx), 0);
444        window.update(cx, |_, window, _| window.refresh()).unwrap();
445        cx.run_until_parked();
446        assert_eq!(samples.borrow().last(), Some(&1.));
447    }
448
449    #[gpui::test]
450    fn test_reduced_motion_skips_the_appear(cx: &mut TestAppContext) {
451        cx.update(|cx| cx.set_reduce_motion(true));
452        let (window, samples) = open(cx, Some(0));
453        assert_eq!(samples.borrow().first(), Some(&1.));
454        assert_eq!(next_frame(window, cx), 0);
455    }
456
457    /// A plot that does not opt in is whole at once and asks for no frames,
458    /// even with an appear duration in the theme.
459    #[gpui::test]
460    fn test_plot_without_a_generation_does_not_appear(cx: &mut TestAppContext) {
461        let (window, samples) = open(cx, None);
462        assert_eq!(samples.borrow().first(), Some(&1.));
463        assert_eq!(next_frame(window, cx), 0);
464    }
465
466    /// A new generation starts the appear over.
467    #[gpui::test]
468    fn test_new_generation_replays_the_appear(cx: &mut TestAppContext) {
469        let (window, samples) = open(cx, Some(0));
470        cx.executor().advance_clock(Duration::from_millis(100));
471        next_frame(window, cx);
472        assert_eq!(samples.borrow().last(), Some(&1.));
473
474        window
475            .update(cx, |view, _, cx| {
476                view.generation = Some(1);
477                cx.notify();
478            })
479            .unwrap();
480        cx.run_until_parked();
481        assert_eq!(samples.borrow().last(), Some(&0.));
482    }
483
484    /// Inside a scope, a plot painted again after a gap is whole at once and
485    /// asks for no frames.
486    #[gpui::test]
487    fn test_scope_keeps_a_finished_appear_across_a_remount(cx: &mut TestAppContext) {
488        let (window, samples) = open_scoped(cx, Some(0));
489        assert_eq!(samples.borrow().last(), Some(&0.));
490        finish_appear(window, cx);
491        assert_eq!(samples.borrow().last(), Some(&1.));
492
493        assert_eq!(update_scoped(window, cx, |view| view.mounted = false), None);
494        assert_eq!(
495            update_scoped(window, cx, |view| view.mounted = true),
496            Some(1.)
497        );
498        let frames = window
499            .update(cx, |_, window, cx| window.simulate_next_frame(cx))
500            .unwrap();
501        assert_eq!(frames, 0);
502
503        // A new generation still replays.
504        assert_eq!(
505            update_scoped(window, cx, |view| view.generation = 1),
506            Some(0.)
507        );
508    }
509
510    /// A plot taken away before its appear finished draws in again.
511    #[gpui::test]
512    fn test_scope_replays_an_unfinished_appear(cx: &mut TestAppContext) {
513        let (window, _) = open_scoped(cx, Some(0));
514        assert_eq!(update_scoped(window, cx, |view| view.mounted = false), None);
515        assert_eq!(
516            update_scoped(window, cx, |view| view.mounted = true),
517            Some(0.)
518        );
519    }
520
521    /// The memory goes with the scope: a scope under a new id, or one that
522    /// stops being painted, draws its plots in afresh.
523    #[gpui::test]
524    fn test_scope_forgets_when_it_goes(cx: &mut TestAppContext) {
525        let (window, _) = open_scoped(cx, Some(0));
526        finish_appear(window, cx);
527        assert_eq!(
528            update_scoped(window, cx, |view| view.scope = Some(1)),
529            Some(0.)
530        );
531
532        finish_appear(window, cx);
533        assert_eq!(
534            update_scoped(window, cx, |view| view.scope = None),
535            Some(0.)
536        );
537        finish_appear(window, cx);
538        assert_eq!(
539            update_scoped(window, cx, |view| view.scope = Some(1)),
540            Some(0.)
541        );
542    }
543
544    /// Without a scope, a plot painted again after a gap draws in again.
545    #[gpui::test]
546    fn test_without_a_scope_a_remount_replays(cx: &mut TestAppContext) {
547        let (window, _) = open_scoped(cx, None);
548        finish_appear(window, cx);
549        assert_eq!(update_scoped(window, cx, |view| view.mounted = false), None);
550        assert_eq!(
551            update_scoped(window, cx, |view| view.mounted = true),
552            Some(0.)
553        );
554    }
555
556    fn at(time: f32) -> PlotAppear {
557        PlotAppear {
558            time,
559            easing: Easing::Linear,
560        }
561    }
562
563    #[test]
564    fn test_complete_appear() {
565        let appear = PlotAppear::complete();
566        assert!(!appear.is_appearing());
567        assert_eq!(appear.progress(), 1.);
568        assert_eq!(appear.staggered(3, 10, 0.5), 1.);
569    }
570
571    #[test]
572    fn test_staggered_marks_share_the_appear() {
573        // The first mark starts at once, the last once the spread has passed.
574        assert_eq!(at(0.).staggered(0, 5, 0.5), 0.);
575        assert_eq!(at(0.25).staggered(0, 5, 0.5), 0.5);
576        assert_eq!(at(0.5).staggered(4, 5, 0.5), 0.);
577        assert_eq!(at(0.75).staggered(4, 5, 0.5), 0.5);
578        // Every mark finishes with the appear.
579        for index in 0..5 {
580            assert_eq!(at(1.).staggered(index, 5, 0.5), 1.);
581        }
582    }
583
584    #[test]
585    fn test_staggered_without_spread_moves_together() {
586        assert_eq!(at(0.4).staggered(0, 3, 0.), 0.4);
587        assert_eq!(at(0.4).staggered(2, 3, 0.), 0.4);
588        // A lone mark ignores the spread.
589        assert_eq!(at(0.4).staggered(0, 1, 0.5), 0.4);
590    }
591}