Skip to main content

gpui_ce_motion/
measured_size.rs

1//! Intrinsic content measurement with explicit constraints and real layout interpolation.
2use crate::{LayoutPositionState, Transition};
3
4use gpui::{
5    AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, GlobalElementId,
6    InspectorElementId, IntoElement, LayoutId, Pixels, Style, Window, px, size,
7};
8
9use std::time::Instant;
10
11/// Which measured dimensions interpolate. Other dimensions follow content immediately.
12#[derive(Clone, Copy, Debug, Default)]
13pub enum SizeAxis {
14    /// Animate width only.
15    Width,
16    /// Animate height only, typically with an explicit width constraint.
17    #[default]
18    Height,
19    /// Animate both measured dimensions.
20    Both,
21}
22
23/// Measures content as a separate layout root, reserves its animated size, and
24/// clips painting/input to that reservation. This adds a layout node. Constraints
25/// are explicit because the content is measured before parent layout is resolved.
26/// This is real layout sizing, not GPU projection. Retain the same CE-scoped ID.
27pub struct AutoSize<E> {
28    id: ElementId,
29    child: Option<E>,
30    width: Option<f32>,
31    height: Option<f32>,
32    axis: SizeAxis,
33    transition: Transition,
34    initial: Option<[f32; 2]>,
35}
36
37impl<E> AutoSize<E> {
38    /// Creates a height animation. First measurement snaps by default.
39    ///
40    /// ```
41    /// use gpui::{div, prelude::*, px};
42    /// use gpui_ce_motion::AutoSize;
43    /// let accordion = AutoSize::new("accordion", div().w(px(280.)).child("Measured content")).width(280.);
44    /// ```
45    pub fn new(id: impl Into<ElementId>, child: E) -> Self {
46        Self {
47            id: id.into(),
48            child: Some(child),
49            width: None,
50            height: None,
51            axis: SizeAxis::Height,
52            transition: Transition::default(),
53            initial: None,
54        }
55    }
56
57    /// Chooses interpolated dimensions.
58    pub fn axis(mut self, axis: SizeAxis) -> Self {
59        self.axis = axis;
60
61        self
62    }
63
64    /// Available width for intrinsic measurement. Does not force the child's width.
65    pub fn width(mut self, width: f32) -> Self {
66        assert!(width.is_finite() && width >= 0.);
67        self.width = Some(width);
68
69        self
70    }
71
72    /// Available height for intrinsic measurement.
73    pub fn height(mut self, height: f32) -> Self {
74        assert!(height.is_finite() && height >= 0.);
75        self.height = Some(height);
76
77        self
78    }
79
80    /// Sets positional spring/tween/instant timing for measured sizes.
81    pub fn transition(mut self, transition: Transition) -> Self {
82        self.transition = transition;
83
84        self
85    }
86
87    /// Opts into entrance from a finite non-negative size, once per mount.
88    pub fn initial(mut self, value: [f32; 2]) -> Self {
89        assert!(value.iter().all(|v| v.is_finite() && *v >= 0.));
90        self.initial = Some(value);
91
92        self
93    }
94}
95
96struct SizeHistory {
97    state: LayoutPositionState,
98    epoch: Instant,
99}
100
101// The official pin adds fade_out; registry CE 0.2.2 has only bounds.
102#[allow(clippy::needless_update)]
103fn content_mask(bounds: Bounds<Pixels>) -> ContentMask<Pixels> {
104    ContentMask {
105        bounds,
106        ..Default::default()
107    }
108}
109
110impl<E: IntoElement + 'static> IntoElement for AutoSize<E> {
111    type Element = Self;
112
113    fn into_element(self) -> Self {
114        self
115    }
116}
117
118impl<E: IntoElement + 'static> Element for AutoSize<E> {
119    type RequestLayoutState = AnyElement;
120    type PrepaintState = Bounds<Pixels>;
121
122    fn id(&self) -> Option<ElementId> {
123        Some(self.id.clone())
124    }
125
126    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
127        None
128    }
129
130    fn request_layout(
131        &mut self,
132        id: Option<&GlobalElementId>,
133        _: Option<&InspectorElementId>,
134        window: &mut Window,
135        cx: &mut App,
136    ) -> (LayoutId, AnyElement) {
137        let mut child = self.child.take().expect("one layout").into_any_element();
138
139        let available = |value: Option<f32>| {
140            value.map_or(AvailableSpace::MaxContent, |v| {
141                AvailableSpace::Definite(px(v))
142            })
143        };
144
145        let measured = child.layout_as_root(
146            size(available(self.width), available(self.height)),
147            window,
148            cx,
149        );
150
151        let target = [f64::from(measured.width), f64::from(measured.height)];
152        let now = cx.background_executor().now();
153
154        let transitions = match self.axis {
155            SizeAxis::Width => [self.transition, Transition::Instant],
156            SizeAxis::Height => [Transition::Instant, self.transition],
157            SizeAxis::Both => [self.transition; 2],
158        };
159
160        let frame = window.with_element_state(
161            id.expect("size ID"),
162            |state: Option<SizeHistory>, window| {
163                let mut state = state.unwrap_or_else(|| {
164                    let mut state = LayoutPositionState::default();
165
166                    if let Some(initial) = self.initial {
167                        state.sample(
168                            initial.map(f64::from),
169                            self.transition,
170                            std::time::Duration::ZERO,
171                            cx.reduce_motion(),
172                        );
173                    }
174
175                    SizeHistory { state, epoch: now }
176                });
177
178                let frame = state.state.sample_axes(
179                    target,
180                    transitions,
181                    now.saturating_duration_since(state.epoch),
182                    cx.reduce_motion(),
183                );
184
185                if frame.is_animating() {
186                    window.request_animation_frame();
187                }
188
189                (frame, state)
190            },
191        );
192
193        let [mut width, mut height] = frame
194            .position()
195            .map(|v| px(v.clamp(0., f64::from(f32::MAX)) as f32));
196
197        if matches!(self.axis, SizeAxis::Height) {
198            width = measured.width;
199        }
200
201        if matches!(self.axis, SizeAxis::Width) {
202            height = measured.height;
203        }
204
205        let style = Style {
206            size: size(width.into(), height.into()),
207            flex_shrink: 0.,
208            ..Default::default()
209        };
210
211        (window.request_layout(style, [], cx), child)
212    }
213
214    fn prepaint(
215        &mut self,
216        _: Option<&GlobalElementId>,
217        _: Option<&InspectorElementId>,
218        bounds: Bounds<Pixels>,
219        child: &mut AnyElement,
220        window: &mut Window,
221        cx: &mut App,
222    ) -> Bounds<Pixels> {
223        window.with_content_mask(Some(content_mask(bounds)), |w| {
224            child.prepaint_at(bounds.origin, w, cx);
225        });
226
227        bounds
228    }
229
230    fn paint(
231        &mut self,
232        _: Option<&GlobalElementId>,
233        _: Option<&InspectorElementId>,
234        _: Bounds<Pixels>,
235        child: &mut AnyElement,
236        bounds: &mut Bounds<Pixels>,
237        window: &mut Window,
238        cx: &mut App,
239    ) {
240        window.with_content_mask(Some(content_mask(*bounds)), |w| child.paint(w, cx));
241    }
242}