1use std::cell::RefCell;
39use std::panic::Location;
40use std::rc::Rc;
41use std::time::Duration;
42
43use gpui::{
44 App, AvailableSpace, Bounds, Element, ElementId, GlobalElementId, InspectorElementId,
45 IntoElement, LayoutId, Pixels, Point, SharedString, Size, Style, Window, px, size,
46};
47use gpui_kit_theme::{ActiveTheme, SpringPreset};
48use web_time::Instant;
49
50use super::keyed;
51use super::{Interpolate, MotionSpec, Spring, Transition};
52
53const EPSILON: f32 = 0.5;
57
58const MEMORY: Duration = Duration::from_millis(500);
65
66const HANDOFF_GRACE: u64 = 30;
72
73#[derive(Default)]
74struct FlipState {
75 origin: Option<Point<Pixels>>,
77 from: Point<Pixels>,
79 current: Point<Pixels>,
81 elapsed: Duration,
82 last_frame: Option<Instant>,
83 size: Option<Transition<Size<Pixels>>>,
86 natural: Option<Size<Pixels>>,
88 available: Option<Size<AvailableSpace>>,
91 offered: bool,
99 recorded_at: Option<Instant>,
101 size_frame: Option<Instant>,
103 seen_frame: Option<u64>,
105 contested_through: Option<u64>,
107}
108
109impl FlipState {
110 fn advance(&mut self, now: Instant) {
111 if let Some(last) = self.last_frame {
112 self.elapsed += now.saturating_duration_since(last);
113 }
114 self.last_frame = Some(now);
115 }
116
117 fn sample(&self, spring: Spring, settle: Duration) -> Point<Pixels> {
118 if self.elapsed >= settle {
119 return Point::default();
120 }
121 self.from.lerp(Point::default(), spring.value(self.elapsed))
122 }
123
124 fn record(&mut self, origin: Point<Pixels>, residual: Point<Pixels>) {
130 if let Some(previous) = self.origin
131 && (moved(previous.x, origin.x) || moved(previous.y, origin.y))
132 {
133 self.from = previous - origin + residual;
134 self.elapsed = Duration::ZERO;
135 }
136 self.origin = Some(origin);
137 }
138
139 fn record_size(
147 &mut self,
148 natural: Size<Pixels>,
149 spec: MotionSpec,
150 now: Instant,
151 ) -> Size<Pixels> {
152 self.natural = Some(natural);
153 let mut transition = self
154 .size
155 .unwrap_or_else(|| Transition::new(natural, spec))
156 .spec(spec);
157 if let Some(last) = self.size_frame {
158 transition.advance(now.saturating_duration_since(last));
159 }
160 self.size_frame = Some(now);
161 let target = transition.target();
162 if moved(target.width, natural.width) || moved(target.height, natural.height) {
163 transition.set(natural);
164 }
165 self.size = Some(transition);
166 transition.value()
167 }
168
169 fn settle_size(&mut self, natural: Size<Pixels>, spec: MotionSpec, now: Instant) {
171 self.natural = Some(natural);
172 let mut transition = self
173 .size
174 .unwrap_or_else(|| Transition::new(natural, spec))
175 .spec(spec);
176 transition.snap(natural);
177 self.size = Some(transition);
178 self.size_frame = Some(now);
179 }
180
181 fn settle(&mut self, origin: Point<Pixels>, settle: Duration) {
183 self.origin = Some(origin);
184 self.from = Point::default();
185 self.current = Point::default();
186 self.elapsed = settle;
187 self.last_frame = None;
188 }
189
190 fn forget_if_stale(&mut self, now: Instant) {
193 let stale = self
194 .recorded_at
195 .is_some_and(|at| now.saturating_duration_since(at) > MEMORY);
196 if stale {
197 self.origin = None;
198 self.from = Point::default();
199 self.current = Point::default();
200 self.elapsed = Duration::ZERO;
201 self.last_frame = None;
202 self.size = None;
203 self.natural = None;
204 self.size_frame = None;
205 }
206 self.recorded_at = Some(now);
207 }
208
209 fn claim(&mut self, frame: Option<u64>) -> bool {
220 let Some(frame) = frame else {
221 return false;
222 };
223 if self.seen_frame == Some(frame) {
224 self.contested_through = Some(frame + 1);
225 }
226 self.seen_frame = Some(frame);
227 self.contested_through
228 .is_some_and(|through| frame <= through)
229 }
230}
231
232fn moved(a: Pixels, b: Pixels) -> bool {
233 (f32::from(a) - f32::from(b)).abs() > EPSILON
234}
235
236#[derive(Clone)]
242pub struct Flip {
243 id: SharedString,
244 state: Rc<RefCell<FlipState>>,
245}
246
247impl std::fmt::Debug for Flip {
248 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 formatter
250 .debug_struct("Flip")
251 .field("id", &self.id)
252 .field("offset", &self.offset())
253 .field("size", &self.size())
254 .finish()
255 }
256}
257
258impl Flip {
259 pub fn id(&self) -> &SharedString {
260 &self.id
261 }
262
263 pub fn offset(&self) -> Point<Pixels> {
270 self.state.borrow().current
271 }
272
273 pub fn size(&self) -> Option<Size<Pixels>> {
279 self.state.borrow().size.map(|size| size.value())
280 }
281
282 pub fn target_size(&self) -> Option<Size<Pixels>> {
284 self.state.borrow().natural
285 }
286
287 pub fn is_contended(&self) -> bool {
291 let state = self.state.borrow();
292 match (state.seen_frame, state.contested_through) {
293 (Some(frame), Some(through)) => frame <= through,
294 _ => false,
295 }
296 }
297
298 pub fn is_animating(&self) -> bool {
299 let offset = self.offset();
300 let sliding = offset.x.abs() > px(EPSILON) || offset.y.abs() > px(EPSILON);
301 sliding || self.state.borrow().size.is_some_and(|s| s.is_animating())
302 }
303}
304
305pub fn flip(id: impl Into<SharedString>, cx: &mut App) -> Flip {
307 let id = id.into();
308 let state = keyed::slot::<FlipState>(&id, cx);
309 Flip { id, state }
310}
311
312pub fn shared_flip(id: impl Into<SharedString>, cx: &mut App) -> Flip {
326 let id = id.into();
327 let state = keyed::slot_retained::<FlipState>(&id, HANDOFF_GRACE, cx);
328 Flip { id, state }
329}
330
331pub fn tracked_ids(cx: &App) -> Vec<SharedString> {
337 keyed::ids::<FlipState>(cx)
338}
339
340pub trait Flipping: IntoElement + Sized {
342 fn flip(self, flip: &Flip, window: &mut Window, cx: &mut App) -> Flipped {
351 flipped(self, flip, false, window, cx)
352 }
353
354 fn flip_size(self, flip: &Flip, window: &mut Window, cx: &mut App) -> Flipped {
366 flipped(self, flip, true, window, cx)
367 }
368}
369
370fn flipped<E: IntoElement>(
371 element: E,
372 flip: &Flip,
373 sized: bool,
374 window: &mut Window,
375 cx: &mut App,
376) -> Flipped {
377 let spring = Spring::preset(cx.theme(), SpringPreset::Grab);
378 let element = Flipped {
379 element: element.into_any_element(),
380 state: Rc::clone(&flip.state),
381 spring,
382 settle: spring.settle_time(),
383 sized,
384 measuring: false,
385 measured_against: None,
386 reduce_motion: cx.reduce_motion(),
387 frame: keyed::frame_counter(cx),
388 };
389 if flip.is_animating() {
392 window.request_animation_frame();
393 }
394 element
395}
396
397impl<E: IntoElement> Flipping for E {}
398
399pub struct Flipped {
402 element: gpui::AnyElement,
403 state: Rc<RefCell<FlipState>>,
404 spring: Spring,
405 settle: Duration,
406 sized: bool,
407 measuring: bool,
411 measured_against: Option<Size<AvailableSpace>>,
414 reduce_motion: bool,
415 frame: Option<u64>,
416}
417
418impl std::fmt::Debug for Flipped {
419 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
420 formatter
421 .debug_struct("Flipped")
422 .field("offset", &self.state.borrow().current)
423 .field("sized", &self.sized)
424 .field("reduce_motion", &self.reduce_motion)
425 .finish()
426 }
427}
428
429impl Flipped {
430 fn spec(&self) -> MotionSpec {
431 MotionSpec::sprung(self.spring)
432 }
433}
434
435impl IntoElement for Flipped {
436 type Element = Self;
437
438 fn into_element(self) -> Self::Element {
439 self
440 }
441}
442
443impl Element for Flipped {
444 type RequestLayoutState = ();
445 type PrepaintState = ();
446
447 fn id(&self) -> Option<ElementId> {
448 None
449 }
450
451 fn source_location(&self) -> Option<&'static Location<'static>> {
452 None
453 }
454
455 fn request_layout(
456 &mut self,
457 _id: Option<&GlobalElementId>,
458 _inspector_id: Option<&InspectorElementId>,
459 window: &mut Window,
460 cx: &mut App,
461 ) -> (LayoutId, ()) {
462 let now = cx.background_executor().now();
466 self.state.borrow_mut().forget_if_stale(now);
467
468 if !self.sized {
469 return (self.element.request_layout(window, cx), ());
472 }
473
474 let Some(available) = self.state.borrow().available else {
481 self.measuring = false;
482 return (self.element.request_layout(window, cx), ());
483 };
484 self.measuring = true;
485 self.measured_against = Some(available);
486
487 let natural = self.element.layout_as_root(available, window, cx);
491 let spec = self.spec();
492
493 let drawn = {
494 let mut state = self.state.borrow_mut();
495 if self.reduce_motion {
496 state.settle_size(natural, spec, now);
497 natural
498 } else {
499 state.record_size(natural, spec, now)
500 }
501 };
502
503 self.state.borrow_mut().offered = false;
504 let state = Rc::clone(&self.state);
505 let layout_id = window.request_measured_layout(
506 Style::default(),
507 move |known, available, _window, _cx| {
508 let mut state = state.borrow_mut();
509 if !state.offered {
510 state.offered = true;
511 state.available = Some(available);
512 }
513 drop(state);
514 size(
515 known.width.unwrap_or(drawn.width),
516 known.height.unwrap_or(drawn.height),
517 )
518 },
519 );
520 (layout_id, ())
521 }
522
523 fn prepaint(
524 &mut self,
525 _id: Option<&GlobalElementId>,
526 _inspector_id: Option<&InspectorElementId>,
527 bounds: Bounds<Pixels>,
528 _request_layout: &mut (),
529 window: &mut Window,
530 cx: &mut App,
531 ) {
532 let origin = bounds.origin - window.element_offset();
533 let now = cx.background_executor().now();
534 let offset = {
535 let mut state = self.state.borrow_mut();
536 let contested = state.claim(self.frame);
537 if self.sized && !self.measuring {
538 state.available = Some(size(
542 AvailableSpace::Definite(bounds.size.width),
543 AvailableSpace::Definite(bounds.size.height),
544 ));
545 state.settle_size(bounds.size, self.spec(), now);
546 }
547 if self.reduce_motion || contested {
548 state.settle(origin, self.settle);
549 if self.sized
550 && let Some(natural) = state.natural
551 {
552 state.settle_size(natural, self.spec(), now);
553 }
554 } else {
555 state.advance(now);
556 let residual = state.sample(self.spring, self.settle);
557 state.record(origin, residual);
558 state.current = state.sample(self.spring, self.settle);
559 if state.elapsed >= self.settle {
560 state.last_frame = None;
561 }
562 }
563 state.current
564 };
565
566 if self.measuring {
567 self.element.prepaint_as_root(
571 bounds.origin + offset,
572 size(
573 AvailableSpace::Definite(bounds.size.width),
574 AvailableSpace::Definite(bounds.size.height),
575 ),
576 window,
577 cx,
578 );
579 } else {
580 window.with_element_offset(offset, |window| {
581 self.element.prepaint(window, cx);
582 });
583 }
584
585 let state = self.state.borrow();
586 let growing = state.size.is_some_and(|size| size.is_animating());
587 let told_something_new = self.measuring && state.available != self.measured_against;
592 drop(state);
593 if offset != Point::default() || growing || told_something_new {
594 window.request_animation_frame();
595 }
596 }
597
598 fn paint(
599 &mut self,
600 _id: Option<&GlobalElementId>,
601 _inspector_id: Option<&InspectorElementId>,
602 _bounds: Bounds<Pixels>,
603 _request_layout: &mut (),
604 _prepaint: &mut (),
605 window: &mut Window,
606 cx: &mut App,
607 ) {
608 self.element.paint(window, cx);
609 }
610}
611
612#[cfg(test)]
613mod tests {
614 use super::*;
615 use gpui::{point, size};
616 use gpui_kit_theme::Theme;
617
618 fn grab() -> Spring {
619 Spring::preset(&Theme::studio_dark(), SpringPreset::Grab)
620 }
621
622 fn spec() -> MotionSpec {
623 MotionSpec::sprung(grab())
624 }
625
626 struct Frames(Instant);
629
630 impl Frames {
631 fn new() -> Self {
632 Self(Instant::now())
633 }
634
635 fn step(&mut self) -> Instant {
636 self.0 += Duration::from_millis(8);
637 self.0
638 }
639 }
640
641 fn run_to_rest(
643 state: &mut FlipState,
644 natural: Size<Pixels>,
645 frames: &mut Frames,
646 ) -> Size<Pixels> {
647 let mut drawn = natural;
648 for _ in 0..600 {
649 if !state.size.is_some_and(|size| size.is_animating()) {
650 break;
651 }
652 drawn = state.record_size(natural, spec(), frames.step());
653 }
654 drawn
655 }
656
657 #[test]
658 fn the_grab_spring_settles_sooner_than_the_snappy_one() {
659 let snappy = Spring::preset(&Theme::studio_dark(), SpringPreset::Snappy);
660 assert!(grab().settle_time() < snappy.settle_time());
661 }
662
663 #[test]
664 fn a_first_measurement_produces_no_offset() {
665 let mut state = FlipState::default();
666 state.record(point(px(10.0), px(20.0)), Point::default());
667 assert_eq!(state.sample(grab(), grab().settle_time()), Point::default());
668 }
669
670 #[test]
671 fn a_move_inverts_into_the_distance_travelled() {
672 let spring = grab();
673 let settle = spring.settle_time();
674 let mut state = FlipState::default();
675 state.record(point(px(0.0), px(0.0)), Point::default());
676 state.record(point(px(0.0), px(40.0)), Point::default());
677 assert_eq!(state.sample(spring, settle), point(px(0.0), px(-40.0)));
678
679 state.elapsed = settle;
680 assert_eq!(state.sample(spring, settle), Point::default());
681 }
682
683 #[test]
684 fn a_move_mid_slide_continues_from_what_is_on_screen() {
685 let spring = grab();
686 let settle = spring.settle_time();
687 let mut state = FlipState::default();
688 state.record(point(px(0.0), px(0.0)), Point::default());
689 state.record(point(px(0.0), px(40.0)), Point::default());
690
691 state.elapsed = settle / 2;
692 let residual = state.sample(spring, settle);
693 assert!(residual.y > px(-40.0) && residual.y < px(0.0));
694
695 state.record(point(px(0.0), px(60.0)), residual);
696 assert_eq!(
697 state.sample(spring, settle),
698 residual - point(px(0.0), px(20.0))
699 );
700 }
701
702 #[test]
703 fn sub_pixel_drift_does_not_start_a_slide() {
704 let spring = grab();
705 let settle = spring.settle_time();
706 let mut state = FlipState::default();
707 state.record(point(px(0.0), px(0.0)), Point::default());
708 state.record(point(px(0.2), px(0.3)), Point::default());
709 assert_eq!(state.sample(spring, settle), Point::default());
710 }
711
712 #[test]
713 fn a_first_size_is_drawn_at_once() {
714 let mut frames = Frames::new();
715 let mut state = FlipState::default();
716 let first = size(px(100.0), px(40.0));
717 assert_eq!(state.record_size(first, spec(), frames.step()), first);
718 assert!(!state.size.expect("recorded").is_animating());
719 }
720
721 #[test]
722 fn a_size_change_starts_at_the_old_size_and_lands_on_the_new_one() {
723 let mut frames = Frames::new();
724 let mut state = FlipState::default();
725 state.record_size(size(px(100.0), px(40.0)), spec(), frames.step());
726 let grown = size(px(200.0), px(80.0));
727 let drawn = state.record_size(grown, spec(), frames.step());
728 assert_eq!(
729 drawn,
730 size(px(100.0), px(40.0)),
731 "the first frame of a resize is the size it had"
732 );
733 assert_eq!(run_to_rest(&mut state, grown, &mut frames), grown);
734 }
735
736 #[test]
737 fn a_size_change_mid_animation_continues_from_the_size_on_screen() {
738 let mut frames = Frames::new();
739 let mut state = FlipState::default();
740 state.record_size(size(px(100.0), px(40.0)), spec(), frames.step());
741 let wider = size(px(200.0), px(40.0));
742 state.record_size(wider, spec(), frames.step());
743
744 let mut interrupted = size(px(100.0), px(40.0));
745 let mut caught = frames.step();
746 for _ in 0..6 {
747 caught = frames.step();
748 interrupted = state.record_size(wider, spec(), caught);
749 }
750 assert!(
751 interrupted.width > px(100.0) && interrupted.width < px(200.0),
752 "the animation has to be in flight for the claim to mean anything: {interrupted:?}"
753 );
754
755 let widest = size(px(300.0), px(40.0));
758 let drawn = state.record_size(widest, spec(), caught);
759 assert_eq!(
760 drawn, interrupted,
761 "a retarget starts from what is on screen rather than from the old size"
762 );
763 assert_eq!(run_to_rest(&mut state, widest, &mut frames), widest);
764 }
765
766 #[test]
767 fn sub_pixel_size_churn_starts_nothing() {
768 let mut frames = Frames::new();
769 let mut state = FlipState::default();
770 state.record_size(size(px(100.0), px(40.0)), spec(), frames.step());
771 let drawn = state.record_size(size(px(100.3), px(40.2)), spec(), frames.step());
772 assert_eq!(drawn, size(px(100.0), px(40.0)));
773 assert!(!state.size.expect("recorded").is_animating());
774 }
775
776 #[test]
777 fn a_settled_size_is_the_new_size_with_nothing_in_flight() {
778 let mut frames = Frames::new();
779 let mut state = FlipState::default();
780 state.record_size(size(px(100.0), px(40.0)), spec(), frames.step());
781 state.settle_size(size(px(200.0), px(80.0)), spec(), frames.step());
782 let transition = state.size.expect("recorded");
783 assert_eq!(transition.value(), size(px(200.0), px(80.0)));
784 assert!(!transition.is_animating());
785 }
786
787 #[test]
788 fn position_and_size_run_independently() {
789 let mut frames = Frames::new();
790 let spring = grab();
791 let settle = spring.settle_time();
792 let mut state = FlipState::default();
793 state.record(point(px(0.0), px(0.0)), Point::default());
794 state.record_size(size(px(100.0), px(40.0)), spec(), frames.step());
795
796 state.record(point(px(0.0), px(40.0)), Point::default());
797 let taller = size(px(100.0), px(90.0));
798 let drawn = state.record_size(taller, spec(), frames.step());
799 assert_eq!(state.sample(spring, settle), point(px(0.0), px(-40.0)));
800 assert_eq!(drawn, size(px(100.0), px(40.0)));
801 assert_eq!(run_to_rest(&mut state, taller, &mut frames), taller);
802 }
803
804 #[test]
805 fn a_rectangle_older_than_the_handoff_window_is_not_inverted_from() {
806 let start = Instant::now();
807 let mut state = FlipState::default();
808 state.forget_if_stale(start);
809 state.record(point(px(0.0), px(0.0)), Point::default());
810 state.record_size(size(px(100.0), px(40.0)), spec(), start);
811
812 state.forget_if_stale(start + MEMORY / 2);
813 state.record(point(px(0.0), px(300.0)), Point::default());
814 assert_ne!(
815 state.sample(grab(), grab().settle_time()),
816 Point::default(),
817 "a gap inside the window is a handoff and travels"
818 );
819
820 state.forget_if_stale(start + MEMORY / 2 + MEMORY * 2);
821 assert_eq!(state.origin, None, "a stale rectangle is forgotten");
822 assert_eq!(state.size, None);
823 state.record(point(px(0.0), px(600.0)), Point::default());
824 assert_eq!(
825 state.sample(grab(), grab().settle_time()),
826 Point::default(),
827 "an element with no recent rectangle is simply already in place"
828 );
829 }
830
831 #[test]
832 fn two_elements_sharing_an_id_in_one_frame_contest_it() {
833 let mut state = FlipState::default();
834 assert!(
835 !state.claim(Some(7)),
836 "one element per frame is no collision"
837 );
838 assert!(
839 state.claim(Some(7)),
840 "the second element in a frame collides"
841 );
842 assert!(
843 state.claim(Some(8)),
844 "the frame after a collision is still refused"
845 );
846 assert!(
847 !state.claim(Some(9)),
848 "a single renderer resumes once it has a rectangle of its own"
849 );
850 }
851
852 #[test]
853 fn a_host_without_a_frame_counter_never_reports_a_collision() {
854 let mut state = FlipState::default();
855 assert!(!state.claim(None));
856 assert!(!state.claim(None));
857 }
858}