1use 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#[derive(Clone)]
32pub struct PlotAppear {
33 time: f32,
35 easing: Easing,
36}
37
38impl PlotAppear {
39 pub fn complete() -> Self {
41 Self {
42 time: 1.,
43 easing: Easing::Linear,
44 }
45 }
46
47 pub fn progress(&self) -> f32 {
49 if self.time >= 1. {
52 return 1.;
53 }
54 self.easing.sample(self.time)
55 }
56
57 pub fn is_appearing(&self) -> bool {
59 self.time < 1.
60 }
61
62 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
79const APPEAR: &str = "__plot-appear";
81
82type Appeared = Rc<RefCell<HashMap<GlobalElementId, u64>>>;
85
86thread_local! {
87 static SCOPES: RefCell<Vec<Appeared>> = const { RefCell::new(Vec::new()) };
90}
91
92pub struct PlotAppearScope {
114 id: ElementId,
115 child: Option<AnyElement>,
116}
117
118impl PlotAppearScope {
119 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 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 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
212fn current_scope() -> Option<Appeared> {
214 SCOPES.with_borrow(|scopes| scopes.last().cloned())
215}
216
217pub(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 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 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 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 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 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 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 #[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 #[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 #[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 assert_eq!(
505 update_scoped(window, cx, |view| view.generation = 1),
506 Some(0.)
507 );
508 }
509
510 #[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 #[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 #[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 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 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 assert_eq!(at(0.4).staggered(0, 1, 0.5), 0.4);
590 }
591}