Skip to main content

Module dock

Module dock 

Source
Expand description

Dockable layout: splits, tab groups, and tiles canvases that a host can rearrange, persist, and restore — with no appearance of its own.

§The tree is the source of truth

Each region — the center, plus an optional left, right, and bottom dock — is one PaneTree. A tree is pure data. Containers are addressed by NodeId, panels by PanelId, and no GPUI entity handle is stored anywhere in it, which is what lets the layout algebra be exercised without an App and lets a whole layout be compared, normalized, and serialized as an ordinary value. A container is a Split, a Tabs, or a Tiles; there is no leaf variant, so a panel can only ever live inside a Tabs or a Tiles.

Both ids are stable, and the rest of the design leans on it:

  • A NodeId survives every edit and every normalization rule. A container still present after a drag carries the same id it had before, which is what keeps a reconcile from tearing down entities the drag never touched.
  • A PanelId is the panel entity’s EntityId, so it identifies the panel for as long as the entity lives, across any number of moves between groups and regions.

Every mutation goes through the tree — PaneTree::insert_panel, remove_panel, move_panel, split, set_active, and the rest — and each normalizes before returning, so the tree is self-consistent the instant an edit returns. Every edit reports what it did as an EditResult.

§The area reconciles the tree into entities

DockArea owns the trees and a cache of container entities keyed by NodeId (TabGroup for a Tabs node, TilesState for a Tiles one), plus the panel handles keyed by PanelId. After any edit that reports a change it walks the tree, creates entities for ids the cache does not have, drops entries for ids that are gone — telling those panels Panel::on_removed — pushes sizes and active indices into the survivors, and emits DockEvent::LayoutChanged. Nothing else turns a tree edit into live entities.

Because node ids are stable, a steady-state pass creates and drops nothing.

A layout is described the same entity-free way: DockLayout builds a tree, and DockArea::set_center / DockArea::set_dock install it. DockArea::dump and DockArea::load round-trip a whole area through DockAreaState, rebuilding panels through the PanelRegistry.

§The renderer seam

Base supplies behavior; the host supplies appearance. Nothing in this module paints a color, a border, or a size. Three traits carry the appearance in:

  • DockAreaRenderer — the area frame, each split’s frame and the divider between its slots, one dock’s chrome, and the stand-in for a panel this build cannot construct.
  • TabGroupRenderer — the tab bar, how the displayed panel is placed, and the drop indicator.
  • TilesRenderer — a tiles canvas, its tile frames, and their drag bars.

A renderer never sees a drag event or a mouse position. Base attaches the drag sources, drop hit-testing, focus, and keyboard handling to the very elements the renderer returns, and hands it resolved state through DockContext, TabGroupContext, and TileContext — each of which also carries the callbacks (toggle, select_tab, close, resize_to) that the renderer invokes rather than reimplementing.

An area built without a renderer still docks, drags, resizes and persists. It simply draws nothing but the panels themselves.

Panel splits at the same seam: this trait covers behavior, and a presentation layer — gpui_component::dock::Panel — extends it with titles, toolbars, and menus. A panel type implements both.

Every hook is optional in the same way: a renderer that declines one gets base’s own minimum for it. DockAreaRenderer::render_split_handle is the clearest case — return None and the divider falls back to a one-pixel line colored from Theme::resizable, so a skin with no opinion about dividers implements nothing, while one that has an opinion replaces the paint without touching the hit area, the cursor, or the drag.

§Why the layout is data

The usual way to build a dock is to make each container a live view that holds its children, so the widget tree is the layout. That is what this module replaced, and the three costs it carries are the reason:

  • Emptiness has to propagate. When the last panel leaves a tab group, the group must remove itself from its parent, which may empty the parent in turn. With containers as views this is mutual recursion between two types, reaching upward through parent handles — and those handles have to be installed after construction, which in GPUI means a deferred pass. There is a window in which the tree disagrees with itself.
  • Structure and identity are the same thing. Rearranging the widget tree means creating and dropping views, so a drag can reset the state of containers it never touched.
  • Nothing is testable without a window. Asserting that a split collapses correctly requires an App, an entity, and a frame.

Here the tree is a value and the entities are its projection. Collapse is PaneTree::normalize: one post-order pass repeated to a fixpoint, no parent pointers, no deferred work, and idempotent by construction. Identity is a NodeId that survives every edit and every normalization rule, so reconciliation is a diff rather than a rebuild. And the whole layout algebra runs as plain #[test].

What this buys, stated as properties rather than adjectives: a layout can be compared, cloned and serialized as an ordinary value; a steady-state reconcile creates and drops nothing, so a drag leaves untouched panels untouched; and normalize(normalize(t)) == normalize(t) holds for every tree, which is what makes the persisted format canonical.

§Where this sits among docking libraries

Editors tend to build the layout engine into the application — Zed’s PaneGroup and VS Code’s workbench are not reusable outside their hosts. Standalone libraries split the engine from the view to varying degrees: golden-layout owns its DOM outright, FlexLayout keeps a JSON model beside a React renderer, and dockview goes furthest, running a framework-agnostic engine behind thin adapters. All of them still paint their own chrome and expose CSS as the way to change it.

This module takes the same separation one step further: the engine paints nothing at all. A renderer returns elements and base attaches the drag sources, drop hit-testing, focus and keyboard handling to the elements it got back, so appearance is not a set of overrides on top of a default look — there is no default look. crates/component/src/dock and crates/base/examples/showcase/components/dock.rs are two unrelated appearances over one behavior.

