gpui_base/dock/mod.rs
1//! Dockable layout: splits, tab groups, and tiles canvases that a host can
2//! rearrange, persist, and restore — with no appearance of its own.
3//!
4//! # The tree is the source of truth
5//!
6//! Each region — the center, plus an optional left, right, and bottom dock —
7//! is one [`PaneTree`]. A tree is pure data. Containers are addressed by
8//! [`NodeId`], panels by [`PanelId`], and no GPUI entity handle is stored
9//! anywhere in it, which is what lets the layout algebra be exercised without
10//! an `App` and lets a whole layout be compared, normalized, and serialized as
11//! an ordinary value. A container is a `Split`, a `Tabs`, or a `Tiles`; there
12//! is no leaf variant, so a panel can only ever live inside a `Tabs` or a
13//! `Tiles`.
14//!
15//! Both ids are *stable*, and the rest of the design leans on it:
16//!
17//! - A [`NodeId`] survives every edit and every normalization rule. A
18//! container still present after a drag carries the same id it had before,
19//! which is what keeps a reconcile from tearing down entities the drag never
20//! touched.
21//! - A [`PanelId`] is the panel entity's `EntityId`, so it identifies the
22//! panel for as long as the entity lives, across any number of moves between
23//! groups and regions.
24//!
25//! Every mutation goes through the tree — [`PaneTree::insert_panel`],
26//! [`remove_panel`](PaneTree::remove_panel),
27//! [`move_panel`](PaneTree::move_panel), [`split`](PaneTree::split),
28//! [`set_active`](PaneTree::set_active), and the rest — and each normalizes
29//! before returning, so the tree is self-consistent the instant an edit
30//! returns. Every edit reports what it did as an [`EditResult`].
31//!
32//! # The area reconciles the tree into entities
33//!
34//! [`DockArea`] owns the trees and a cache of container entities keyed by
35//! `NodeId` ([`TabGroup`] for a `Tabs` node, [`TilesState`] for a `Tiles`
36//! one), plus the panel handles keyed by `PanelId`. After any edit that
37//! reports a change it walks the tree, creates entities for ids the cache does
38//! not have, drops entries for ids that are gone — telling those panels
39//! [`Panel::on_removed`] — pushes sizes and active indices into the survivors,
40//! and emits [`DockEvent::LayoutChanged`]. Nothing else turns a tree edit into
41//! live entities.
42//!
43//! Because node ids are stable, a steady-state pass creates and drops nothing.
44//!
45//! A layout is described the same entity-free way: [`DockLayout`] builds a
46//! tree, and [`DockArea::set_center`] / [`DockArea::set_dock`] install it.
47//! [`DockArea::dump`] and [`DockArea::load`] round-trip a whole area through
48//! [`DockAreaState`], rebuilding panels through the [`PanelRegistry`].
49//!
50//! # The renderer seam
51//!
52//! Base supplies behavior; the host supplies appearance. Nothing in this
53//! module paints a color, a border, or a size. Three traits carry the
54//! appearance in:
55//!
56//! - [`DockAreaRenderer`] — the area frame, each split's frame and the divider
57//! between its slots, one dock's chrome, and the stand-in for a panel this
58//! build cannot construct.
59//! - [`TabGroupRenderer`] — the tab bar, how the displayed panel is placed,
60//! and the drop indicator.
61//! - [`TilesRenderer`] — a tiles canvas, its tile frames, and their drag bars.
62//!
63//! A renderer never sees a drag event or a mouse position. Base attaches the
64//! drag sources, drop hit-testing, focus, and keyboard handling to the very
65//! elements the renderer returns, and hands it resolved state through
66//! [`DockContext`], [`TabGroupContext`], and [`TileContext`] — each of which
67//! also carries the callbacks (`toggle`, `select_tab`, `close`, `resize_to`)
68//! that the renderer invokes rather than reimplementing.
69//!
70//! An area built without a renderer still docks, drags, resizes and persists.
71//! It simply draws nothing but the panels themselves.
72//!
73//! [`Panel`] splits at the same seam: this trait covers behavior, and a
74//! presentation layer — `gpui_component::dock::Panel` — extends it with
75//! titles, toolbars, and menus. A panel type implements both.
76//!
77//! Every hook is optional in the same way: a renderer that declines one gets
78//! base's own minimum for it. [`DockAreaRenderer::render_split_handle`] is the
79//! clearest case — return `None` and the divider falls back to a one-pixel
80//! line colored from `Theme::resizable`, so a skin with no opinion about
81//! dividers implements nothing, while one that has an opinion replaces the
82//! paint without touching the hit area, the cursor, or the drag.
83//!
84//! # Why the layout is data
85//!
86//! The usual way to build a dock is to make each container a live view that
87//! holds its children, so the widget tree *is* the layout. That is what this
88//! module replaced, and the three costs it carries are the reason:
89//!
90//! - **Emptiness has to propagate.** When the last panel leaves a tab group,
91//! the group must remove itself from its parent, which may empty the parent
92//! in turn. With containers as views this is mutual recursion between two
93//! types, reaching upward through parent handles — and those handles have to
94//! be installed after construction, which in GPUI means a deferred pass.
95//! There is a window in which the tree disagrees with itself.
96//! - **Structure and identity are the same thing.** Rearranging the widget
97//! tree means creating and dropping views, so a drag can reset the state of
98//! containers it never touched.
99//! - **Nothing is testable without a window.** Asserting that a split collapses
100//! correctly requires an `App`, an entity, and a frame.
101//!
102//! Here the tree is a value and the entities are its projection. Collapse is
103//! [`PaneTree::normalize`]: one post-order pass repeated to a fixpoint, no
104//! parent pointers, no deferred work, and idempotent by construction. Identity
105//! is a [`NodeId`] that survives every edit and every normalization rule, so
106//! reconciliation is a diff rather than a rebuild. And the whole layout algebra
107//! runs as plain `#[test]`.
108//!
109//! What this buys, stated as properties rather than adjectives: a layout can be
110//! compared, cloned and serialized as an ordinary value; a steady-state
111//! reconcile creates and drops nothing, so a drag leaves untouched panels
112//! untouched; and `normalize(normalize(t)) == normalize(t)` holds for every
113//! tree, which is what makes the persisted format canonical.
114//!
115//! # Where this sits among docking libraries
116//!
117//! Editors tend to build the layout engine into the application — Zed's
118//! `PaneGroup` and VS Code's workbench are not reusable outside their hosts.
119//! Standalone libraries split the engine from the view to varying degrees:
120//! `golden-layout` owns its DOM outright, `FlexLayout` keeps a JSON model
121//! beside a React renderer, and `dockview` goes furthest, running a
122//! framework-agnostic engine behind thin adapters. All of them still paint
123//! their own chrome and expose CSS as the way to change it.
124//!
125//! This module takes the same separation one step further: the engine paints
126//! nothing at all. A renderer returns elements and base attaches the drag
127//! sources, drop hit-testing, focus and keyboard handling to the elements it
128//! got back, so appearance is not a set of overrides on top of a default look —
129//! there is no default look. `crates/component/src/dock` and
130//! `crates/base/examples/showcase/components/dock.rs` are two unrelated
131//! appearances over one behavior.
132//!
133//! The naming follows the same neighborhood where it can. A tab group here is
134//! what Zed calls a `Pane` and `dockview` calls a group; a [`Dock`] is Zed's
135//! dock; a [`Panel`] is the dockable content, as in `dockview` — note that
136//! VS Code uses "panel" for the bottom *region* instead, and `rc-dock` uses it
137//! for the tab container.
138//!
139//! # A minimal area
140//!
141//! ```ignore
142//! use std::rc::Rc;
143//! use gpui::{px, Context, Window};
144//! use gpui_base::dock::{DockArea, DockLayout, DockPlacement};
145//!
146//! let area = cx.new(|cx| {
147//! DockArea::new("workspace", Some(1), window, cx).with_renderer(Rc::new(MySkin))
148//! });
149//!
150//! area.update(cx, |area, cx| {
151//! area.set_center(
152//! DockLayout::h_split()
153//! .child(DockLayout::tabs().panel(files.clone()), Some(px(240.)))
154//! .child(DockLayout::tabs().panel(editor.clone()), None),
155//! window,
156//! cx,
157//! );
158//! area.set_dock(
159//! DockPlacement::Bottom,
160//! DockLayout::tabs().panel(terminal.clone()),
161//! window,
162//! cx,
163//! );
164//! });
165//! ```
166//!
167//! `crates/base/examples/showcase/components/dock.rs` is that program in full,
168//! renderers included — run it with `cargo run -p gpui-base dock`.
169//! `crates/component/src/dock` is the production skin over the same seam.
170
171mod active;
172mod dock_area;
173mod dock_placement;
174mod drag;
175pub mod layout;
176mod panel;
177mod registry;
178mod state;
179mod state_convert;
180mod tab_group;
181#[cfg(test)]
182pub(crate) mod test_support;
183mod tiles_geometry;
184mod tiles_state;
185
186pub use dock_area::{DockArea, DockAreaRenderer, DockContext, DockEvent};
187pub use dock_placement::{Dock, DockSizing};
188pub use drag::{AnyDrag, DragPanel, DropIndicator, DropPlaceholderBounds, DropTarget};
189// `split_placement_at` stays internal for the same reason: where a drop lands
190// is base's decision, and a renderer is told the result through
191// `TabGroupContext::drop_indicator`.
192pub use layout::{
193 DockLayout, EditResult, InsertTarget, NodeId, PaneNode, PaneRef, PaneTree, PanelId, RootKind,
194 TilePanel,
195};
196pub use panel::{Panel, PanelEvent, PanelView};
197pub use registry::{PanelBuildContext, PanelRegistry, register_panel};
198pub use state::{DockAreaState, DockPlacement, DockState, PanelInfo, PanelState, TileMeta};
199/// Both halves of the persistence seam. `PaneTree::to_state` reads panel
200/// properties through `PanelSource`; `PaneTree::from_state` turns persisted
201/// leaves back into panels through `PanelBuilder`. Exporting only the first
202/// left `from_state` public but uncallable, since no caller outside this crate
203/// could name the trait its parameter requires.
204pub use state_convert::{PanelBuilder, PanelSource};
205pub use tab_group::{
206 TabGroup, TabGroupConstraints, TabGroupContext, TabGroupEvent, TabGroupRenderer,
207};
208/// What a skin actually needs off the tiles geometry: the two sizes it has to
209/// draw to, and which edge a resize is pulling.
210pub use tiles_geometry::{DRAG_BAR_HEIGHT, HANDLE_SIZE, ResizeSide};
211// The arithmetic itself is deliberately not re-exported. Base resolves every
212// bound before a renderer sees it — a skin is handed finished `Bounds`, never
213// asked to snap anything — so `magnetic_snap`, `snap_edge`, `round_to_grid`,
214// `round_point_to_grid`, `compute_resized_bounds`, `apply_boundary_constraints`
215// and `content_size` had no caller outside this crate and no purpose there.
216// `ResizeDrag` and `TileChange` are `TilesState`'s own fields, and
217// `MINIMUM_SIZE` is a constraint base applies on the skin's behalf.
218pub use tiles_state::{TileContext, TilesEvent, TilesRenderer, TilesState};