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

  • GridLayoutState owns the layout and the rules it settles under: cols(n), compaction(Option<Axis>), prevent_collision(bool), draggable(bool), resizable(bool), and the starting default_areas(...).
  • GridArea is one item’s place, in zero-based cells: col, row, col_span, row_span, the resize limits min_span / max_span, and locked. It is the record an application persists.
  • GridLayout is the element. It places each GridItem in the area with the same id. Set the row height with row_height(rems(..)) and the gaps with GPUI’s own gap, gap_x and gap_y, as on a CSS grid.
  • GridItem is one item; it implements Styled and ParentElement, and takes .when(..) and .map(..) like any element.
  • grid_drag_handle(&state, id) returns the element an item built with drag_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.

CC BY 4.0 · Documentation prose and original illustrations GPUI Kit has rights to license may be copied or adapted with attribution. License. Credit GPUI Kit, link to this source page and the CC BY 4.0 license, and indicate any changes. Code examples and software source remain under Apache-2.0. Existing Apache permissions remain; third-party material retains its own terms.