Skip to main content

gpui_base/dock/
mod.rs

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