Skip to main content

gpui_kit_material/components/
adaptive.rs

1//! Window size classes and a size-aware layout element, shared by the
2//! navigation suite and the canonical layouts.
3//!
4//! Source: neither the Figma kit nor md3.io defines size classes, so the
5//! breakpoints are m3e's (`packages/m3e-motion/src/window-size-class.ts`,
6//! from androidx `WindowSizeClass` at
7//! `19991f2c0858aadfcb442ce02b2c8e4743c96898`). Width: 0 / 600 / 840 / 1200 /
8//! 1600 dp. Height: 0 / 480 / 900 dp. A logical GPUI pixel is a dp here, and
9//! the classes ignore the text scale: they describe the window, not its text.
10use gpui::{prelude::*, *};
11
12/// A width bucket, smallest first.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub enum WindowWidthClass {
15    /// Below 600dp.
16    Compact,
17    /// 600dp to 839dp.
18    Medium,
19    /// 840dp to 1199dp.
20    Expanded,
21    /// 1200dp to 1599dp.
22    Large,
23    /// 1600dp and wider.
24    ExtraLarge,
25}
26
27impl WindowWidthClass {
28    /// The class's lower bound in dp.
29    pub const fn min_width(self) -> f32 {
30        match self {
31            Self::Compact => 0.,
32            Self::Medium => 600.,
33            Self::Expanded => 840.,
34            Self::Large => 1200.,
35            Self::ExtraLarge => 1600.,
36        }
37    }
38
39    /// The class of a width in dp. A non-finite width is compact.
40    pub fn of(width: f32) -> Self {
41        if width >= 1600. {
42            Self::ExtraLarge
43        } else if width >= 1200. {
44            Self::Large
45        } else if width >= 840. {
46            Self::Expanded
47        } else if width >= 600. {
48            Self::Medium
49        } else {
50            Self::Compact
51        }
52    }
53}
54
55/// A height bucket, smallest first.
56#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
57pub enum WindowHeightClass {
58    /// Below 480dp.
59    Compact,
60    /// 480dp to 899dp.
61    Medium,
62    /// 900dp and taller.
63    Expanded,
64}
65
66impl WindowHeightClass {
67    /// The class of a height in dp. A non-finite height is compact.
68    pub fn of(height: f32) -> Self {
69        if height >= 900. {
70            Self::Expanded
71        } else if height >= 480. {
72            Self::Medium
73        } else {
74            Self::Compact
75        }
76    }
77}
78
79/// The size class of a window or container.
80#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
81pub struct WindowSizeClass {
82    /// The width bucket.
83    pub width: WindowWidthClass,
84    /// The height bucket.
85    pub height: WindowHeightClass,
86}
87
88impl WindowSizeClass {
89    /// The class of a `width` x `height` dp area.
90    pub fn of(width: f32, height: f32) -> Self {
91        Self {
92            width: WindowWidthClass::of(width),
93            height: WindowHeightClass::of(height),
94        }
95    }
96
97    /// The class of a measured size.
98    pub fn of_size(size: Size<Pixels>) -> Self {
99        Self::of(f32::from(size.width), f32::from(size.height))
100    }
101
102    /// The class of the window's content area.
103    pub fn of_window(window: &Window) -> Self {
104        Self::of_size(window.viewport_size())
105    }
106}
107
108type Builder = Box<dyn FnOnce(Bounds<Pixels>, &mut Window, &mut App) -> AnyElement>;
109
110/// An element that fills its parent and builds its content once its size is
111/// known, in the same frame.
112///
113/// GPUI has no container queries, and `NavigationBar::adaptive`'s measure and
114/// redraw costs a frame at the wrong layout. Building during prepaint avoids
115/// both. The parent must give it a definite size: its height never comes from
116/// its content.
117pub struct SizedContent {
118    id: ElementId,
119    builder: Option<Builder>,
120}
121
122/// Build content from the bounds this element is given. See [`SizedContent`].
123pub fn sized_content(
124    id: impl Into<ElementId>,
125    builder: impl FnOnce(Bounds<Pixels>, &mut Window, &mut App) -> AnyElement + 'static,
126) -> SizedContent {
127    SizedContent {
128        id: id.into(),
129        builder: Some(Box::new(builder)),
130    }
131}
132
133impl IntoElement for SizedContent {
134    type Element = Self;
135    fn into_element(self) -> Self {
136        self
137    }
138}
139
140impl Element for SizedContent {
141    type RequestLayoutState = ();
142    type PrepaintState = Option<AnyElement>;
143
144    fn id(&self) -> Option<ElementId> {
145        Some(self.id.clone())
146    }
147
148    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
149        None
150    }
151
152    fn request_layout(
153        &mut self,
154        _: Option<&GlobalElementId>,
155        _: Option<&InspectorElementId>,
156        window: &mut Window,
157        cx: &mut App,
158    ) -> (LayoutId, ()) {
159        let mut style = Style::default();
160        style.size.width = relative(1.).into();
161        style.size.height = relative(1.).into();
162        (window.request_layout(style, None, cx), ())
163    }
164
165    fn prepaint(
166        &mut self,
167        _: Option<&GlobalElementId>,
168        _: Option<&InspectorElementId>,
169        bounds: Bounds<Pixels>,
170        _: &mut (),
171        window: &mut Window,
172        cx: &mut App,
173    ) -> Option<AnyElement> {
174        let builder = self.builder.take()?;
175        let mut child = builder(bounds, window, cx);
176        child.prepaint_as_root(bounds.origin, bounds.size.into(), window, cx);
177        Some(child)
178    }
179
180    fn paint(
181        &mut self,
182        _: Option<&GlobalElementId>,
183        _: Option<&InspectorElementId>,
184        _: Bounds<Pixels>,
185        _: &mut (),
186        child: &mut Option<AnyElement>,
187        window: &mut Window,
188        cx: &mut App,
189    ) {
190        if let Some(child) = child {
191            child.paint(window, cx);
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    use core::prelude::v1::test;
200
201    #[test]
202    fn the_breakpoints_are_the_references_lower_bounds() {
203        use WindowWidthClass::*;
204        for (width, class) in [
205            (0., Compact),
206            (599.9, Compact),
207            (600., Medium),
208            (839.9, Medium),
209            (840., Expanded),
210            (1199.9, Expanded),
211            (1200., Large),
212            (1599.9, Large),
213            (1600., ExtraLarge),
214        ] {
215            assert_eq!(WindowWidthClass::of(width), class, "{width}");
216            assert!(class.min_width() <= width);
217        }
218        assert_eq!(WindowWidthClass::of(f32::NAN), Compact);
219        assert_eq!(WindowHeightClass::of(479.), WindowHeightClass::Compact);
220        assert_eq!(WindowHeightClass::of(480.), WindowHeightClass::Medium);
221        assert_eq!(WindowHeightClass::of(900.), WindowHeightClass::Expanded);
222    }
223
224    struct Harness;
225    impl Render for Harness {
226        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
227            div()
228                .size(px(500.))
229                .child(sized_content("sized", |bounds, _, _| {
230                    div()
231                        .debug_selector(|| "built".into())
232                        .w(bounds.size.width / 2.)
233                        .h(bounds.size.height)
234                        .into_any_element()
235                }))
236        }
237    }
238
239    #[gpui::test]
240    fn content_is_built_from_the_given_size_in_the_first_frame(cx: &mut gpui::TestAppContext) {
241        let (_, cx) = cx.add_window_view(|_, _| Harness);
242        cx.update(|window, cx| {
243            window.refresh();
244            window.draw(cx).clear(cx);
245        });
246        let built = cx.debug_bounds("built").expect("built in the first frame");
247        assert_eq!(built.size, size(px(250.), px(500.)));
248    }
249}