Skip to main content

gpui_motion_kit/
measured_size.rs

1//! Intrinsic content measurement with explicit constraints and real layout interpolation.
2use crate::core::{LayoutPositionState, Transition};
3
4use gpui_kit::{
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 Kit-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_kit::{div, prelude::*, px};
42    /// use gpui_motion_kit::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
101fn content_mask(bounds: Bounds<Pixels>) -> ContentMask<Pixels> {
102    ContentMask { bounds }
103}
104
105impl<E: IntoElement + 'static> IntoElement for AutoSize<E> {
106    type Element = Self;
107
108    fn into_element(self) -> Self {
109        self
110    }
111}
112
113impl<E: IntoElement + 'static> Element for AutoSize<E> {
114    type RequestLayoutState = AnyElement;
115    type PrepaintState = Bounds<Pixels>;
116
117    fn id(&self) -> Option<ElementId> {
118        Some(self.id.clone())
119    }
120
121    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
122        None
123    }
124
125    fn request_layout(
126        &mut self,
127        id: Option<&GlobalElementId>,
128        _: Option<&InspectorElementId>,
129        window: &mut Window,
130        cx: &mut App,
131    ) -> (LayoutId, AnyElement) {
132        let mut child = self.child.take().expect("one layout").into_any_element();
133
134        let available = |value: Option<f32>| {
135            value.map_or(AvailableSpace::MaxContent, |v| {
136                AvailableSpace::Definite(px(v))
137            })
138        };
139
140        let measured = child.layout_as_root(
141            size(available(self.width), available(self.height)),
142            window,
143            cx,
144        );
145
146        let target = [f64::from(measured.width), f64::from(measured.height)];
147        let now = cx.background_executor().now();
148
149        let transitions = match self.axis {
150            SizeAxis::Width => [self.transition, Transition::Instant],
151            SizeAxis::Height => [Transition::Instant, self.transition],
152            SizeAxis::Both => [self.transition; 2],
153        };
154
155        let frame = window.with_element_state(
156            id.expect("size ID"),
157            |state: Option<SizeHistory>, window| {
158                let mut state = state.unwrap_or_else(|| {
159                    let mut state = LayoutPositionState::default();
160
161                    if let Some(initial) = self.initial {
162                        state.sample(
163                            initial.map(f64::from),
164                            self.transition,
165                            std::time::Duration::ZERO,
166                            cx.reduce_motion(),
167                        );
168                    }
169
170                    SizeHistory { state, epoch: now }
171                });
172
173                let frame = state.state.sample_axes(
174                    target,
175                    transitions,
176                    now.saturating_duration_since(state.epoch),
177                    cx.reduce_motion(),
178                );
179
180                if frame.is_animating() {
181                    window.request_animation_frame();
182                }
183
184                (frame, state)
185            },
186        );
187
188        let [mut width, mut height] = frame
189            .position()
190            .map(|v| px(v.clamp(0., f64::from(f32::MAX)) as f32));
191
192        if matches!(self.axis, SizeAxis::Height) {
193            width = measured.width;
194        }
195
196        if matches!(self.axis, SizeAxis::Width) {
197            height = measured.height;
198        }
199
200        let style = Style {
201            size: size(width.into(), height.into()),
202            flex_shrink: 0.,
203            ..Default::default()
204        };
205
206        (window.request_layout(style, [], cx), child)
207    }
208
209    fn prepaint(
210        &mut self,
211        _: Option<&GlobalElementId>,
212        _: Option<&InspectorElementId>,
213        bounds: Bounds<Pixels>,
214        child: &mut AnyElement,
215        window: &mut Window,
216        cx: &mut App,
217    ) -> Bounds<Pixels> {
218        window.with_content_mask(Some(content_mask(bounds)), |w| {
219            child.prepaint_at(bounds.origin, w, cx);
220        });
221
222        bounds
223    }
224
225    fn paint(
226        &mut self,
227        _: Option<&GlobalElementId>,
228        _: Option<&InspectorElementId>,
229        _: Bounds<Pixels>,
230        child: &mut AnyElement,
231        bounds: &mut Bounds<Pixels>,
232        window: &mut Window,
233        cx: &mut App,
234    ) {
235        window.with_content_mask(Some(content_mask(*bounds)), |w| child.paint(w, cx));
236    }
237}