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