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