Skip to main content

gpui_base/dock/
panel.rs

1use std::{any::Any, collections::HashMap, sync::Arc};
2
3use gpui::{
4    AnyView, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Render, WeakEntity, Window,
5};
6
7use super::layout::PanelId;
8use super::state::PanelState;
9use super::state_convert::PanelSource;
10use super::tab_group::TabGroup;
11
12pub enum PanelEvent {
13    ZoomIn,
14    ZoomOut,
15    LayoutChanged,
16}
17
18/// Behavior a dockable panel provides. Presentation lives in the layer above:
19/// `gpui_component::dock::Panel` extends this with titles, toolbars, and menus.
20#[allow(unused_variables)]
21pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
22    /// Identifies the panel in persisted layouts. Once chosen, never change it.
23    fn panel_name(&self) -> &'static str;
24
25    /// Whether the panel is drawn at all. A hidden panel keeps its place in
26    /// the layout tree and its tab, and reappears when this turns back on;
27    /// a container whose panels are all hidden gives up its slot.
28    fn visible(&self, cx: &App) -> bool {
29        true
30    }
31
32    /// Whether the panel may be closed. A container can still refuse — the
33    /// last group of a dock does — so this is permission, not a guarantee.
34    fn closable(&self, cx: &App) -> bool {
35        true
36    }
37
38    /// Whether the panel can zoom at all. Where the zoom control appears is a
39    /// presentation decision and belongs to the layer above.
40    fn zoomable(&self, cx: &App) -> bool {
41        true
42    }
43
44    /// Called with the frame-end net state when this panel becomes, or stops
45    /// being, the displayed tab of its group: exactly one notification per
46    /// edge, delivered on the next tick after the change — never same-value
47    /// repeats nor false-then-true flips within one frame.
48    ///
49    /// A panel removed from its group is NOT told `false`; [`Panel::on_removed`]
50    /// is the deactivation signal. A hidden panel occupying the active slot
51    /// still receives `true` even though rendering falls back to the first
52    /// visible panel.
53    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {}
54
55    /// Called when the group displaying this panel zooms in or out.
56    ///
57    /// Only the panel that is currently displayed is told: a group has one
58    /// zoom state, and it is the visible panel that fills the dock. Panels
59    /// sharing the group's other tabs hear nothing, and a panel that is not
60    /// displayed when the zoom changes is never told about it retroactively.
61    fn set_zoomed(&mut self, zoomed: bool, window: &mut Window, cx: &mut Context<Self>) {}
62
63    /// Called when the panel joins a tab group, with a weak handle on it.
64    ///
65    /// Delivered before any `set_active`, so a panel can hold the handle and
66    /// act on the first activation. A panel moved between groups is told
67    /// again, with the new group; it is not told it was removed in between.
68    fn on_added_to(
69        &mut self,
70        group: WeakEntity<TabGroup>,
71        window: &mut Window,
72        cx: &mut Context<Self>,
73    ) {
74    }
75
76    /// Called when the panel leaves the dock for good — closed, or displaced
77    /// by a wholesale `set_center`, `set_dock`, `remove_dock` or `load`.
78    ///
79    /// This is also the deactivation signal: a panel that was displayed is not
80    /// told `set_active(false)` on its way out. A panel dragged from one group
81    /// to another never leaves the dock, so it never hears this.
82    fn on_removed(&mut self, window: &mut Window, cx: &mut Context<Self>) {}
83
84    /// The panel's own persisted state, written into the layout under its
85    /// [`panel_name`](Panel::panel_name) and handed back to the
86    /// [`PanelRegistry`](crate::dock::PanelRegistry) builder on the next load.
87    ///
88    /// The default records the name and nothing else, which is enough for a
89    /// panel whose builder can reconstruct it from the name alone.
90    fn dump(&self, cx: &App) -> PanelState {
91        PanelState::new(self.panel_name())
92    }
93}
94
95/// Object-safe counterpart of [`Panel`], used to hold heterogeneous panel
96/// entities behind a single handle.
97#[allow(unused_variables)]
98pub trait PanelView: 'static + Send + Sync {
99    fn panel_name(&self, cx: &App) -> &'static str;
100    fn panel_id(&self, cx: &App) -> PanelId;
101    fn closable(&self, cx: &App) -> bool;
102    fn zoomable(&self, cx: &App) -> bool;
103    fn visible(&self, cx: &App) -> bool;
104    fn set_active(&self, active: bool, window: &mut Window, cx: &mut App);
105    fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App);
106    fn on_added_to(&self, group: WeakEntity<TabGroup>, window: &mut Window, cx: &mut App);
107    fn on_removed(&self, window: &mut Window, cx: &mut App);
108    fn view(&self) -> AnyView;
109    fn focus_handle(&self, cx: &App) -> FocusHandle;
110    fn dump(&self, cx: &App) -> PanelState;
111
112    /// The concrete value behind this handle.
113    ///
114    /// A layer above base cannot recover its own richer panel trait object
115    /// from this one: `Arc<dyn some_skin::PanelView>` coerces *to*
116    /// `Arc<dyn PanelView>`, and Rust has no coercion back — a sub-trait
117    /// object cannot be recovered from a super-trait object. The registry
118    /// documents the same wall one layer in.
119    ///
120    /// So a layer that needs more off a panel than this trait carries defines
121    /// a *concrete* handle type, implements this trait for it by delegation,
122    /// hands base that, and recovers it here with
123    /// `panel.as_any().downcast_ref::<ItsOwnHandle>()`. That downcast works
124    /// precisely because the type it names is concrete.
125    ///
126    /// The blanket implementation for `Entity<T>` answers with the entity, so
127    /// a caller that knows the panel type can recover `Entity<T>` instead.
128    /// Which of the two a given handle holds is not fixed: a failed
129    /// `downcast_ref` means only that this handle was built by someone else,
130    /// and every caller needs a path for that.
131    fn as_any(&self) -> &dyn Any;
132}
133
134impl<T: Panel> PanelView for Entity<T> {
135    fn panel_name(&self, cx: &App) -> &'static str {
136        self.read(cx).panel_name()
137    }
138
139    fn panel_id(&self, _: &App) -> PanelId {
140        PanelId::from(self.entity_id())
141    }
142
143    fn closable(&self, cx: &App) -> bool {
144        self.read(cx).closable(cx)
145    }
146
147    fn zoomable(&self, cx: &App) -> bool {
148        self.read(cx).zoomable(cx)
149    }
150
151    fn visible(&self, cx: &App) -> bool {
152        self.read(cx).visible(cx)
153    }
154
155    fn set_active(&self, active: bool, window: &mut Window, cx: &mut App) {
156        self.update(cx, |this, cx| {
157            this.set_active(active, window, cx);
158        })
159    }
160
161    fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App) {
162        self.update(cx, |this, cx| {
163            this.set_zoomed(zoomed, window, cx);
164        })
165    }
166
167    fn on_added_to(&self, group: WeakEntity<TabGroup>, window: &mut Window, cx: &mut App) {
168        self.update(cx, |this, cx| this.on_added_to(group, window, cx));
169    }
170
171    fn on_removed(&self, window: &mut Window, cx: &mut App) {
172        self.update(cx, |this, cx| this.on_removed(window, cx));
173    }
174
175    fn view(&self) -> AnyView {
176        self.clone().into()
177    }
178
179    fn focus_handle(&self, cx: &App) -> FocusHandle {
180        self.read(cx).focus_handle(cx)
181    }
182
183    fn dump(&self, cx: &App) -> PanelState {
184        self.read(cx).dump(cx)
185    }
186
187    fn as_any(&self) -> &dyn Any {
188        self
189    }
190}
191
192impl From<&dyn PanelView> for AnyView {
193    fn from(handle: &dyn PanelView) -> Self {
194        handle.view()
195    }
196}
197
198impl<T: Panel> From<&dyn PanelView> for Entity<T> {
199    fn from(value: &dyn PanelView) -> Self {
200        value.view().downcast::<T>().unwrap()
201    }
202}
203
204impl PartialEq for dyn PanelView {
205    fn eq(&self, other: &Self) -> bool {
206        self.view() == other.view()
207    }
208}
209
210/// Reads panel properties out of the live entity map that `DockArea` keeps.
211///
212/// This is the `PanelSource` implementation `PaneTree::to_state` runs
213/// against when `DockArea::dump` writes a live layout out.
214pub(crate) struct LivePanels<'a> {
215    panels: &'a HashMap<PanelId, Arc<dyn PanelView>>,
216    cx: &'a App,
217}
218
219impl<'a> LivePanels<'a> {
220    pub(crate) fn new(panels: &'a HashMap<PanelId, Arc<dyn PanelView>>, cx: &'a App) -> Self {
221        Self { panels, cx }
222    }
223}
224
225impl PanelSource for LivePanels<'_> {
226    fn panel_name(&self, id: PanelId) -> &'static str {
227        self.panels
228            .get(&id)
229            .map(|panel| panel.panel_name(self.cx))
230            .unwrap_or("")
231    }
232
233    fn is_visible(&self, id: PanelId) -> bool {
234        self.panels
235            .get(&id)
236            .is_some_and(|panel| panel.visible(self.cx))
237    }
238
239    fn dump(&self, id: PanelId) -> PanelState {
240        self.panels
241            .get(&id)
242            .map(|panel| panel.dump(self.cx))
243            .unwrap_or_default()
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::super::state::PanelInfo;
250    use super::*;
251    use gpui::{
252        AppContext as _, Context, Empty, EventEmitter, FocusHandle, Focusable, IntoElement, Render,
253        TestAppContext, Window,
254    };
255
256    struct Probe {
257        focus_handle: FocusHandle,
258        visible: bool,
259    }
260
261    impl Panel for Probe {
262        fn panel_name(&self) -> &'static str {
263            "Probe"
264        }
265
266        fn visible(&self, _: &App) -> bool {
267            self.visible
268        }
269    }
270
271    impl EventEmitter<PanelEvent> for Probe {}
272    impl Focusable for Probe {
273        fn focus_handle(&self, _: &App) -> FocusHandle {
274            self.focus_handle.clone()
275        }
276    }
277    impl Render for Probe {
278        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
279            Empty
280        }
281    }
282
283    #[gpui::test]
284    fn a_panel_entity_answers_through_the_object_safe_view(cx: &mut TestAppContext) {
285        let panel = cx.new(|cx| Probe {
286            focus_handle: cx.focus_handle(),
287            visible: false,
288        });
289        let view: Arc<dyn PanelView> = Arc::new(panel.clone());
290
291        cx.read(|cx| {
292            assert_eq!(view.panel_name(cx), "Probe");
293            assert_eq!(view.visible(cx), false);
294            assert_eq!(view.panel_id(cx), PanelId::from(panel.entity_id()));
295        });
296    }
297
298    #[gpui::test]
299    fn the_default_dump_records_only_the_panel_name(cx: &mut TestAppContext) {
300        let panel = cx.new(|cx| Probe {
301            focus_handle: cx.focus_handle(),
302            visible: true,
303        });
304        let state = cx.read(|cx| panel.read(cx).dump(cx));
305
306        assert_eq!(state.panel_name, "Probe");
307        assert!(state.children.is_empty());
308        assert_eq!(state.info, PanelInfo::panel(serde_json::Value::Null));
309    }
310}