The naming follows the same neighborhood where it can. A tab group here is what Zed calls a Pane and dockview calls a group; a Dock is Zed’s dock; a Panel is the dockable content, as in dockview — note that VS Code uses “panel” for the bottom region instead, and rc-dock uses it for the tab container.

§A minimal area

use std::rc::Rc;
use gpui::{px, Context, Window};
use gpui_base::dock::{DockArea, DockLayout, DockPlacement};

let area = cx.new(|cx| {
    DockArea::new("workspace", Some(1), window, cx).with_renderer(Rc::new(MySkin))
});

area.update(cx, |area, cx| {
    area.set_center(
        DockLayout::h_split()
            .child(DockLayout::tabs().panel(files.clone()), Some(px(240.)))
            .child(DockLayout::tabs().panel(editor.clone()), None),
        window,
        cx,
    );
    area.set_dock(
        DockPlacement::Bottom,
        DockLayout::tabs().panel(terminal.clone()),
        window,
        cx,
    );
});

crates/base/examples/showcase/components/dock.rs is that program in full, renderers included — run it with cargo run -p gpui-base dock. crates/component/src/dock is the production skin over the same seam.

Re-exports§

pub use layout::DockLayout;
pub use layout::EditResult;
pub use layout::InsertTarget;
pub use layout::NodeId;
pub use layout::PaneNode;
pub use layout::PaneRef;
pub use layout::PaneTree;
pub use layout::PanelId;
pub use layout::RootKind;
pub use layout::TilePanel;

Modules§

layout

Structs§

AnyDrag
A host-owned value being dragged over the dock, opaque to the dock itself.
Dock
Runtime state for one dock: whether it is open, collapsible, its current size, and whether it is mid-resize.
DockArea
The main area of the dock.
DockAreaState
Used to serialize and deserialize the DockArea.
DockContext
What a skin needs to draw one dock, and the callbacks it invokes rather than reimplementing the open/close and clamping behavior.
DockSizing
Pure arithmetic for resizing one dock. The caller supplies the area bounds and the opposite dock’s size; base does not reach across entities to find them — the original Dock::resize read sibling dock sizes straight off the (application-owned) DockArea entity, which base has no way to do.
DockState
Used to serialize and deserialize the Dock.
DragPanel
A panel being dragged out of a tab group.
DropIndicator
What the skin should draw while a drag hovers a group.
DropPlaceholderBounds
The bounds a drop placeholder should occupy within a tab group, given where the drop would land.
PanelBuildContext
Everything a panel builder needs to reconstruct a panel from persisted data.
PanelRegistry
Global registry of panel builders, keyed by panel name, used to reconstruct a panel view from persisted PanelState/PanelInfo data.
PanelState
Used to serialize and deserialize the DockerItem.
TabGroup
A tab group’s behavior, with no appearance of its own.
TabGroupConstraints
Everything the container knows about a group’s place in the dock.
TabGroupContext
What a skin needs to draw a tab group, and the callbacks it invokes rather than reimplementing behavior.
TileContext
What a skin needs to draw one tile, and the callbacks it invokes rather than reimplementing the snapping and resize arithmetic.
TileMeta
TilesState
A tiles canvas’s behavior, with no appearance of its own.

Enums§

DockEvent
What the dock area reports outward.
DockPlacement
Placement of a Dock relative to the center area.
DropTarget
Where a host-owned drag landed.
PanelEvent
PanelInfo
ResizeSide
What a skin actually needs off the tiles geometry: the two sizes it has to draw to, and which edge a resize is pulling. Which edge (or corner) of a tile a resize drag is manipulating.
TabGroupEvent
Behavior a tab group cannot carry out on its own.
TilesEvent
What a tiles canvas cannot carry out on its own.

Constants§

DRAG_BAR_HEIGHT
What a skin actually needs off the tiles geometry: the two sizes it has to draw to, and which edge a resize is pulling. Height of the tile’s drag bar. This is hit-target geometry the skin must agree with when it paints the drag bar, not a visual constant, so it lives here rather than in crates/component.
HANDLE_SIZE
What a skin actually needs off the tiles geometry: the two sizes it has to draw to, and which edge a resize is pulling. Size of the resize-handle hit target at a tile’s corner/edge. Same reasoning as DRAG_BAR_HEIGHT.

Traits§

DockAreaRenderer
Appearance for the dock area. Base draws none of it.
Panel
Behavior a dockable panel provides. Presentation lives in the layer above: gpui_component::dock::Panel extends this with titles, toolbars, and menus.
PanelBuilder
Both halves of the persistence seam. PaneTree::to_state reads panel properties through PanelSource; PaneTree::from_state turns persisted leaves back into panels through PanelBuilder. Exporting only the first left from_state public but uncallable, since no caller outside this crate could name the trait its parameter requires. Turns a persisted leaf into a live panel id.
PanelSource
Both halves of the persistence seam. PaneTree::to_state reads panel properties through PanelSource; PaneTree::from_state turns persisted leaves back into panels through PanelBuilder. Exporting only the first left from_state public but uncallable, since no caller outside this crate could name the trait its parameter requires. How the layout tree reads properties of panels it only knows by id.
PanelView
Object-safe counterpart of Panel, used to hold heterogeneous panel entities behind a single handle.
TabGroupRenderer
Appearance for a tab group. Base draws none of it.
TilesRenderer
Appearance for a tiles canvas. Base draws none of it.

Functions§

register_panel
Register the Panel init by panel_name to global registry.