1use std::collections::HashMap;
12
13use gpui_kit::{
14 AnyElement, App, Bounds, ContentMask, Element, ElementId, Entity, GlobalElementId,
15 InspectorElementId, IntoElement, LayoutId, Pixels, SharedString, Window,
16};
17
18use crate::Theme;
19use crate::dock::motion::{MOVING, Track};
20
21#[derive(Default)]
24pub struct GlideState {
25 tracks: HashMap<SharedString, Track>,
26}
27
28impl GlideState {
29 pub fn new() -> Self {
31 Self::default()
32 }
33
34 pub fn retain(&mut self, keys: impl IntoIterator<Item = SharedString>) {
37 let keys: Vec<SharedString> = keys.into_iter().collect();
38 self.tracks.retain(|key, _| keys.contains(key));
39 }
40
41 fn place(
42 &mut self,
43 key: &SharedString,
44 slot: Bounds<Pixels>,
45 cx: &App,
46 ) -> (Bounds<Pixels>, bool) {
47 let now = cx.background_executor().now();
48 let motion = &Theme::global(cx).motion;
49 let (duration, curve) = (motion.layout, motion.layout_curve.clone());
50 let track = self
51 .tracks
52 .entry(key.clone())
53 .or_insert_with(|| Track::still(slot, now));
54 if cx.reduce_motion() {
55 *track = Track::still(slot, now);
56 } else {
57 track.retarget(slot, now, duration, &curve);
58 }
59 track.sample(now, duration, &curve)
60 }
61}
62
63pub struct Glide {
66 key: SharedString,
67 state: Entity<GlideState>,
68 child: Option<AnyElement>,
69}
70
71impl Glide {
72 pub fn new(
74 key: impl Into<SharedString>,
75 state: &Entity<GlideState>,
76 child: impl IntoElement,
77 ) -> Self {
78 Self {
79 key: key.into(),
80 state: state.clone(),
81 child: Some(child.into_any_element()),
82 }
83 }
84}
85
86impl IntoElement for Glide {
87 type Element = Self;
88
89 fn into_element(self) -> Self::Element {
90 self
91 }
92}
93
94impl Element for Glide {
95 type RequestLayoutState = ();
96 type PrepaintState = ();
97
98 fn id(&self) -> Option<ElementId> {
99 None
100 }
101
102 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
103 None
104 }
105
106 fn request_layout(
107 &mut self,
108 _: Option<&GlobalElementId>,
109 _: Option<&InspectorElementId>,
110 window: &mut Window,
111 cx: &mut App,
112 ) -> (LayoutId, ()) {
113 let child = self.child.as_mut().expect("laid out once");
114 (child.request_layout(window, cx), ())
115 }
116
117 fn prepaint(
118 &mut self,
119 _: Option<&GlobalElementId>,
120 _: Option<&InspectorElementId>,
121 bounds: Bounds<Pixels>,
122 _: &mut (),
123 window: &mut Window,
124 cx: &mut App,
125 ) {
126 let key = self.key.clone();
127 let (painted, moving) = self
128 .state
129 .update(cx, |state, cx| state.place(&key, bounds, cx));
130 if !moving {
131 if let Some(child) = self.child.as_mut() {
132 child.prepaint(window, cx);
133 }
134 return;
135 }
136 window.request_animation_frame();
137 let child = self.child.take().expect("prepainted once");
138 let offset = window.element_offset() + (painted.origin - bounds.origin);
139 let mask = painted.intersect(&window.content_mask().bounds);
140 window.defer_draw(child, offset, MOVING, Some(ContentMask { bounds: mask }));
141 }
142
143 fn paint(
144 &mut self,
145 _: Option<&GlobalElementId>,
146 _: Option<&InspectorElementId>,
147 _: Bounds<Pixels>,
148 _: &mut (),
149 _: &mut (),
150 window: &mut Window,
151 cx: &mut App,
152 ) {
153 if let Some(child) = self.child.as_mut() {
154 child.paint(window, cx);
155 }
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use gpui_kit::{TestAppContext, point, px, size};
162
163 use super::*;
164
165 fn rect(x: f32) -> Bounds<Pixels> {
166 Bounds::new(point(px(x), px(0.)), size(px(100.), px(100.)))
167 }
168
169 #[gpui_kit::test]
170 fn a_pane_glides_to_a_new_slot_and_rests_there(cx: &mut TestAppContext) {
171 cx.update(|cx| {
172 gpui_kit::init(cx);
173 crate::init(cx);
174 let mut state = GlideState::new();
175 let key = SharedString::from("pane");
176 assert_eq!(state.place(&key, rect(0.), cx), (rect(0.), false));
177 let (start, moving) = state.place(&key, rect(200.), cx);
178 assert!(moving || cx.reduce_motion());
179 assert_eq!(
180 start.origin.x,
181 if cx.reduce_motion() { px(200.) } else { px(0.) }
182 );
183 });
184 cx.executor()
185 .advance_clock(std::time::Duration::from_millis(500));
186 cx.update(|cx| {
187 let mut state = GlideState::new();
188 let key = SharedString::from("pane");
189 state.place(&key, rect(200.), cx);
190 assert_eq!(state.place(&key, rect(200.), cx), (rect(200.), false));
191 });
192 }
193
194 #[gpui_kit::test]
195 fn a_pane_that_returns_after_retain_starts_at_rest(cx: &mut TestAppContext) {
196 cx.update(|cx| {
197 gpui_kit::init(cx);
198 crate::init(cx);
199 let mut state = GlideState::new();
200 let key = SharedString::from("pane");
201 state.place(&key, rect(0.), cx);
202 state.retain([]);
203 assert_eq!(state.place(&key, rect(300.), cx), (rect(300.), false));
204 });
205 }
206}