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