gpui_component/dock/
stack_panel.rs

1use std::sync::Arc;
2
3use crate::{
4    dock::PanelInfo,
5    h_flex,
6    resizable::{
7        resizable_panel, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState,
8        ResizableState, PANEL_MIN_SIZE,
9    },
10    ActiveTheme, AxisExt as _, Placement,
11};
12
13use super::{DockArea, Panel, PanelEvent, PanelState, PanelView, TabPanel};
14use gpui::{
15    App, Axis, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
16    ParentElement, Pixels, Render, Styled, Subscription, WeakEntity, Window,
17};
18use smallvec::SmallVec;
19
20pub struct StackPanel {
21    pub(super) parent: Option<WeakEntity<StackPanel>>,
22    pub(super) axis: Axis,
23    focus_handle: FocusHandle,
24    pub(crate) panels: SmallVec<[Arc<dyn PanelView>; 2]>,
25    state: Entity<ResizableState>,
26    _subscriptions: Vec<Subscription>,
27}
28
29impl Panel for StackPanel {
30    fn panel_name(&self) -> &'static str {
31        "StackPanel"
32    }
33
34    fn title(&self, _window: &gpui::Window, _cx: &gpui::App) -> gpui::AnyElement {
35        "StackPanel".into_any_element()
36    }
37    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut App) {
38        for panel in &self.panels {
39            panel.set_active(active, window, cx);
40        }
41    }
42    fn dump(&self, cx: &App) -> PanelState {
43        let sizes = self.state.read(cx).sizes().clone();
44        let mut state = PanelState::new(self);
45        for panel in &self.panels {
46            state.add_child(panel.dump(cx));
47            state.info = PanelInfo::stack(sizes.clone(), self.axis);
48        }
49
50        state
51    }
52}
53
54impl StackPanel {
55    pub fn new(axis: Axis, _: &mut Window, cx: &mut Context<Self>) -> Self {
56        let state = ResizableState::new(cx);
57
58        // Bubble up the resize event.
59        let _subscriptions = vec![cx.subscribe(&state, |_, _, _: &ResizablePanelEvent, cx| {
60            cx.emit(PanelEvent::LayoutChanged)
61        })];
62
63        Self {
64            axis,
65            parent: None,
66            focus_handle: cx.focus_handle(),
67            panels: SmallVec::new(),
68            state,
69            _subscriptions,
70        }
71    }
72
73    /// The first level of the stack panel is root, will not have a parent.
74    fn is_root(&self) -> bool {
75        self.parent.is_none()
76    }
77
78    /// Return true if self or parent only have last panel.
79    pub(super) fn is_last_panel(&self, cx: &App) -> bool {
80        if self.panels.len() > 1 {
81            return false;
82        }
83
84        if let Some(parent) = &self.parent {
85            if let Some(parent) = parent.upgrade() {
86                return parent.read(cx).is_last_panel(cx);
87            }
88        }
89
90        true
91    }
92
93    pub(super) fn panels_len(&self) -> usize {
94        self.panels.len()
95    }
96
97    /// Return the index of the panel.
98    pub(crate) fn index_of_panel(&self, panel: Arc<dyn PanelView>) -> Option<usize> {
99        self.panels.iter().position(|p| p == &panel)
100    }
101
102    fn assert_panel_is_valid(&self, panel: &Arc<dyn PanelView>) {
103        assert!(
104            panel.view().downcast::<TabPanel>().is_ok()
105                || panel.view().downcast::<StackPanel>().is_ok(),
106            "Panel must be a `TabPanel` or `StackPanel`"
107        );
108    }
109
110    /// Add a panel at the end of the stack.
111    ///
112    /// If `size` is `None`, the panel will be given the average size of all panels in the stack.
113    ///
114    /// The `panel` must be a [`TabPanel`] or [`StackPanel`].
115    pub fn add_panel(
116        &mut self,
117        panel: Arc<dyn PanelView>,
118        size: Option<Pixels>,
119        dock_area: WeakEntity<DockArea>,
120        window: &mut Window,
121        cx: &mut Context<Self>,
122    ) {
123        self.insert_panel(panel, self.panels.len(), size, dock_area, window, cx);
124    }
125
126    /// Add a panel at the [`Placement`].
127    ///
128    /// The `panel` must be a [`TabPanel`] or [`StackPanel`].
129    pub fn add_panel_at(
130        &mut self,
131        panel: Arc<dyn PanelView>,
132        placement: Placement,
133        size: Option<Pixels>,
134        dock_area: WeakEntity<DockArea>,
135        window: &mut Window,
136        cx: &mut Context<Self>,
137    ) {
138        self.insert_panel_at(
139            panel,
140            self.panels_len(),
141            placement,
142            size,
143            dock_area,
144            window,
145            cx,
146        );
147    }
148
149    /// Insert a panel at the index.
150    ///
151    /// The `panel` must be a [`TabPanel`] or [`StackPanel`].
152    #[allow(clippy::too_many_arguments)]
153    pub fn insert_panel_at(
154        &mut self,
155        panel: Arc<dyn PanelView>,
156        ix: usize,
157        placement: Placement,
158        size: Option<Pixels>,
159        dock_area: WeakEntity<DockArea>,
160        window: &mut Window,
161        cx: &mut Context<Self>,
162    ) {
163        match placement {
164            Placement::Top | Placement::Left => {
165                self.insert_panel_before(panel, ix, size, dock_area, window, cx)
166            }
167            Placement::Right | Placement::Bottom => {
168                self.insert_panel_after(panel, ix, size, dock_area, window, cx)
169            }
170        }
171    }
172
173    /// Insert a panel at the index.
174    ///
175    /// The `panel` must be a [`TabPanel`] or [`StackPanel`].
176    pub fn insert_panel_before(
177        &mut self,
178        panel: Arc<dyn PanelView>,
179        ix: usize,
180        size: Option<Pixels>,
181        dock_area: WeakEntity<DockArea>,
182        window: &mut Window,
183        cx: &mut Context<Self>,
184    ) {
185        self.insert_panel(panel, ix, size, dock_area, window, cx);
186    }
187
188    /// Insert a panel after the index.
189    ///
190    /// The `panel` must be a [`TabPanel`] or [`StackPanel`].
191    pub fn insert_panel_after(
192        &mut self,
193        panel: Arc<dyn PanelView>,
194        ix: usize,
195        size: Option<Pixels>,
196        dock_area: WeakEntity<DockArea>,
197        window: &mut Window,
198        cx: &mut Context<Self>,
199    ) {
200        self.insert_panel(panel, ix + 1, size, dock_area, window, cx);
201    }
202
203    fn insert_panel(
204        &mut self,
205        panel: Arc<dyn PanelView>,
206        ix: usize,
207        size: Option<Pixels>,
208        dock_area: WeakEntity<DockArea>,
209        window: &mut Window,
210        cx: &mut Context<Self>,
211    ) {
212        self.assert_panel_is_valid(&panel);
213
214        // If the panel is already in the stack, return.
215        if let Some(_) = self.index_of_panel(panel.clone()) {
216            return;
217        }
218
219        let view = cx.entity().clone();
220        window.defer(cx, {
221            let panel = panel.clone();
222
223            move |window, cx| {
224                // If the panel is a TabPanel, set its parent to this.
225                if let Ok(tab_panel) = panel.view().downcast::<TabPanel>() {
226                    tab_panel.update(cx, |tab_panel, _| tab_panel.set_parent(view.downgrade()));
227                } else if let Ok(stack_panel) = panel.view().downcast::<Self>() {
228                    stack_panel.update(cx, |stack_panel, _| {
229                        stack_panel.parent = Some(view.downgrade())
230                    });
231                }
232
233                // Subscribe to the panel's layout change event.
234                _ = dock_area.update(cx, |this, cx| {
235                    if let Ok(tab_panel) = panel.view().downcast::<TabPanel>() {
236                        this.subscribe_panel(&tab_panel, window, cx);
237                    } else if let Ok(stack_panel) = panel.view().downcast::<Self>() {
238                        this.subscribe_panel(&stack_panel, window, cx);
239                    }
240                });
241            }
242        });
243
244        let ix = if ix > self.panels.len() {
245            self.panels.len()
246        } else {
247            ix
248        };
249
250        // Get avg size of all panels to insert new panel, if size is None.
251        let size = match size {
252            Some(size) => size,
253            None => {
254                let state = self.state.read(cx);
255                (state.total_size() / (state.sizes().len() + 1) as f32).max(PANEL_MIN_SIZE)
256            }
257        };
258
259        self.panels.insert(ix, panel.clone());
260        self.state.update(cx, |state, cx| {
261            state.insert_panel(Some(size), Some(ix), cx);
262        });
263        cx.emit(PanelEvent::LayoutChanged);
264        cx.notify();
265    }
266
267    /// Remove panel from the stack.
268    ///
269    /// If `ix` is not found, do nothing.
270    pub fn remove_panel(
271        &mut self,
272        panel: Arc<dyn PanelView>,
273        window: &mut Window,
274        cx: &mut Context<Self>,
275    ) {
276        let Some(ix) = self.index_of_panel(panel.clone()) else {
277            return;
278        };
279
280        self.panels.remove(ix);
281        self.state.update(cx, |state, cx| {
282            state.remove_panel(ix, cx);
283        });
284
285        cx.emit(PanelEvent::LayoutChanged);
286        self.remove_self_if_empty(window, cx);
287    }
288
289    /// Replace the old panel with the new panel at same index.
290    pub(super) fn replace_panel(
291        &mut self,
292        old_panel: Arc<dyn PanelView>,
293        new_panel: Entity<StackPanel>,
294        _: &mut Window,
295        cx: &mut Context<Self>,
296    ) {
297        if let Some(ix) = self.index_of_panel(old_panel.clone()) {
298            self.panels[ix] = Arc::new(new_panel.clone());
299
300            let panel_state = ResizablePanelState::default();
301            self.state.update(cx, |state, cx| {
302                state.replace_panel(ix, panel_state, cx);
303            });
304            cx.emit(PanelEvent::LayoutChanged);
305        }
306    }
307
308    /// If children is empty, remove self from parent view.
309    pub(crate) fn remove_self_if_empty(&mut self, window: &mut Window, cx: &mut Context<Self>) {
310        if self.is_root() {
311            return;
312        }
313
314        if !self.panels.is_empty() {
315            return;
316        }
317
318        let view = cx.entity().clone();
319        if let Some(parent) = self.parent.as_ref() {
320            _ = parent.update(cx, |parent, cx| {
321                parent.remove_panel(Arc::new(view.clone()), window, cx);
322            });
323        }
324
325        cx.emit(PanelEvent::LayoutChanged);
326        cx.notify();
327    }
328
329    /// Find the first top left in the stack.
330    pub(super) fn left_top_tab_panel(
331        &self,
332        check_parent: bool,
333        cx: &App,
334    ) -> Option<Entity<TabPanel>> {
335        if check_parent {
336            if let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade()) {
337                if let Some(panel) = parent.read(cx).left_top_tab_panel(true, cx) {
338                    return Some(panel);
339                }
340            }
341        }
342
343        let first_panel = self.panels.first();
344        if let Some(view) = first_panel {
345            if let Ok(tab_panel) = view.view().downcast::<TabPanel>() {
346                Some(tab_panel)
347            } else if let Ok(stack_panel) = view.view().downcast::<StackPanel>() {
348                stack_panel.read(cx).left_top_tab_panel(false, cx)
349            } else {
350                None
351            }
352        } else {
353            None
354        }
355    }
356
357    /// Find the first top right in the stack.
358    pub(super) fn right_top_tab_panel(
359        &self,
360        check_parent: bool,
361        cx: &App,
362    ) -> Option<Entity<TabPanel>> {
363        if check_parent {
364            if let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade()) {
365                if let Some(panel) = parent.read(cx).right_top_tab_panel(true, cx) {
366                    return Some(panel);
367                }
368            }
369        }
370
371        let panel = if self.axis.is_vertical() {
372            self.panels.first()
373        } else {
374            self.panels.last()
375        };
376
377        if let Some(view) = panel {
378            if let Ok(tab_panel) = view.view().downcast::<TabPanel>() {
379                Some(tab_panel)
380            } else if let Ok(stack_panel) = view.view().downcast::<StackPanel>() {
381                stack_panel.read(cx).right_top_tab_panel(false, cx)
382            } else {
383                None
384            }
385        } else {
386            None
387        }
388    }
389
390    /// Remove all panels from the stack.
391    pub(super) fn remove_all_panels(&mut self, _: &mut Window, cx: &mut Context<Self>) {
392        self.panels.clear();
393        self.state.update(cx, |state, cx| {
394            state.clear();
395            cx.notify();
396        });
397    }
398
399    /// Change the axis of the stack panel.
400    pub(super) fn set_axis(&mut self, axis: Axis, _: &mut Window, cx: &mut Context<Self>) {
401        self.axis = axis;
402        cx.notify();
403    }
404}
405
406impl Focusable for StackPanel {
407    fn focus_handle(&self, _cx: &App) -> FocusHandle {
408        self.focus_handle.clone()
409    }
410}
411impl EventEmitter<PanelEvent> for StackPanel {}
412impl EventEmitter<DismissEvent> for StackPanel {}
413impl Render for StackPanel {
414    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
415        h_flex()
416            .size_full()
417            .overflow_hidden()
418            .bg(cx.theme().tab_bar)
419            .child(
420                ResizablePanelGroup::new("stack-panel-group", self.state.clone())
421                    .axis(self.axis)
422                    .children(self.panels.clone().into_iter().map(|panel| {
423                        resizable_panel()
424                            .child(panel.view())
425                            .visible(panel.visible(cx))
426                    })),
427            )
428    }
429}