From 079cc35c48e153a9a6bc8629fb9f7cf8c249fc9f Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 14 Nov 2024 18:33:49 +0800 Subject: [PATCH] dock: Limit drag move or close when tab panel or parent only have 1 panel. (#418) --- crates/app/src/story_workspace.rs | 18 ++++--- crates/ui/src/dock/stack_panel.rs | 15 ++++++ crates/ui/src/dock/tab_panel.rs | 84 +++++++++++++++++++++++-------- 3 files changed, 89 insertions(+), 28 deletions(-) diff --git a/crates/app/src/story_workspace.rs b/crates/app/src/story_workspace.rs index 7f5e3da2..60045b96 100644 --- a/crates/app/src/story_workspace.rs +++ b/crates/app/src/story_workspace.rs @@ -233,12 +233,18 @@ impl StoryWorkspace { cx, ); - let bottom_panels = DockItem::tabs( - vec![ - Arc::new(StoryContainer::panel::(cx)), - Arc::new(StoryContainer::panel::(cx)), - ], - None, + let bottom_panels = DockItem::split_with_sizes( + Axis::Vertical, + vec![DockItem::tabs( + vec![ + Arc::new(StoryContainer::panel::(cx)), + Arc::new(StoryContainer::panel::(cx)), + ], + None, + &dock_area, + cx, + )], + vec![None], &dock_area, cx, ); diff --git a/crates/ui/src/dock/stack_panel.rs b/crates/ui/src/dock/stack_panel.rs index c8889141..ed1a1b20 100644 --- a/crates/ui/src/dock/stack_panel.rs +++ b/crates/ui/src/dock/stack_panel.rs @@ -80,6 +80,21 @@ impl StackPanel { self.parent.is_none() } + /// Return true if self or parent only have last panel. + pub(super) fn is_last_panel(&self, cx: &AppContext) -> bool { + if self.is_root() { + return self.panels.len() == 1; + } + + if let Some(parent) = &self.parent { + if let Some(parent) = parent.upgrade() { + return parent.read(cx).is_last_panel(cx); + } + } + + return false; + } + pub(super) fn panels_len(&self) -> usize { self.panels.len() } diff --git a/crates/ui/src/dock/tab_panel.rs b/crates/ui/src/dock/tab_panel.rs index d036fe1d..cf3cbbf4 100644 --- a/crates/ui/src/dock/tab_panel.rs +++ b/crates/ui/src/dock/tab_panel.rs @@ -22,6 +22,14 @@ use super::{ ClosePanel, DockArea, DockItemState, Panel, PanelEvent, PanelView, StackPanel, ToggleZoom, }; +#[derive(Clone, Copy)] +struct TabState { + closeable: bool, + zoomable: bool, + draggable: bool, + droppable: bool, +} + #[derive(Clone)] pub(crate) struct DragPanel { pub(crate) panel: Arc, @@ -267,7 +275,7 @@ impl TabPanel { fn is_locked(&self, cx: &AppContext) -> bool { let Some(dock_area) = self.dock_area.upgrade() else { - return false; + return true; }; if dock_area.read(cx).is_locked() { @@ -281,16 +289,40 @@ impl TabPanel { self.stack_panel.is_none() } + /// Return true if self or parent only have last panel. + fn is_last_panel(&self, cx: &AppContext) -> bool { + if let Some(parent) = &self.stack_panel { + if let Some(stack_panel) = parent.upgrade() { + if !stack_panel.read(cx).is_last_panel(cx) { + return false; + } + } + } + + self.panels.len() <= 1 + } + + /// Return true if the tab panel is draggable. + /// + /// E.g. if the parent and self only have one panel, it is not draggable. + fn draggable(&self, cx: &AppContext) -> bool { + !self.is_locked(cx) && !self.is_last_panel(cx) + } + + /// Return true if the tab panel is droppable. + /// + /// E.g. if the tab panel is locked, it is not droppable. + fn droppable(&self, cx: &AppContext) -> bool { + !self.is_locked(cx) + } + pub(super) fn set_collapsed(&mut self, collapsed: bool, cx: &mut ViewContext) { self.is_collapsed = collapsed; cx.notify(); } - fn render_toolbar(&self, cx: &mut ViewContext) -> impl IntoElement { - let closeable = self.closeable(cx); - let zoomable = self.zoomable(cx); - - let is_zoomed = self.is_zoomed && zoomable; + fn render_toolbar(&self, state: TabState, cx: &mut ViewContext) -> impl IntoElement { + let is_zoomed = self.is_zoomed && state.zoomable; let view = cx.view().clone(); let build_popup_menu = move |this, cx: &WindowContext| view.read(cx).popup_menu(this, cx); @@ -324,7 +356,7 @@ impl TabPanel { .ghost() .popup_menu(move |this, cx| { build_popup_menu(this, cx) - .when(zoomable, |this| { + .when(state.zoomable, |this| { let name = if is_zoomed { t!("Dock.Zoom Out") } else { @@ -332,7 +364,7 @@ impl TabPanel { }; this.separator().menu(name, Box::new(ToggleZoom)) }) - .when(closeable, |this| { + .when(state.closeable, |this| { this.separator() .menu(t!("Dock.Close"), Box::new(ClosePanel)) }) @@ -341,9 +373,8 @@ impl TabPanel { ) } - fn render_title_bar(&self, cx: &mut ViewContext) -> impl IntoElement { + fn render_title_bar(&self, state: TabState, cx: &mut ViewContext) -> impl IntoElement { let view = cx.view().clone(); - let is_locked = self.is_locked(cx); if self.panels.len() == 1 { let panel = self.panels.get(0).unwrap(); @@ -368,7 +399,7 @@ impl TabPanel { .text_ellipsis() .whitespace_nowrap() .child(panel.title(cx)) - .when(!is_locked, |this| { + .when(state.draggable, |this| { this.on_drag( DragPanel { panel: panel.clone(), @@ -386,7 +417,7 @@ impl TabPanel { .flex_shrink_0() .ml_1() .gap_1() - .child(self.render_toolbar(cx)), + .child(self.render_toolbar(state, cx)), ) .into_any_element(); } @@ -409,12 +440,14 @@ impl TabPanel { .on_click(cx.listener(move |view, _, cx| { view.set_active_ix(ix, cx); })) - .when(!is_locked, |this| { + .when(state.draggable, |this| { this.on_drag(DragPanel::new(panel.clone(), view.clone()), |drag, cx| { cx.stop_propagation(); cx.new_view(|_| drag.clone()) }) - .drag_over::(|this, _, cx| { + }) + .when(state.droppable, |this| { + this.drag_over::(|this, _, cx| { this.rounded_l_none() .border_l_2() .border_r_0() @@ -435,7 +468,7 @@ impl TabPanel { .h_full() .flex_grow() .min_w_16() - .when(!is_locked, |this| { + .when(state.droppable, |this| { this.drag_over::(|this, _, cx| this.bg(cx.theme().drop_target)) .on_drop(cx.listener(move |this, drag: &DragPanel, cx| { this.will_split_placement = None; @@ -462,14 +495,12 @@ impl TabPanel { .bg(cx.theme().tab_bar) .px_2() .gap_1() - .child(self.render_toolbar(cx)), + .child(self.render_toolbar(state, cx)), ) .into_any_element() } - fn render_active_panel(&self, cx: &mut ViewContext) -> impl IntoElement { - let is_locked = self.is_locked(cx); - + fn render_active_panel(&self, state: TabState, cx: &mut ViewContext) -> impl IntoElement { self.active_panel() .map(|panel| { div() @@ -479,7 +510,7 @@ impl TabPanel { .overflow_x_hidden() .flex_1() .child(panel.view()) - .when(!is_locked, |this| { + .when(state.droppable, |this| { this.on_drag_move(cx.listener(Self::on_panel_drag_move)) .child( div() @@ -718,6 +749,15 @@ impl EventEmitter for TabPanel {} impl Render for TabPanel { fn render(&mut self, cx: &mut ViewContext) -> impl gpui::IntoElement { let focus_handle = self.focus_handle(cx); + let mut state = TabState { + closeable: self.closeable(cx), + draggable: self.draggable(cx), + droppable: self.droppable(cx), + zoomable: self.zoomable(cx), + }; + if !state.draggable { + state.closeable = false; + } v_flex() .id("tab-panel") @@ -727,7 +767,7 @@ impl Render for TabPanel { .size_full() .overflow_hidden() .bg(cx.theme().background) - .child(self.render_title_bar(cx)) - .child(self.render_active_panel(cx)) + .child(self.render_title_bar(state, cx)) + .child(self.render_active_panel(state, cx)) } }