Skip to main content

gpui_core/elements/
container_query.rs

1//! A container query element, in the spirit of CSS container queries.
2//! The element's own size is determined solely by its style and the space
3//! offered by its parent.
4
5use refineable::Refineable as _;
6
7use crate::{
8    AnyElement, App, AvailableSpace, Bounds, Element, ElementId, GlobalElementId,
9    InspectorElementId, IntoElement, LayoutId, Pixels, Size, Style, StyleRefinement, Styled,
10    Window, relative,
11};
12
13/// Construct a container query element with the given render callback.
14/// The callback receives the size the element was assigned during layout and
15/// returns the contents to display within it.
16///
17/// By default the element fills its parent (equivalent to `.size_full()`);
18/// use the [`Styled`] methods to size it differently. Because the contents
19/// don't exist until after layout, they cannot influence the element's size.
20///
21/// # Example
22///
23/// ```
24/// # use gpui::{container_query, div, px, IntoElement, ParentElement};
25/// container_query(|size, _window, _cx| {
26///     if size.width < px(240.) {
27///         div().child("Narrow layout")
28///     } else {
29///         div().child("Wide layout")
30///     }
31/// });
32/// ```
33pub fn container_query<E>(
34    render: impl 'static + FnOnce(Size<Pixels>, &mut Window, &mut App) -> E,
35) -> ContainerQuery
36where
37    E: IntoElement,
38{
39    let mut base_style = StyleRefinement::default();
40    base_style.size.width = Some(relative(1.).into());
41    base_style.size.height = Some(relative(1.).into());
42
43    ContainerQuery {
44        render: Some(Box::new(|size, window, cx| {
45            render(size, window, cx).into_any_element()
46        })),
47        style: base_style,
48    }
49}
50
51/// A container query element, created with [`container_query`].
52pub struct ContainerQuery {
53    render: Option<Box<dyn FnOnce(Size<Pixels>, &mut Window, &mut App) -> AnyElement>>,
54    style: StyleRefinement,
55}
56
57impl Element for ContainerQuery {
58    type RequestLayoutState = ();
59    type PrepaintState = Option<AnyElement>;
60
61    fn id(&self) -> Option<ElementId> {
62        None
63    }
64
65    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
66        None
67    }
68
69    fn request_layout(
70        &mut self,
71        _id: Option<&GlobalElementId>,
72        _inspector_id: Option<&InspectorElementId>,
73        window: &mut Window,
74        cx: &mut App,
75    ) -> (LayoutId, Self::RequestLayoutState) {
76        let mut style = Style::default();
77        style.refine(&self.style);
78        let layout_id = window.request_layout(style, [], cx);
79        (layout_id, ())
80    }
81
82    fn prepaint(
83        &mut self,
84        _id: Option<&GlobalElementId>,
85        _inspector_id: Option<&InspectorElementId>,
86        bounds: Bounds<Pixels>,
87        _request_layout: &mut Self::RequestLayoutState,
88        window: &mut Window,
89        cx: &mut App,
90    ) -> Option<AnyElement> {
91        let render = self.render.take()?;
92        let mut child = render(bounds.size, window, cx);
93        child.layout_as_root(bounds.size.map(AvailableSpace::Definite), window, cx);
94        child.prepaint_at(bounds.origin, window, cx);
95        Some(child)
96    }
97
98    fn paint(
99        &mut self,
100        _id: Option<&GlobalElementId>,
101        _inspector_id: Option<&InspectorElementId>,
102        _bounds: Bounds<Pixels>,
103        _request_layout: &mut Self::RequestLayoutState,
104        prepaint: &mut Self::PrepaintState,
105        window: &mut Window,
106        cx: &mut App,
107    ) {
108        if let Some(child) = prepaint {
109            child.paint(window, cx);
110        }
111    }
112}
113
114impl IntoElement for ContainerQuery {
115    type Element = Self;
116
117    fn into_element(self) -> Self::Element {
118        self
119    }
120}
121
122impl Styled for ContainerQuery {
123    fn style(&mut self) -> &mut StyleRefinement {
124        &mut self.style
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use crate::{
131        Context, IntoElement, ParentElement, Pixels, Render, Size, Styled, TestAppContext, Window,
132        container_query, div, px, size,
133    };
134    use std::cell::Cell;
135    use std::rc::Rc;
136
137    struct ContainerQueryView {
138        last_size: Rc<Cell<Option<Size<Pixels>>>>,
139    }
140
141    impl Render for ContainerQueryView {
142        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
143            let last_size = self.last_size.clone();
144            container_query(move |container_size, _window, _cx| {
145                last_size.set(Some(container_size));
146                if container_size.width < px(400.) {
147                    div().size_full().child("narrow")
148                } else {
149                    div().size_full().child("wide")
150                }
151            })
152        }
153    }
154
155    #[gpui::test]
156    fn container_query_builds_children_from_measured_size(cx: &mut TestAppContext) {
157        let last_size = Rc::new(Cell::new(None));
158        let (_, cx) = cx.add_window_view({
159            let last_size = last_size.clone();
160            move |_, _| ContainerQueryView { last_size }
161        });
162
163        cx.simulate_resize(size(px(640.), px(480.)));
164        let wide = last_size
165            .get()
166            .expect("container_query should run after the wide resize");
167        assert!(
168            wide.width >= px(400.),
169            "wide window should offer a container at least 400px wide, got {}",
170            wide.width
171        );
172
173        cx.simulate_resize(size(px(320.), px(480.)));
174        let narrow = last_size
175            .get()
176            .expect("container_query should run after the narrow resize");
177        assert!(
178            narrow.width < px(400.),
179            "narrow window should offer a container under 400px wide, got {}",
180            narrow.width
181        );
182        assert_ne!(
183            wide, narrow,
184            "resizing the window should rebuild children from a new measured size"
185        );
186    }
187
188    #[gpui::test]
189    fn container_query_honors_explicit_size(cx: &mut TestAppContext) {
190        let last_size = Rc::new(Cell::new(None));
191        struct SizedQuery {
192            last_size: Rc<Cell<Option<Size<Pixels>>>>,
193        }
194
195        impl Render for SizedQuery {
196            fn render(
197                &mut self,
198                _window: &mut Window,
199                _cx: &mut Context<Self>,
200            ) -> impl IntoElement {
201                let last_size = self.last_size.clone();
202                container_query(move |container_size, _window, _cx| {
203                    last_size.set(Some(container_size));
204                    div().size_full()
205                })
206                .w(px(240.))
207                .h(px(80.))
208            }
209        }
210
211        let (_, cx) = cx.add_window_view({
212            let last_size = last_size.clone();
213            move |_, _| SizedQuery { last_size }
214        });
215        cx.simulate_resize(size(px(800.), px(600.)));
216        assert_eq!(
217            last_size.get(),
218            Some(size(px(240.), px(80.))),
219            "container_query should measure from its own style, not only the parent offer"
220        );
221    }
222}