gpui_component/dock/
stack_panel.rs

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