Skip to main content

gpui_cn/
glide.rs

1//! Panes that glide to a new slot when a layout the application owns
2//! changes, with the dock's motion.
3//!
4//! A [`Glide`] lays its child out at the slot it is given and paints it
5//! moving from the rect it was last painted at, so a layout computed
6//! elsewhere, such as by a server, moves like a [`DockSkin`] layout. The
7//! child is laid out at its final size once, and only moves and clips.
8//!
9//! [`DockSkin`]: crate::DockSkin
10
11use 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/// Where each [`Glide`] of one layout is, by its key. The view that owns
22/// the layout keeps one and hands it to every glide.
23#[derive(Default)]
24pub struct GlideState {
25    tracks: HashMap<SharedString, Track>,
26}
27
28impl GlideState {
29    /// No glides yet: each pane rests where it is first laid out.
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    /// Forgets the panes whose keys are not in `keys`, so a pane that comes
35    /// back starts at rest.
36    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
63/// Lays a pane out at its slot and paints it gliding there from where it
64/// was, above the panes at rest and clipped to its parent.
65pub struct Glide {
66    key: SharedString,
67    state: Entity<GlideState>,
68    child: Option<AnyElement>,
69}
70
71impl Glide {
72    /// A pane known as `key` across frames, whose motion `state` keeps.
73    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}