use std::sync::Arc; use gpui::{ App, AppContext as _, Axis, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, WeakEntity, Window, }; use gpui_component::{ ActiveTheme, AxisExt as _, Placement, ResizablePanelEvent, ResizablePanelGroup, ResizableState, h_flex, resizable_panel, }; use smallvec::SmallVec; use super::{DockArea, Panel, PanelEvent, PanelState, PanelView, TabPanel}; use crate::PanelInfo; use crate::resize_handle::PANEL_MIN_SIZE; pub struct StackPanel { pub(super) parent: Option>, pub(super) axis: Axis, focus_handle: FocusHandle, pub(crate) panels: SmallVec<[Arc; 2]>, state: Entity, _subscriptions: Vec, } impl Panel for StackPanel { fn panel_name(&self) -> &'static str { "StackPanel" } fn title(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { "StackPanel" } fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context) { for panel in &self.panels { panel.set_active(active, window, cx); } } fn dump(&self, cx: &App) -> PanelState { let sizes = self.state.read(cx).sizes().clone(); let mut state = PanelState::new(self); state.info = PanelInfo::stack(sizes, self.axis); for panel in &self.panels { state.add_child(panel.dump(cx)); } state } } impl StackPanel { pub fn new(axis: Axis, _: &mut Window, cx: &mut Context) -> Self { let state = cx.new(|_| ResizableState::default()); let _subscriptions = vec![ // Bubble up the resize event. cx.subscribe(&state, |_, _, _: &ResizablePanelEvent, cx| { cx.emit(PanelEvent::LayoutChanged) }), ]; Self { axis, parent: None, focus_handle: cx.focus_handle(), panels: SmallVec::new(), state, _subscriptions, } } /// The first level of the stack panel is root, will not have a parent. fn is_root(&self) -> bool { self.parent.is_none() } /// Return true if self or parent only have last panel. pub(super) fn is_last_panel(&self, cx: &App) -> bool { if self.panels.len() > 1 { return false; } if let Some(parent) = &self.parent && let Some(parent) = parent.upgrade() { return parent.read(cx).is_last_panel(cx); } true } pub(super) fn panels_len(&self) -> usize { self.panels.len() } /// Return the index of the panel. pub(crate) fn index_of_panel(&self, panel: Arc) -> Option { self.panels.iter().position(|p| p == &panel) } fn assert_panel_is_valid(&self, panel: &Arc) { assert!( panel.view().downcast::().is_ok() || panel.view().downcast::().is_ok(), "Panel must be a `TabPanel` or `StackPanel`" ); } /// Add a panel at the end of the stack. /// /// If `size` is `None`, the panel will be given the average size of all panels in the stack. /// /// The `panel` must be a [`TabPanel`] or [`StackPanel`]. pub fn add_panel( &mut self, panel: Arc, size: Option, dock_area: WeakEntity, window: &mut Window, cx: &mut Context, ) { self.insert_panel(panel, self.panels.len(), size, dock_area, window, cx); } /// Add a panel at the [`Placement`]. /// /// The `panel` must be a [`TabPanel`] or [`StackPanel`]. pub fn add_panel_at( &mut self, panel: Arc, placement: Placement, size: Option, dock_area: WeakEntity, window: &mut Window, cx: &mut Context, ) { self.insert_panel_at( panel, self.panels_len(), placement, size, dock_area, window, cx, ); } /// Insert a panel at the index. /// /// The `panel` must be a [`TabPanel`] or [`StackPanel`]. #[allow(clippy::too_many_arguments)] pub fn insert_panel_at( &mut self, panel: Arc, ix: usize, placement: Placement, size: Option, dock_area: WeakEntity, window: &mut Window, cx: &mut Context, ) { match placement { Placement::Top | Placement::Left => { self.insert_panel_before(panel, ix, size, dock_area, window, cx) } Placement::Right | Placement::Bottom => { self.insert_panel_after(panel, ix, size, dock_area, window, cx) } } } /// Insert a panel at the index. /// /// The `panel` must be a [`TabPanel`] or [`StackPanel`]. pub fn insert_panel_before( &mut self, panel: Arc, ix: usize, size: Option, dock_area: WeakEntity, window: &mut Window, cx: &mut Context, ) { self.insert_panel(panel, ix, size, dock_area, window, cx); } /// Insert a panel after the index. /// /// The `panel` must be a [`TabPanel`] or [`StackPanel`]. pub fn insert_panel_after( &mut self, panel: Arc, ix: usize, size: Option, dock_area: WeakEntity, window: &mut Window, cx: &mut Context, ) { self.insert_panel(panel, ix + 1, size, dock_area, window, cx); } fn insert_panel( &mut self, panel: Arc, ix: usize, size: Option, dock_area: WeakEntity, window: &mut Window, cx: &mut Context, ) { self.assert_panel_is_valid(&panel); // If the panel is already in the stack, return. if self.index_of_panel(panel.clone()).is_some() { return; } let view = cx.entity().clone(); window.defer(cx, { let panel = panel.clone(); move |window, cx| { // If the panel is a TabPanel, set its parent to this. if let Ok(tab_panel) = panel.view().downcast::() { tab_panel.update(cx, |tab_panel, _| tab_panel.set_parent(view.downgrade())); } else if let Ok(stack_panel) = panel.view().downcast::() { stack_panel.update(cx, |stack_panel, _| { stack_panel.parent = Some(view.downgrade()) }); } // Subscribe to the panel's layout change event. _ = dock_area.update(cx, |this, cx| { if let Ok(tab_panel) = panel.view().downcast::() { this.subscribe_panel(&tab_panel, window, cx); } else if let Ok(stack_panel) = panel.view().downcast::() { this.subscribe_panel(&stack_panel, window, cx); } }); } }); let ix = if ix > self.panels.len() { self.panels.len() } else { ix }; // Get avg size of all panels to insert new panel, if size is None. let size = match size { Some(size) => size, None => { let state = self.state.read(cx); (state.container_size() / (state.sizes().len() + 1) as f32).max(PANEL_MIN_SIZE) } }; self.panels.insert(ix, panel.clone()); self.state.update(cx, |state, cx| { state.insert_panel(Some(size), Some(ix), cx); }); cx.emit(PanelEvent::LayoutChanged); cx.notify(); } /// Remove panel from the stack. /// /// If `ix` is not found, do nothing. pub fn remove_panel( &mut self, panel: Arc, window: &mut Window, cx: &mut Context, ) { let Some(ix) = self.index_of_panel(panel.clone()) else { return; }; self.panels.remove(ix); self.state.update(cx, |state, cx| { state.remove_panel(ix, cx); }); cx.emit(PanelEvent::LayoutChanged); self.remove_self_if_empty(window, cx); } /// Replace the old panel with the new panel at same index. pub(super) fn replace_panel( &mut self, old_panel: Arc, new_panel: Entity, _: &mut Window, cx: &mut Context, ) { if let Some(ix) = self.index_of_panel(old_panel.clone()) { self.panels[ix] = Arc::new(new_panel.clone()); self.state.update(cx, |state, cx| { state.reset_panel(ix, cx); }); cx.emit(PanelEvent::LayoutChanged); } } /// If children is empty, remove self from parent view. pub(crate) fn remove_self_if_empty(&mut self, window: &mut Window, cx: &mut Context) { if self.is_root() { return; } if !self.panels.is_empty() { return; } let view = cx.entity().clone(); if let Some(parent) = self.parent.as_ref() { _ = parent.update(cx, |parent, cx| { parent.remove_panel(Arc::new(view.clone()), window, cx); }); } cx.emit(PanelEvent::LayoutChanged); cx.notify(); } /// Find the first top left in the stack. pub(super) fn left_top_tab_panel( &self, check_parent: bool, cx: &App, ) -> Option> { if check_parent && let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade()) && let Some(panel) = parent.read(cx).left_top_tab_panel(true, cx) { return Some(panel); } let first_panel = self.panels.first(); if let Some(view) = first_panel { if let Ok(tab_panel) = view.view().downcast::() { Some(tab_panel) } else if let Ok(stack_panel) = view.view().downcast::() { stack_panel.read(cx).left_top_tab_panel(false, cx) } else { None } } else { None } } /// Find the first top right in the stack. pub(super) fn right_top_tab_panel( &self, check_parent: bool, cx: &App, ) -> Option> { if check_parent && let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade()) && let Some(panel) = parent.read(cx).right_top_tab_panel(true, cx) { return Some(panel); } let panel = if self.axis.is_vertical() { self.panels.first() } else { self.panels.last() }; if let Some(view) = panel { if let Ok(tab_panel) = view.view().downcast::() { Some(tab_panel) } else if let Ok(stack_panel) = view.view().downcast::() { stack_panel.read(cx).right_top_tab_panel(false, cx) } else { None } } else { None } } /// Remove all panels from the stack. pub(super) fn remove_all_panels(&mut self, _: &mut Window, cx: &mut Context) { self.panels.clear(); self.state.update(cx, |state, cx| { state.clear(); cx.notify(); }); } /// Change the axis of the stack panel. pub(super) fn set_axis(&mut self, axis: Axis, _: &mut Window, cx: &mut Context) { self.axis = axis; cx.notify(); } } impl Focusable for StackPanel { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() } } impl EventEmitter for StackPanel {} impl EventEmitter for StackPanel {} impl Render for StackPanel { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { h_flex() .size_full() .overflow_hidden() .bg(cx.theme().tokens.tab_bar) .child( ResizablePanelGroup::new("stack-panel-group") .with_state(&self.state) .axis(self.axis) .children(self.panels.clone().into_iter().map(|panel| { resizable_panel() .child(panel.view()) .visible(panel.visible(cx)) })), ) } }