From 000e5f29b839f69f34d2e21be9568c8f8dd830ea Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 10 Jul 2026 11:29:27 -0700 Subject: [PATCH 1/3] gpui: Add container_query element An element whose contents are built after its own size is resolved, using the same prepaint two-pass technique as uniform_list (no deferred draws). Also reworks the grid_layout example to demonstrate it by collapsing the Holy Grail layout to a stacked column when narrow. --- crates/gpui/examples/grid_layout.rs | 103 ++++++++-------- crates/gpui/src/elements/container_query.rs | 130 ++++++++++++++++++++ crates/gpui/src/elements/mod.rs | 2 + 3 files changed, 182 insertions(+), 53 deletions(-) create mode 100644 crates/gpui/src/elements/container_query.rs diff --git a/crates/gpui/examples/grid_layout.rs b/crates/gpui/examples/grid_layout.rs index 650a3e37bbc2f0..3831cbd0481ac1 100644 --- a/crates/gpui/examples/grid_layout.rs +++ b/crates/gpui/examples/grid_layout.rs @@ -1,68 +1,65 @@ #![cfg_attr(target_family = "wasm", no_main)] use gpui::{ - App, Bounds, Context, Hsla, Window, WindowBounds, WindowOptions, div, prelude::*, px, rgb, size, + App, Bounds, Context, Hsla, Window, WindowBounds, WindowOptions, container_query, div, + prelude::*, px, rgb, size, }; use gpui_platform::application; // https://en.wikipedia.org/wiki/Holy_grail_(web_design) +// +// Resize the window: the layout is chosen by `container_query` based on the +// measured size of the container, collapsing to a single stacked column when +// it becomes too narrow for the three-column grid. struct HolyGrailExample {} impl Render for HolyGrailExample { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let block = |color: Hsla| { - div() - .size_full() - .bg(color) - .border_1() - .border_dashed() - .rounded_md() - .border_color(gpui::white()) - .items_center() - }; + container_query(|container_size, _window, _cx| { + let block = |color: Hsla| { + div() + .size_full() + .bg(color) + .border_1() + .border_dashed() + .rounded_md() + .border_color(gpui::white()) + .items_center() + }; - div() - .gap_1() - .grid() - .bg(rgb(0x505050)) - .size(px(500.0)) - .shadow_lg() - .border_1() - .size_full() - .grid_cols(5) - .grid_rows(5) - .child( - block(gpui::white()) - .row_span(1) - .col_span_full() - .child("Header"), - ) - .child( - block(gpui::red()) - .col_span(1) - .h_56() - .child("Table of contents"), - ) - .child( - block(gpui::green()) - .col_span(3) - .row_span(3) - .child("Content"), - ) - .child( - block(gpui::blue()) - .col_span(1) - .row_span(3) - .child("AD :(") - .text_color(gpui::white()), - ) - .child( - block(gpui::black()) - .row_span(1) - .col_span_full() - .text_color(gpui::white()) - .child("Footer"), - ) + let header = block(gpui::white()).child(format!("Header — {}", container_size.width)); + let table_of_contents = block(gpui::red()).child("Table of contents"); + let content = block(gpui::green()).child("Content"); + let ad = block(gpui::blue()) + .child("AD :(") + .text_color(gpui::white()); + let footer = block(gpui::black()) + .text_color(gpui::white()) + .child("Footer"); + + let container = div().gap_1().bg(rgb(0x505050)).shadow_lg().size_full(); + + if container_size.width < px(400.) { + container + .flex() + .flex_col() + .child(header.h_12().flex_none()) + .child(table_of_contents.h_20().flex_none()) + .child(content.flex_1()) + .child(ad.h_20().flex_none()) + .child(footer.h_12().flex_none()) + } else { + container + .grid() + .grid_cols(5) + .grid_rows(5) + .child(header.row_span(1).col_span_full()) + .child(table_of_contents.col_span(1).h_56()) + .child(content.col_span(3).row_span(3)) + .child(ad.col_span(1).row_span(3)) + .child(footer.row_span(1).col_span_full()) + } + }) } } diff --git a/crates/gpui/src/elements/container_query.rs b/crates/gpui/src/elements/container_query.rs new file mode 100644 index 00000000000000..1c7f78d6a022a1 --- /dev/null +++ b/crates/gpui/src/elements/container_query.rs @@ -0,0 +1,130 @@ +//! A container query element, in the spirit of CSS container queries. +//! The element's own size is determined solely by its style and the space +//! offered by its parent. Once that size is known (during prepaint), the +//! provided closure is called with the measured size to build the contents, +//! which are then laid out within it. This allows children to be chosen or +//! configured based on the actual size of their container rather than the +//! window or a hardcoded breakpoint. + +use refineable::Refineable as _; + +use crate::{ + AnyElement, App, AvailableSpace, Bounds, Element, ElementId, GlobalElementId, + InspectorElementId, IntoElement, LayoutId, Pixels, Size, Style, StyleRefinement, Styled, + Window, relative, +}; + +/// Construct a container query element with the given render callback. +/// The callback receives the size the element was assigned during layout and +/// returns the contents to display within it. +/// +/// By default the element fills its parent (equivalent to `.size_full()`); +/// use the [`Styled`] methods to size it differently. Because the contents +/// don't exist until after layout, they cannot influence the element's size. +/// +/// # Example +/// +/// ``` +/// # use gpui::{container_query, div, px, IntoElement, ParentElement}; +/// container_query(|size, _window, _cx| { +/// if size.width < px(240.) { +/// div().child("Narrow layout") +/// } else { +/// div().child("Wide layout") +/// } +/// }); +/// ``` +pub fn container_query( + render: impl 'static + FnOnce(Size, &mut Window, &mut App) -> E, +) -> ContainerQuery +where + E: IntoElement, +{ + let mut base_style = StyleRefinement::default(); + base_style.size.width = Some(relative(1.).into()); + base_style.size.height = Some(relative(1.).into()); + + ContainerQuery { + render: Some(Box::new(|size, window, cx| { + render(size, window, cx).into_any_element() + })), + style: base_style, + } +} + +/// A container query element, created with [`container_query`]. +pub struct ContainerQuery { + render: Option, &mut Window, &mut App) -> AnyElement>>, + style: StyleRefinement, +} + +impl Element for ContainerQuery { + type RequestLayoutState = (); + type PrepaintState = Option; + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut style = Style::default(); + style.refine(&self.style); + let layout_id = window.request_layout(style, [], cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Option { + let render = self.render.take()?; + let mut child = render(bounds.size, window, cx); + child.layout_as_root(bounds.size.map(AvailableSpace::Definite), window, cx); + child.prepaint_at(bounds.origin, window, cx); + Some(child) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + if let Some(child) = prepaint { + child.paint(window, cx); + } + } +} + +impl IntoElement for ContainerQuery { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Styled for ContainerQuery { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} diff --git a/crates/gpui/src/elements/mod.rs b/crates/gpui/src/elements/mod.rs index bfbc08b3f49792..8a2a1b70a7bf28 100644 --- a/crates/gpui/src/elements/mod.rs +++ b/crates/gpui/src/elements/mod.rs @@ -1,6 +1,7 @@ mod anchored; mod animation; mod canvas; +mod container_query; mod deferred; mod div; mod image_cache; @@ -14,6 +15,7 @@ mod uniform_list; pub use anchored::*; pub use animation::*; pub use canvas::*; +pub use container_query::*; pub use deferred::*; pub use div::*; pub use image_cache::*; From 51cff5766268f7f9dd88655e49e2aa687a62857d Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 18:42:50 +0000 Subject: [PATCH 2/3] Autofix --- crates/gpui/examples/grid_layout.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/gpui/examples/grid_layout.rs b/crates/gpui/examples/grid_layout.rs index 3831cbd0481ac1..9c6d723eba0de2 100644 --- a/crates/gpui/examples/grid_layout.rs +++ b/crates/gpui/examples/grid_layout.rs @@ -30,9 +30,7 @@ impl Render for HolyGrailExample { let header = block(gpui::white()).child(format!("Header — {}", container_size.width)); let table_of_contents = block(gpui::red()).child("Table of contents"); let content = block(gpui::green()).child("Content"); - let ad = block(gpui::blue()) - .child("AD :(") - .text_color(gpui::white()); + let ad = block(gpui::blue()).child("AD :(").text_color(gpui::white()); let footer = block(gpui::black()) .text_color(gpui::white()) .child("Footer"); From 87c9223a363e651ff278677f6af18c9f6939664e Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 11 Jul 2026 16:00:19 -0700 Subject: [PATCH 3/3] Update container_query.rs --- crates/gpui/src/elements/container_query.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/gpui/src/elements/container_query.rs b/crates/gpui/src/elements/container_query.rs index 1c7f78d6a022a1..0363ce6900835b 100644 --- a/crates/gpui/src/elements/container_query.rs +++ b/crates/gpui/src/elements/container_query.rs @@ -1,10 +1,6 @@ //! A container query element, in the spirit of CSS container queries. //! The element's own size is determined solely by its style and the space -//! offered by its parent. Once that size is known (during prepaint), the -//! provided closure is called with the measured size to build the contents, -//! which are then laid out within it. This allows children to be chosen or -//! configured based on the actual size of their container rather than the -//! window or a hardcoded breakpoint. +//! offered by its parent. use refineable::Refineable as _;