Skip to main content

gpui_ce_motion/
layout_native.rs

1//! Stock CE layout measurement and prepaint translation; no renderer modifications.
2use crate::{LayoutPositionState, Transition};
3
4use gpui::{
5    AnyElement, App, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement,
6    LayoutId, Pixels, Window, point, px,
7};
8
9use std::{cell::Cell, rc::Rc, time::Instant};
10
11/// A view-owned coordinate scope for automatic position-only layout animation.
12/// Clone shares the ancestor scope; retain one group across renders and use that
13/// same group for nested animated elements. Independent groups should not nest.
14/// Element history is owned by CE's keyed element state and lasts while mounted.
15#[derive(Clone, Debug, Default)]
16pub struct LayoutGroup {
17    ancestor: Rc<Cell<Option<[f64; 2]>>>,
18}
19
20impl LayoutGroup {
21    /// Wraps an element without adding a layout node or changing its styles.
22    /// Keep the ID stable per logical object, unique under the enclosing CE ID.
23    /// Build styles/handlers on `element` before wrapping it. New mounts snap.
24    ///
25    /// Measurements exclude scrolling/manual prepaint offsets. Nested members use
26    /// positions relative to their closest animated ancestor, preventing double
27    /// animation. Sizes change immediately. Normal ancestor clipping still applies.
28    ///
29    /// ```
30    /// use gpui::{div, prelude::*, px};
31    /// use gpui_ce_motion::LayoutGroup;
32    /// // A persistent view field, initialized once:
33    /// let group = LayoutGroup::default();
34    /// let item = group.position("card-position", div().id("card").w(px(140.0)).child("Card"));
35    /// ```
36    pub fn position<E: IntoElement + 'static>(
37        &self,
38        id: impl Into<ElementId>,
39        element: E,
40    ) -> LayoutPositionElement<E> {
41        LayoutPositionElement {
42            id: id.into(),
43            element: Some(element),
44            group: self.clone(),
45            transition: Transition::default(),
46            enabled: true,
47        }
48    }
49}
50
51/// Transparent native wrapper produced by [`LayoutGroup::position`].
52/// Its child's paint, hitboxes and accessibility bounds receive the same prepaint
53/// translation. No scale/rotation, size projection, or cross-parent shared IDs.
54pub struct LayoutPositionElement<E> {
55    id: ElementId,
56    element: Option<E>,
57    group: LayoutGroup,
58    transition: Transition,
59    enabled: bool,
60}
61
62impl<E> LayoutPositionElement<E> {
63    /// Chooses the positional spring/tween/instant transition.
64    pub fn transition(mut self, transition: Transition) -> Self {
65        self.transition = transition;
66
67        self
68    }
69
70    /// Disables interpolation without changing IDs or coordinate scopes. Disabled
71    /// elements follow layout immediately and do not schedule animation frames.
72    pub fn enabled(mut self, enabled: bool) -> Self {
73        self.enabled = enabled;
74
75        self
76    }
77}
78
79struct RetainedPosition {
80    state: LayoutPositionState,
81    epoch: Instant,
82}
83
84struct AncestorScope {
85    ancestor: Rc<Cell<Option<[f64; 2]>>>,
86    previous: Option<[f64; 2]>,
87}
88
89impl Drop for AncestorScope {
90    fn drop(&mut self) {
91        self.ancestor.set(self.previous);
92    }
93}
94
95impl<E: IntoElement + 'static> IntoElement for LayoutPositionElement<E> {
96    type Element = Self;
97
98    fn into_element(self) -> Self::Element {
99        self
100    }
101}
102
103impl<E: IntoElement + 'static> Element for LayoutPositionElement<E> {
104    type RequestLayoutState = AnyElement;
105    type PrepaintState = ();
106
107    fn id(&self) -> Option<ElementId> {
108        Some(self.id.clone())
109    }
110
111    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
112        None
113    }
114
115    fn request_layout(
116        &mut self,
117        _: Option<&GlobalElementId>,
118        _: Option<&InspectorElementId>,
119        window: &mut Window,
120        cx: &mut App,
121    ) -> (LayoutId, AnyElement) {
122        let mut child = self
123            .element
124            .take()
125            .expect("layout requested once")
126            .into_any_element();
127
128        let layout = child.request_layout(window, cx);
129
130        (layout, child)
131    }
132
133    fn prepaint(
134        &mut self,
135        id: Option<&GlobalElementId>,
136        _: Option<&InspectorElementId>,
137        bounds: gpui::Bounds<Pixels>,
138        child: &mut AnyElement,
139        window: &mut Window,
140        cx: &mut App,
141    ) {
142        // CE has already added the snapped ancestor/scroll offset to these bounds.
143        let raw = bounds.origin - window.pixel_snap_point(window.element_offset());
144        let origin = [f64::from(raw.x), f64::from(raw.y)];
145        let parent = self.group.ancestor.get().unwrap_or([0.; 2]);
146        let relative = std::array::from_fn(|i| origin[i] - parent[i]);
147        let now = cx.background_executor().now();
148        let reduced = cx.reduce_motion() || !self.enabled;
149
150        let frame = window.with_element_state(
151            id.expect("position wrapper has an ID"),
152            |state: Option<RetainedPosition>, window| {
153                let mut retained = state.unwrap_or_else(|| RetainedPosition {
154                    state: LayoutPositionState::default(),
155                    epoch: now,
156                });
157
158                let frame = retained.state.sample(
159                    relative,
160                    self.transition,
161                    now.saturating_duration_since(retained.epoch),
162                    reduced,
163                );
164
165                if frame.is_animating() {
166                    window.request_animation_frame();
167                }
168
169                (frame, retained)
170            },
171        );
172
173        let ancestor = self.group.ancestor.clone();
174
175        let _scope = AncestorScope {
176            previous: ancestor.replace(Some(origin)),
177            ancestor,
178        };
179
180        let [x, y] = frame
181            .offset()
182            .map(|v| v.clamp(f64::from(f32::MIN), f64::from(f32::MAX)) as f32);
183
184        window.with_element_offset(point(px(x), px(y)), |window| {
185            child.prepaint(window, cx);
186        });
187    }
188
189    fn paint(
190        &mut self,
191        _: Option<&GlobalElementId>,
192        _: Option<&InspectorElementId>,
193        _: gpui::Bounds<Pixels>,
194        child: &mut AnyElement,
195        _: &mut (),
196        window: &mut Window,
197        cx: &mut App,
198    ) {
199        child.paint(window, cx);
200    }
201}