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