Skip to main content

gpui_ce_motion/
pop_layout.rs

1//! Managed-container exit layout without subtree transforms.
2use crate::{LayoutPositionState, Transition};
3
4use gpui::{
5    AnyElement, App, AvailableSpace, Bounds, Display, Element, ElementId, GlobalElementId,
6    InspectorElementId, IntoElement, LayoutId, Pixels, Point, Style, Window, point, px, size,
7};
8
9use std::{cell::Cell, rc::Rc, time::Instant};
10
11/// Retain in the view. A container records its origin; exiting items reserve no
12/// flow space but remain at their last measured parent-relative bounds. Use the
13/// same container and retain outgoing content with `PresenceGroup` until exit
14/// finishes. Ancestor clipping still applies. Independent containers must use
15/// separate handles; this is not cross-parent projection.
16#[derive(Clone, Debug, Default)]
17pub struct PopLayout {
18    origin: Rc<Cell<Point<Pixels>>>,
19}
20
21impl PopLayout {
22    /// Wraps the container without adding a layout node.
23    ///
24    /// ```
25    /// use gpui::{div, prelude::*};
26    /// use gpui_ce_motion::PopLayout;
27    /// let pop = PopLayout::default(); // persistent view field
28    /// let row = div().flex().child(pop.item("item", true, div().child("Retained content")));
29    /// let container = pop.container("container", row);
30    /// ```
31    ///
32    /// Exiting children are
33    /// measured as separate roots and painted in its coordinate space, so borders,
34    /// padding and gaps do not introduce absolute-positioning corrections.
35    pub fn container<E: IntoElement + 'static>(
36        &self,
37        id: impl Into<ElementId>,
38        child: E,
39    ) -> PopElement<E> {
40        PopElement {
41            id: id.into(),
42            child: Some(child),
43            scope: self.clone(),
44            present: None,
45            transition: Transition::default(),
46        }
47    }
48
49    /// Wraps a retained item. False removes it from flow immediately. Its child
50    /// still owns exit opacity/motion, input suppression and focus restoration.
51    /// Owns positional reflow as well as exit positioning. Do not also wrap the
52    /// item in `LayoutGroup::position`. For x/y exit motion, animate inner content
53    /// inside a fixed-size child wrapper; the outer item owns its layout position.
54    pub fn item<E: IntoElement + 'static>(
55        &self,
56        id: impl Into<ElementId>,
57        present: bool,
58        child: E,
59    ) -> PopElement<E> {
60        PopElement {
61            id: id.into(),
62            child: Some(child),
63            scope: self.clone(),
64            present: Some(present),
65            transition: Transition::default(),
66        }
67    }
68}
69
70/// Native wrapper constructed by `PopLayout`.
71pub struct PopElement<E> {
72    id: ElementId,
73    child: Option<E>,
74    scope: PopLayout,
75    present: Option<bool>,
76    transition: Transition,
77}
78
79impl<E> PopElement<E> {
80    /// Chooses the item's positional reflow transition. Containers do not animate
81    /// themselves. Sizes change immediately; popped positions freeze until reentry.
82    pub fn transition(mut self, transition: Transition) -> Self {
83        self.transition = transition;
84
85        self
86    }
87}
88
89struct History {
90    bounds: Option<Bounds<Pixels>>,
91    position: LayoutPositionState,
92    epoch: Instant,
93}
94
95struct OriginScope {
96    origin: Rc<Cell<Point<Pixels>>>,
97    previous: Point<Pixels>,
98}
99
100impl Drop for OriginScope {
101    fn drop(&mut self) {
102        self.origin.set(self.previous);
103    }
104}
105
106/// Per-frame layout data for a popped element. Native implementations should use
107/// `PopLayout::item`; this type only appears through the `Element` trait.
108pub struct PopRequest {
109    child: AnyElement,
110    bounds: Option<Bounds<Pixels>>,
111}
112
113impl<E: IntoElement + 'static> IntoElement for PopElement<E> {
114    type Element = Self;
115
116    fn into_element(self) -> Self {
117        self
118    }
119}
120
121impl<E: IntoElement + 'static> Element for PopElement<E> {
122    type RequestLayoutState = PopRequest;
123    type PrepaintState = ();
124
125    fn id(&self) -> Option<ElementId> {
126        Some(self.id.clone())
127    }
128
129    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
130        None
131    }
132
133    fn request_layout(
134        &mut self,
135        id: Option<&GlobalElementId>,
136        _: Option<&InspectorElementId>,
137        w: &mut Window,
138        cx: &mut App,
139    ) -> (LayoutId, PopRequest) {
140        let mut child = self.child.take().expect("one layout").into_any_element();
141        let now = cx.background_executor().now();
142
143        let old_bounds =
144            w.with_element_state(id.expect("pop ID"), |history: Option<History>, _| {
145                let h = history.unwrap_or_else(|| History {
146                    bounds: None,
147                    position: LayoutPositionState::default(),
148                    epoch: now,
149                });
150
151                (h.bounds, h)
152            });
153
154        let layout = if self.present == Some(false) {
155            // A detached child avoids flex gaps and containing-block border drift.
156            let style = Style {
157                display: Display::None,
158                ..Default::default()
159            };
160
161            w.request_layout(style, [], cx)
162        } else {
163            child.request_layout(w, cx)
164        };
165
166        (
167            layout,
168            PopRequest {
169                child,
170                bounds: old_bounds,
171            },
172        )
173    }
174
175    fn prepaint(
176        &mut self,
177        id: Option<&GlobalElementId>,
178        _: Option<&InspectorElementId>,
179        bounds: Bounds<Pixels>,
180        request: &mut PopRequest,
181        w: &mut Window,
182        cx: &mut App,
183    ) {
184        if self.present.is_none() {
185            let _scope = OriginScope {
186                previous: self.scope.origin.replace(bounds.origin),
187                origin: self.scope.origin.clone(),
188            };
189
190            request.child.prepaint(w, cx);
191
192            return;
193        }
194
195        let now = cx.background_executor().now();
196
197        if self.present == Some(false) {
198            if let Some(old) = request.bounds {
199                // The detached placeholder has no meaningful origin. Freeze the
200                // retained presentation instead, so reentry never follows it.
201                w.with_element_state(id.expect("pop ID"), |history: Option<History>, _| {
202                    let mut h = history.expect("history initialized during layout");
203
204                    h.position.sample(
205                        [f64::from(old.origin.x), f64::from(old.origin.y)],
206                        Transition::Instant,
207                        now.saturating_duration_since(h.epoch),
208                        true,
209                    );
210
211                    ((), h)
212                });
213                request.child.prepaint_as_root(
214                    self.scope.origin.get() + old.origin,
215                    size(
216                        AvailableSpace::Definite(old.size.width),
217                        AvailableSpace::Definite(old.size.height),
218                    ),
219                    w,
220                    cx,
221                );
222            }
223        } else {
224            let origin = bounds.origin - self.scope.origin.get();
225
226            let frame = w.with_element_state(id.expect("pop ID"), |history: Option<History>, w| {
227                let mut h = history.expect("history initialized during layout");
228
229                let frame = h.position.sample(
230                    [f64::from(origin.x), f64::from(origin.y)],
231                    self.transition,
232                    now.saturating_duration_since(h.epoch),
233                    cx.reduce_motion(),
234                );
235
236                let [x, y] = frame
237                    .position()
238                    .map(|v| v.clamp(f32::MIN as f64, f32::MAX as f64) as f32);
239
240                h.bounds = Some(Bounds {
241                    origin: point(px(x), px(y)),
242                    size: bounds.size,
243                });
244
245                if frame.is_animating() {
246                    w.request_animation_frame();
247                }
248
249                (frame, h)
250            });
251
252            let [x, y] = frame
253                .offset()
254                .map(|v| v.clamp(f32::MIN as f64, f32::MAX as f64) as f32);
255
256            w.with_element_offset(point(px(x), px(y)), |w| request.child.prepaint(w, cx));
257        }
258    }
259
260    fn paint(
261        &mut self,
262        _: Option<&GlobalElementId>,
263        _: Option<&InspectorElementId>,
264        _: Bounds<Pixels>,
265        request: &mut PopRequest,
266        _: &mut (),
267        w: &mut Window,
268        cx: &mut App,
269    ) {
270        if self.present != Some(false) || request.bounds.is_some() {
271            request.child.paint(w, cx);
272        }
273    }
274}