Skip to main content

gpui_component/dock/
mod.rs

1//! The gpui-component appearance for the dock.
2//!
3//! The layout tree, the persisted schema, the drag geometry, the active-panel
4//! state machine and the container entities all live in
5//! [`gpui_base::dock`]. This module is the skin over them: it re-exports the
6//! types a consumer needs, adds the presentation half of the panel traits
7//! (see [`panel`]), and implements base's three renderer traits.
8//!
9//! ```ignore
10//! let area = cx.new(|cx| {
11//!     DockArea::new("main", Some(1), window, cx).with_renderer(DockSkin::new(cx))
12//! });
13//! ```
14//!
15//! A [`DockArea`] built without [`DockSkin`] still docks, drags and persists —
16//! it simply draws no chrome at all.
17
18mod dock;
19mod invalid_panel;
20mod panel;
21mod tab_panel;
22#[cfg(test)]
23mod test_support;
24mod tiles;
25
26use std::{cell::Cell, rc::Rc};
27
28use gpui::{App, AppContext as _, Context, Entity, SharedString, WeakEntity, Window, actions};
29
30use crate::scroll::ScrollbarMode;
31
32/// The behavior half of the panel traits, which every panel implements
33/// alongside [`Panel`]. Exported under this name because `Panel` in this
34/// module is the presentation half that extends it.
35pub use gpui_base::dock::Panel as BasePanel;
36/// The object-safe counterpart of [`BasePanel`], for the same reason.
37pub use gpui_base::dock::PanelView as BasePanelView;
38/// Everything [`gpui_base::dock`] exports, so a consumer never has to depend
39/// on the foundation crate directly to write a skin or read a container's
40/// state. Kept in step with base's own list by
41/// `every_base_dock_export_is_reachable_from_here`.
42///
43/// Two names are handled elsewhere and one is deliberately absent:
44/// base's `Panel` and `PanelView` arrive as [`BasePanel`] and [`BasePanelView`]
45/// because this module's `Panel`/`PanelView` are the presentation halves that
46/// extend them, and base's `Dock` — a plain state struct holding one dock's
47/// open, collapsible, size and resizing flags — is not re-exported at all,
48/// because the name meant a panel container in every released version of this
49/// crate and handing it back with a different meaning is worse than dropping
50/// it. A skin reads a dock through [`DockContext`].
51pub use gpui_base::dock::{
52    AnyDrag, DRAG_BAR_HEIGHT, DockArea, DockAreaRenderer, DockAreaState, DockContext, DockEvent,
53    DockLayout, DockPlacement, DockSizing, DockState, DragPanel, DropIndicator,
54    DropPlaceholderBounds, DropTarget, EditResult, HANDLE_SIZE, InsertTarget, NodeId, PaneNode,
55    PaneRef, PaneTree, PanelBuildContext, PanelBuilder, PanelEvent, PanelId, PanelInfo,
56    PanelRegistry, PanelSource, PanelState, ResizeSide, RootKind, TabGroup, TabGroupConstraints,
57    TabGroupContext, TabGroupEvent, TabGroupRenderer, TileContext, TileMeta, TilePanel, TilesEvent,
58    TilesRenderer, TilesState, register_panel,
59};
60pub use panel::*;
61pub use tab_panel::DragPanelPreview;
62
63actions!(dock, [ToggleZoom, ClosePanel]);
64
65pub(crate) fn init(cx: &mut App) {
66    // `gpui_base::dock::PanelRegistry::init` is crate-private, but the global
67    // it installs is not: `DockArea::new` and `register_panel` both create it
68    // on demand, and this keeps the old guarantee that it exists as soon as
69    // `gpui_component::init` has run.
70    if cx.try_global::<PanelRegistry>().is_none() {
71        cx.set_global(PanelRegistry::new());
72    }
73}
74
75/// What every part of the skin reads, and the dock area it belongs to.
76///
77/// The renderer is the only skin-owned object in the picture, so the settings
78/// the old `DockArea` carried — the panel style, whether dock collapse
79/// affordances are offered at all — live here. It is shared by reference with
80/// the per-container renderers, which are built once each and outlive any one
81/// frame.
82pub(crate) struct SkinShared {
83    area: WeakEntity<DockArea>,
84    panel_style: Cell<PanelStyle>,
85    toggle_button_visible: Cell<bool>,
86    tiles_scrollbar_mode: Cell<Option<ScrollbarMode>>,
87    /// The dock whose resize handle is being dragged, if any. Only one can be.
88    resizing_dock: Cell<Option<DockPlacement>>,
89}
90
91impl SkinShared {
92    pub(crate) fn area(&self) -> &WeakEntity<DockArea> {
93        &self.area
94    }
95
96    pub(crate) fn panel_style(&self) -> PanelStyle {
97        self.panel_style.get()
98    }
99
100    pub(crate) fn is_toggle_button_visible(&self) -> bool {
101        self.toggle_button_visible.get()
102    }
103
104    pub(crate) fn tiles_scrollbar_mode(&self) -> Option<ScrollbarMode> {
105        self.tiles_scrollbar_mode.get()
106    }
107
108    pub(crate) fn resizing_dock(&self) -> &Cell<Option<DockPlacement>> {
109        &self.resizing_dock
110    }
111
112    /// Redraw the area after a setting changed. The skin is not an entity, so
113    /// nothing else would notice.
114    fn notify(&self, cx: &mut App) {
115        _ = self.area.update(cx, |_, cx| cx.notify());
116    }
117}
118
119/// The gpui-component appearance for a [`DockArea`], and the handle its
120/// settings are changed through.
121///
122/// Install it at construction, where the area's own weak handle is available:
123///
124/// ```ignore
125/// let skin = DockSkin::new(cx);
126/// DockArea::new("main", None, window, cx).with_renderer(skin)
127/// ```
128///
129/// Keep the returned handle to change a setting later; it is an `Rc`, so a
130/// clone and the installed renderer are the same skin.
131pub struct DockSkin {
132    shared: Rc<SkinShared>,
133}
134
135impl DockSkin {
136    /// Build a [`DockArea`] wearing this appearance, together with the handle
137    /// its settings are changed through.
138    ///
139    /// The skin needs the area's own weak handle, so it can only be built
140    /// while the area is being constructed; this is that dance done once.
141    pub fn dock_area(
142        id: impl Into<SharedString>,
143        version: Option<usize>,
144        window: &mut Window,
145        cx: &mut App,
146    ) -> (Entity<DockArea>, Rc<Self>) {
147        let mut skin = None;
148        let area = cx.new(|cx| {
149            let this = Self::new(cx);
150            skin = Some(this.clone());
151            DockArea::new(id, version, window, cx).with_renderer(this)
152        });
153        // The closure above runs before `cx.new` returns.
154        (
155            area,
156            skin.expect("DockSkin::new ran inside the constructor"),
157        )
158    }
159
160    pub fn new(cx: &mut Context<DockArea>) -> Rc<Self> {
161        Rc::new(Self {
162            shared: Rc::new(SkinShared {
163                area: cx.weak_entity(),
164                panel_style: Cell::new(PanelStyle::default()),
165                toggle_button_visible: Cell::new(true),
166                tiles_scrollbar_mode: Cell::new(None),
167                resizing_dock: Cell::new(None),
168            }),
169        })
170    }
171
172    pub(crate) fn shared(&self) -> &Rc<SkinShared> {
173        &self.shared
174    }
175
176    /// Whether a single-panel tab group draws a plain title or a full tab bar.
177    pub fn panel_style(&self) -> PanelStyle {
178        self.shared.panel_style()
179    }
180
181    pub fn set_panel_style(&self, style: PanelStyle, cx: &mut App) {
182        self.shared.panel_style.set(style);
183        self.shared.notify(cx);
184    }
185
186    /// Whether tab bars offer the affordance that collapses a neighbouring
187    /// dock.
188    pub fn is_toggle_button_visible(&self) -> bool {
189        self.shared.is_toggle_button_visible()
190    }
191
192    pub fn set_toggle_button_visible(&self, visible: bool, cx: &mut App) {
193        self.shared.toggle_button_visible.set(visible);
194        self.shared.notify(cx);
195    }
196
197    /// When a tiles canvas shows its scrollbar. `None` follows the theme.
198    pub fn tiles_scrollbar_mode(&self) -> Option<ScrollbarMode> {
199        self.shared.tiles_scrollbar_mode()
200    }
201
202    pub fn set_tiles_scrollbar_mode(&self, mode: Option<ScrollbarMode>, cx: &mut App) {
203        self.shared.tiles_scrollbar_mode.set(mode);
204        self.shared.notify(cx);
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    /// Every name `gpui_base::dock` exports has to be reachable from
211    /// `gpui_component::dock`, or an application cannot write its own skin
212    /// without depending on the foundation crate directly.
213    ///
214    /// This reads both export lists rather than naming them, because the way
215    /// this went wrong was checking the list against a description of base
216    /// instead of against base itself: a hand-written list cannot notice a
217    /// name base gained after it was written. `TilesState` and `TilesEvent`
218    /// were missing when this was added.
219    ///
220    /// The parse is deliberately crude — it takes the braces of each
221    /// `pub use ...::{..}` and the tail of each single-name `pub use a::b;` —
222    /// so a reformat of either file could trip it. That failure says "look at
223    /// the two lists", which is the right thing to do anyway.
224    fn exported_names(source: &str, prefix: &str) -> Vec<String> {
225        let mut names = Vec::new();
226        let mut rest = source;
227        while let Some(at) = rest.find(prefix) {
228            rest = &rest[at + prefix.len()..];
229            let Some(end) = rest.find(';') else { break };
230            let (item, tail) = rest.split_at(end);
231            rest = tail;
232            let item = item.trim();
233            let list = match (item.find('{'), item.rfind('}')) {
234                (Some(open), Some(close)) if open < close => &item[open + 1..close],
235                // `pub use a::b;` — the name is the last path segment.
236                _ => item.rsplit("::").next().unwrap_or(""),
237            };
238            names.extend(
239                list.split(',')
240                    .map(|name| name.split(" as ").next().unwrap_or("").trim().to_string())
241                    .filter(|name| !name.is_empty()),
242            );
243        }
244        names.sort();
245        names.dedup();
246        names
247    }
248
249    #[test]
250    fn every_base_dock_export_is_reachable_from_here() {
251        let base = include_str!("../../../base/src/dock/mod.rs");
252        let skin = include_str!("mod.rs");
253
254        let exported = exported_names(base, "pub use ");
255        assert!(
256            exported.len() > 30,
257            "the parse found only {} names in base's dock module, so it is \
258             reading the wrong thing rather than reporting the truth",
259            exported.len()
260        );
261
262        let reachable = exported_names(skin, "pub use gpui_base::dock::");
263        // `Panel` and `PanelView` are re-exported under other names because
264        // this module's own `Panel`/`PanelView` extend them; `Dock` is a
265        // documented omission. See the doc on the re-export block.
266        let renamed = ["Panel", "PanelView"];
267        let omitted = ["Dock"];
268
269        let missing: Vec<&String> = exported
270            .iter()
271            .filter(|name| {
272                !reachable.contains(name)
273                    && !renamed.contains(&name.as_str())
274                    && !omitted.contains(&name.as_str())
275            })
276            .collect();
277
278        assert!(
279            missing.is_empty(),
280            "gpui_base::dock exports these, and gpui_component::dock does not \
281             re-export them: {missing:?}. Add them to the list, or add the \
282             name to `omitted` with the reason on the re-export block."
283        );
284    }
285}