Grid Layout
A grid whose items the user drags and resizes into place, the way dashboards and widget boards are arranged. Items push one another aside, float toward the top as space opens up, and flow around locked items.
Like every gpui-base primitive, Grid Layout supplies behavior and the geometry required to implement it — where each item sits, the drag, the resize, the keyboard model, and the layout arithmetic — without imposing a visual language. The drop placeholder, the resize grip, the dragged and selected item styles, and the motion items settle with all come from the application. GPUI Component’s GridLayout supplies them from the theme.
#Example
The single native Cargo entrypoint selects this primitive from the shared showcase implementation. The same showcase is compiled once for the WASM preview above.
cargo run -p gpui-base-examples -- grid-layout
#Import
use gpui_kit::base::{GridArea, GridItem, GridLayout, GridLayoutEvent, GridLayoutState, grid_drag_handle};
#Anatomy and API
GridLayoutStateowns the layout and the rules it settles under:cols(n),compaction(Option<Axis>),prevent_collision(bool),draggable(bool),resizable(bool), and the startingdefault_areas(...).GridAreais one item’s place, in zero-based cells:col,row,col_span,row_span, the resize limitsmin_span/max_span, andlocked. It is the record an application persists.GridLayoutis the element. It places eachGridItemin the area with the same id. Set the row height withrow_height(rems(..))and the gaps with GPUI’s owngap,gap_xandgap_y, as on a CSS grid.GridItemis one item; it implementsStyledandParentElement, and takes.when(..)and.map(..)like any element.grid_drag_handle(&state, id)returns the element an item built withdrag_handle_only(true)is moved by.
let state = cx.new(|cx| {
GridLayoutState::new(cx).cols(12).default_areas([
GridArea::new("revenue").col_span(8).row_span(4).min_span(4, 2),
GridArea::new("orders").col(8).col_span(4).row_span(2),
GridArea::new("notes").col(8).row(2).col_span(4).row_span(2).locked(true),
])
});
GridLayout::new("dashboard", &state)
.row_height(rems(2.5))
.gap_3()
.item(GridItem::new("revenue").child(revenue_card))
.item(GridItem::new("orders").child(orders_card))
.item(GridItem::new("notes").child(notes_card))
col is a cell index counted from zero, not a one-based CSS grid line as in GPUI’s col_start, so a layout reads the same in code and in its saved form.
#Presentation
Grid Layout paints nothing by default. Give it a look with:
dragging_item(|style| ..)— the style layered over the item under the pointer;selected_item(|style| ..)— the style layered over the selected item, whether the pointer or the keyboard selected it;focused_item(|style| ..)— layered over that while the keyboard drives the grid: the focus ring;placeholder(|window, cx| ..)— what fills the cells the dragged item will land in;resize_handle(|window, cx| ..)— what a resizable item shows in the trailing-bottom corner it is resized by;transition(Transition)— how items ease into their cells. Without one, items move at once.
#Narrow windows
reflow(cols) lays the items out over fewer columns for display without changing the layout the state holds — a one-column stack below a breakpoint, say:
GridLayout::new("dashboard", &state)
.when(window.viewport_size().width < px(768.), |grid| grid.reflow(1))
The areas are derived from the state’s: fitted to the width and compacted upward in the wide layout’s reading order, with locks set aside. While reflowed the grid is read-only — items cannot be dragged, resized or moved from the keyboard — so nothing is saved at the narrower width. Selection still works.
#State and events
Subscribe to GridLayoutEvent::Change to persist what the user arranges. It carries the whole layout and is emitted once per committed change — when a drag or resize is released, after a keyboard command, and after move_item or resize_item — not on every pointer movement. set_areas replaces the layout without emitting it, because the application already holds the layout it set. GridLayoutEvent::Select reports the item the user selects — by pressing it, or with Tab or Shift-Tab — once per change, so a view that follows the selection, such as a settings panel, need not watch every redraw. set_selected_item does not emit it. GridLayoutEvent is #[non_exhaustive], so match it with a wildcard arm.
GridArea implements Serialize and Deserialize. Every field after the place has a default, so a layout saved with only id, col, row, col_span and row_span still loads.
Set the state’s rules before default_areas, because the areas are settled under the rules set so far.
#Keyboard
Clicking an item selects it and focuses the grid. With the grid focused, the arrow keys move the selected item one cell, Shift with the arrow keys resizes it, Tab and Shift-Tab move the selection, and Escape cancels a drag. Past the last or before the first item, Tab is not handled, so focus moves on out of the grid. The keys are bound to the MoveLeft, MoveRight, MoveUp, MoveDown, ShrinkWidth, GrowWidth, ShrinkHeight, GrowHeight, SelectNextItem and SelectPreviousItem actions in the GridLayout key context.
#Complete Rust example
The complete implementation used by the runnable showcase is embedded directly from Rust source:
use gpui_base::{GridItem, GridLayout};
use super::*;
impl BaseShowcase {
pub(in super::super) fn grid_layout(&self) -> impl IntoElement {
let tile = |title: &'static str, value: &'static str| {
div()
.size_full()
.p_2()
.flex()
.flex_col()
.gap_1()
.border_1()
.border_color(super::example_rgb(0x171717))
.bg(super::example_rgb(0xffffff))
.child(
div()
.text_xs()
.text_color(super::example_rgb(0x737373))
.child(title),
)
.child(div().child(value))
};
div().w_96().text_sm().child(
GridLayout::new("example-grid-layout", &self.grid_layout)
.accessibility_label("Dashboard")
.gap_2()
.placeholder(|_, _| {
div()
.size_full()
.border_1()
.border_dashed()
.border_color(super::example_rgb(0x737373))
.into_any_element()
})
.resize_handle(|_, _| {
div()
.absolute()
.right(px(3.))
.bottom(px(3.))
.size(px(6.))
.border_r_2()
.border_b_2()
.border_color(super::example_rgb(0x737373))
.into_any_element()
})
.dragging_item(|style| style.opacity(0.85))
.selected_item(|style| style.border_2().border_color(super::example_rgb(0x2563eb)))
.item(GridItem::new("revenue").child(tile("Revenue", "$48,210")))
.item(GridItem::new("orders").child(tile("Orders", "1,284")))
.item(GridItem::new("visitors").child(tile("Visitors", "23,906")))
.item(GridItem::new("notes").child(tile("Notes (locked)", "Pinned"))),
)
}
}
The command above supplies application initialization, window creation, and shared BaseShowcase state.
#Accessibility
The grid is a labelled group, “Grid layout” by default; name what it contains with GridLayout::accessibility_label. Each item is a group that reports whether it is selected; name it with GridItem::accessibility_label, usually its visible title.
#Notes
Give each item a stable id that does not change when items are reordered. The grid is as wide as its container and as tall as its rows, so put it in a scrollable region when the rows can outgrow the window.