GridLayout

GridLayout arranges items on a grid of columns and rows and lets the user move and resize them with the pointer or the keyboard. Items push one another out of the way, float toward the top as space opens up, and flow around locked items. Use it for dashboards, widget boards, and other surfaces where the user owns the arrangement.

The behavior and layout model are the Grid Layout primitive from gpui-base; this module re-exports it and adds grid_layout, which dresses it in the theme.

#Import

use gpui_kit::component::grid_layout::{
    GridArea, GridItem, GridLayoutEvent, GridLayoutState, grid_drag_handle, grid_layout,
};

#Usage

Create one GridLayoutState with the grid’s rules and its areas, then pair each GridItem with the area of the same id.

let state = cx.new(|cx| {
    GridLayoutState::new(cx).default_areas([
        GridArea::new("revenue").col_span(8).row_span(4),
        GridArea::new("orders").col(8).col_span(4).row_span(2),
        GridArea::new("visitors").col(8).row(2).col_span(4).row_span(2),
    ])
});

grid_layout("dashboard", &state, cx)
    .item(GridItem::new("revenue").child(revenue_card))
    .item(GridItem::new("orders").child(orders_card))
    .item(GridItem::new("visitors").child(visitors_card))

A GridArea places an item in zero-based grid cells: col is the leading column, row the top row, and col_span and row_span the number of columns and rows it covers. An item without an area is not drawn.

The grid is as wide as its container and as tall as its rows. Place it inside a scrollable region when the rows can outgrow the window.

#Grid rules

Set the rules before the areas, because default_areas settles the areas under the rules set so far.

use gpui_kit::Axis;

let state = cx.new(|cx| {
    GridLayoutState::new(cx)
        .cols(6)
        .compaction(Some(Axis::Vertical))
        .default_areas(areas)
});
RuleDefaultDescription
cols12The number of equal columns the width is divided into
compactionSome(Axis::Vertical)The direction items float toward, or None to leave them where they are put
prevent_collisionfalseRefuse a move or resize that would overlap another item instead of pushing it
draggabletrueWhether items can be moved
resizabletrueWhether items can be resized

Each rule has a set_* counterpart for changing it later, such as set_compaction, or set_draggable to switch a dashboard between viewing and editing.

The row height and the gaps belong to the element, as on a CSS grid:

grid_layout("dashboard", &state, cx)
    .row_height(rems(3.))
    .gap_4()

grid_layout uses the theme’s medium spacing; .gap(..), .gap_x(..) or .gap_y(..) override it. Rows default to 2.5rem, so they follow the application’s zoom.

#Item limits

Limit how far an item can be resized, or lock it in place. A locked item is neither moved, resized, nor pushed; other items flow around it.

GridArea::new("revenue").col_span(6).row_span(4).min_span(3, 2).max_span(8, 6);

GridArea::new("notes").col(8).col_span(4).row_span(3).locked(true);

#Saving the layout

GridArea implements Serialize and Deserialize. Subscribe to GridLayoutEvent::Change to persist the arrangement whenever the user changes it, and restore it with default_areas or set_areas.

cx.subscribe(&state, |this, _, event: &GridLayoutEvent, cx| {
    if let GridLayoutEvent::Change(areas) = event {
        this.save_dashboard_layout(areas, cx);
    }
})
.detach();

Change is emitted when a drag or resize ends, when a keyboard command changes the layout, and after move_item or resize_item. A drag reports one change on release, not one per pointer movement. set_areas does not emit Change, because the application already holds the layout it set. GridLayoutEvent is #[non_exhaustive], so match it with if let or a wildcard arm.

#Following the selection

GridLayoutEvent::Select reports the item the user selects — by pressing it, or with Tab or Shift-Tab — once per change. A settings panel can follow it without watching every redraw:

cx.subscribe(&state, |this, _, event: &GridLayoutEvent, cx| {
    if let GridLayoutEvent::Select(id) = event {
        this.inspected_tile = id.clone();
        cx.notify();
    }
})
.detach();

set_selected_item changes the selection without emitting it.

#Narrow windows

reflow(cols) shows the items over fewer columns without changing the stored layout — a one-column stack below a breakpoint, say:

grid_layout("dashboard", &state, cx)
    .when(window.viewport_size().width < px(768.), |grid| grid.reflow(1))

While reflowed the grid is read-only, so nothing is saved at the narrower width; selection still works.

#Drag handle

By default the whole item moves it. When an item’s content has its own pointer interaction, such as a table or a chart, move the item only by a handle, usually its header:

GridItem::new("orders")
    .drag_handle_only(true)
    .child(
        v_flex()
            .size_full()
            .child(grid_drag_handle(&state, "orders").p_2().child("Orders"))
            .child(orders_table),
    )

#Programmatic changes

state.update(cx, |state, cx| {
    state.move_item("orders", 0, 4, cx);
    state.resize_item("revenue", 6, 3, cx);
});

Both apply the same pushing and compaction as the pointer and return whether the layout changed.

#Appearance

grid_layout adds a dashed placeholder where the dragged item will land, a resize grip in each resizable item’s corner, a raised dragged item, and the theme’s move motion, which follows the application’s reduced-motion preference. While the grid is editable, the selected item carries a ring in the primary color, so an editor shows which item its settings apply to; while the keyboard drives the grid the focus ring takes its place, never both. A read-only grid marks no selection. Each can be replaced on the returned builder with placeholder, resize_handle, dragging_item, selected_item, focused_item or transition.

A GridItem paints nothing of its own: its content supplies the surface, usually a card or a GroupBox. Give the item the same corner radius as its content so that the raised shadow and the rings follow the same corners.

GridItem::new("orders")
    .rounded(cx.theme().radius_lg)
    .child(orders_card)

#Keyboard

Clicking an item selects it and focuses the grid. With the grid focused:

KeyAction
Tab / Shift-TabSelect the next or previous item in reading order; past either end, focus leaves the grid
Arrow keysMove the selected item one cell
Shift + arrow keysResize the selected item by one cell
EscapeCancel the drag in progress

When a command cannot apply, such as moving past the leading edge, the key passes to the surrounding view.

#Accessibility

The grid is a labelled group, “Grid layout” by default; use accessibility_label on the grid to name what it contains. Each item is a group that reports whether it is selected. Name items with GridItem::accessibility_label, usually their visible title.

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.