From d937a90db6644243c836ddb9c5448d6c131f0dfc Mon Sep 17 00:00:00 2001 From: xda <49030173@qq.com> Date: Fri, 22 Nov 2024 14:41:04 +0800 Subject: [PATCH] dock: Render toggle dock buttons inside the Panels. (#414) Co-authored-by: Jason Lee --- crates/app/src/story_workspace.rs | 8 +- crates/ui/src/dock/dock.rs | 6 +- crates/ui/src/dock/mod.rs | 80 ++++++++++++++-- crates/ui/src/dock/stack_panel.rs | 73 +++++++++++++-- crates/ui/src/dock/tab_panel.rs | 135 ++++++++++++++++++++++++--- crates/ui/src/dock/toggle_buttons.rs | 112 ---------------------- 6 files changed, 264 insertions(+), 150 deletions(-) delete mode 100644 crates/ui/src/dock/toggle_buttons.rs diff --git a/crates/app/src/story_workspace.rs b/crates/app/src/story_workspace.rs index 346d3521..4183e9f9 100644 --- a/crates/app/src/story_workspace.rs +++ b/crates/app/src/story_workspace.rs @@ -11,7 +11,7 @@ use story::{ use ui::{ button::{Button, ButtonStyled as _}, color_picker::{ColorPicker, ColorPickerEvent}, - dock::{DockArea, DockAreaState, DockEvent, DockItem, ToggleButtons}, + dock::{DockArea, DockAreaState, DockEvent, DockItem}, h_flex, popup_menu::PopupMenuExt, theme::{ActiveTheme, Theme}, @@ -392,12 +392,6 @@ impl Render for StoryWorkspace { .justify_end() .px_2() .gap_2() - .child( - ToggleButtons::new(self.dock_area.downgrade()) - .small() - .outline() - .mr_4(), - ) .child(self.theme_color_picker.clone()) .child( Button::new("theme-mode") diff --git a/crates/ui/src/dock/dock.rs b/crates/ui/src/dock/dock.rs index 7834fccc..5e56a484 100644 --- a/crates/ui/src/dock/dock.rs +++ b/crates/ui/src/dock/dock.rs @@ -283,7 +283,7 @@ impl Dock { impl Render for Dock { fn render(&mut self, cx: &mut ViewContext) -> impl gpui::IntoElement { - if !self.open { + if !self.open && !self.placement.is_bottom() { return div(); } @@ -294,6 +294,10 @@ impl Render for Dock { DockPlacement::Left | DockPlacement::Right => this.h_flex().h_full().w(self.size), DockPlacement::Bottom => this.w_full().h(self.size), }) + // Bottom Dock should keep the title bar, then user can click the Toggle button + .when(!self.open && self.placement.is_bottom(), |this| { + this.h(px(30.)) + }) .map(|this| match &self.panel { DockItem::Split { view, .. } => this.child(view.clone()), DockItem::Tabs { view, .. } => this.child(view.clone()), diff --git a/crates/ui/src/dock/mod.rs b/crates/ui/src/dock/mod.rs index 24f5a9c4..7d13d2b2 100644 --- a/crates/ui/src/dock/mod.rs +++ b/crates/ui/src/dock/mod.rs @@ -4,14 +4,14 @@ mod panel; mod stack_panel; mod state; mod tab_panel; -mod toggle_buttons; use anyhow::Result; pub use dock::*; use gpui::{ actions, canvas, div, prelude::FluentBuilder, AnyElement, AnyView, AppContext, Axis, Bounds, - EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _, Pixels, Render, - SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowContext, + Edges, Entity as _, EntityId, EventEmitter, InteractiveElement as _, IntoElement, + ParentElement as _, Pixels, Render, SharedString, Styled, Subscription, View, ViewContext, + VisualContext, WeakView, WindowContext, }; use std::sync::Arc; @@ -19,7 +19,6 @@ pub use panel::*; pub use stack_panel::*; pub use state::*; pub use tab_panel::*; -pub use toggle_buttons::*; pub fn init(cx: &mut AppContext) { cx.set_global(PanelRegistry::new()); @@ -45,6 +44,9 @@ pub struct DockArea { /// The center view of the dockarea. items: DockItem, + /// The entity_id of the TabPanel where each toggle button should be displayed, + toggle_button_panels: Edges>, + /// The left dock of the dockarea. left_dock: Option>, /// The bottom dock of the dockarea. @@ -215,6 +217,22 @@ impl DockItem { } } } + + /// Recursively traverses to find the left-most and top-most TabPanel. + pub(crate) fn left_top_tab_panel(&self, cx: &AppContext) -> Option> { + match self { + DockItem::Tabs { view, .. } => Some(view.clone()), + DockItem::Split { view, .. } => view.read(cx).left_top_tab_panel(true, cx), + } + } + + /// Recursively traverses to find the right-most and top-most TabPanel. + pub(crate) fn right_top_tab_panel(&self, cx: &AppContext) -> Option> { + match self { + DockItem::Tabs { view, .. } => Some(view.clone()), + DockItem::Split { view, .. } => view.read(cx).right_top_tab_panel(true, cx), + } + } } impl DockArea { @@ -238,6 +256,7 @@ impl DockArea { bounds: Bounds::default(), items: dock_item, zoom_view: None, + toggle_button_panels: Edges::default(), left_dock: None, right_dock: None, bottom_dock: None, @@ -262,7 +281,7 @@ impl DockArea { pub fn set_root(&mut self, item: DockItem, cx: &mut ViewContext) { self.subscribe_item(&item, cx); self.items = item; - + self.update_toggle_button_tab_panels(cx); cx.notify(); } @@ -284,6 +303,7 @@ impl DockArea { dock.set_open(open, cx); dock })); + self.update_toggle_button_tab_panels(cx); } pub fn set_bottom_dock( @@ -304,6 +324,7 @@ impl DockArea { dock.set_open(open, cx); dock })); + self.update_toggle_button_tab_panels(cx); } pub fn set_right_dock( @@ -324,6 +345,7 @@ impl DockArea { dock.set_open(open, cx); dock })); + self.update_toggle_button_tab_panels(cx); } /// Set locked state of the dock area, if locked, the dock area cannot be split or move, but allows to resize panels. @@ -399,7 +421,7 @@ impl DockArea { } self.items = state.center.to_item(weak_self, cx); - + self.update_toggle_button_tab_panels(cx); Ok(()) } @@ -443,7 +465,18 @@ impl DockArea { self._subscriptions .push(cx.subscribe(view, move |_, _, event, cx| match event { - PanelEvent::LayoutChanged => cx.emit(DockEvent::LayoutChanged), + PanelEvent::LayoutChanged => { + let dock_area = cx.view().clone(); + cx.spawn(|_, mut cx| async move { + let _ = cx.update(|cx| { + let _ = dock_area.update(cx, |view, cx| { + view.update_toggle_button_tab_panels(cx) + }); + }); + }) + .detach(); + cx.emit(DockEvent::LayoutChanged); + } _ => {} })); } @@ -482,7 +515,17 @@ impl DockArea { }) .detach() } - PanelEvent::LayoutChanged => cx.emit(DockEvent::LayoutChanged), + PanelEvent::LayoutChanged => { + let dock_area = cx.view().clone(); + cx.spawn(|_, mut cx| async move { + let _ = cx.update(|cx| { + let _ = dock_area + .update(cx, |view, cx| view.update_toggle_button_tab_panels(cx)); + }); + }) + .detach(); + cx.emit(DockEvent::LayoutChanged); + } }); self._subscriptions.push(subscription); @@ -509,6 +552,27 @@ impl DockArea { DockItem::Tabs { view, .. } => view.clone().into_any_element(), } } + + pub fn update_toggle_button_tab_panels(&mut self, cx: &mut ViewContext) { + // Left toggle button + self.toggle_button_panels.left = self + .items + .left_top_tab_panel(cx) + .map(|view| view.entity_id()); + + // Right toggle button + self.toggle_button_panels.right = self + .items + .right_top_tab_panel(cx) + .map(|view| view.entity_id()); + + // Bottom toggle button + self.toggle_button_panels.bottom = self + .bottom_dock + .as_ref() + .and_then(|dock| dock.read(cx).panel.left_top_tab_panel(cx)) + .map(|view| view.entity_id()); + } } impl EventEmitter for DockArea {} impl Render for DockArea { diff --git a/crates/ui/src/dock/stack_panel.rs b/crates/ui/src/dock/stack_panel.rs index ed1a1b20..5fa6a44e 100644 --- a/crates/ui/src/dock/stack_panel.rs +++ b/crates/ui/src/dock/stack_panel.rs @@ -8,7 +8,7 @@ use crate::{ ResizablePanelGroup, }, theme::ActiveTheme, - Placement, + AxisExt as _, Placement, }; use super::{DockArea, DockItemState, Panel, PanelEvent, PanelView, TabPanel}; @@ -82,17 +82,13 @@ impl StackPanel { /// 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; + self.panels.len() == 1 } pub(super) fn panels_len(&self) -> usize { @@ -195,9 +191,7 @@ impl StackPanel { move |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, cx| { - tab_panel.set_parent(view.downgrade(), cx) - }); + 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()) @@ -286,6 +280,67 @@ impl StackPanel { cx.notify(); } + /// Find the first top left in the stack. + pub(super) fn left_top_tab_panel( + &self, + check_parent: bool, + cx: &AppContext, + ) -> Option> { + if check_parent { + if let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade()) { + if 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: &AppContext, + ) -> Option> { + if check_parent { + if let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade()) { + if 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, cx: &mut ViewContext) { self.panels.clear(); diff --git a/crates/ui/src/dock/tab_panel.rs b/crates/ui/src/dock/tab_panel.rs index 3389e7cc..90797394 100644 --- a/crates/ui/src/dock/tab_panel.rs +++ b/crates/ui/src/dock/tab_panel.rs @@ -2,9 +2,10 @@ use std::sync::Arc; use gpui::{ div, prelude::FluentBuilder, px, rems, AnchorCorner, AppContext, DefiniteLength, DismissEvent, - DragMoveEvent, Empty, EventEmitter, FocusHandle, FocusableView, InteractiveElement as _, - IntoElement, ParentElement, Pixels, Render, ScrollHandle, StatefulInteractiveElement, Styled, - View, ViewContext, VisualContext as _, WeakView, WindowContext, + DragMoveEvent, Empty, Entity, EventEmitter, FocusHandle, FocusableView, + InteractiveElement as _, IntoElement, ParentElement, Pixels, Render, ScrollHandle, + SharedString, StatefulInteractiveElement, Styled, View, ViewContext, VisualContext as _, + WeakView, WindowContext, }; use rust_i18n::t; @@ -19,7 +20,8 @@ use crate::{ }; use super::{ - ClosePanel, DockArea, DockItemState, Panel, PanelEvent, PanelView, StackPanel, ToggleZoom, + ClosePanel, DockArea, DockItemState, DockPlacement, Panel, PanelEvent, PanelView, StackPanel, + ToggleZoom, }; #[derive(Clone, Copy)] @@ -76,7 +78,6 @@ pub struct TabPanel { tab_bar_scroll_handle: ScrollHandle, is_zoomed: bool, is_collapsed: bool, - /// When drag move, will get the placement of the panel to be split will_split_placement: Option, } @@ -160,7 +161,7 @@ impl TabPanel { } } - pub(super) fn set_parent(&mut self, view: WeakView, _: &mut ViewContext) { + pub(super) fn set_parent(&mut self, view: WeakView) { self.stack_panel = Some(view); } @@ -273,6 +274,11 @@ impl TabPanel { } } + pub(super) fn set_collapsed(&mut self, collapsed: bool, cx: &mut ViewContext) { + self.is_collapsed = collapsed; + cx.notify(); + } + fn is_locked(&self, cx: &AppContext) -> bool { let Some(dock_area) = self.dock_area.upgrade() else { return true; @@ -316,11 +322,6 @@ impl TabPanel { !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, state: TabState, cx: &mut ViewContext) -> impl IntoElement { let is_zoomed = self.is_zoomed && state.zoomable; let view = cx.view().clone(); @@ -373,9 +374,81 @@ impl TabPanel { ) } + fn render_dock_toggle_button( + &self, + placement: DockPlacement, + cx: &mut ViewContext, + ) -> Option { + if self.is_zoomed { + return None; + } + + let view_entity_id = cx.view().entity_id(); + let dock_area = self.dock_area.upgrade()?.read(cx); + let toggle_button_panels = dock_area.toggle_button_panels; + + // Check if current TabPanel's entity_id matches the one stored in DockArea for this placement + if !match placement { + DockPlacement::Left => toggle_button_panels.left == Some(view_entity_id), + DockPlacement::Right => toggle_button_panels.right == Some(view_entity_id), + DockPlacement::Bottom => toggle_button_panels.bottom == Some(view_entity_id), + } { + return None; + } + + let is_open = dock_area.is_dock_open(placement, cx); + + let icon = match placement { + DockPlacement::Left => { + if is_open { + IconName::PanelLeft + } else { + IconName::PanelLeftOpen + } + } + DockPlacement::Right => { + if is_open { + IconName::PanelRight + } else { + IconName::PanelRightOpen + } + } + DockPlacement::Bottom => { + if is_open { + IconName::PanelBottom + } else { + IconName::PanelBottomOpen + } + } + }; + + Some( + Button::new(SharedString::from(format!("toggle-dock:{:?}", placement))) + .icon(icon) + .xsmall() + .ghost() + .tooltip(match is_open { + true => t!("Dock.Collapse"), + false => t!("Dock.Expand"), + }) + .on_click(cx.listener({ + let dock_area = self.dock_area.clone(); + move |_, _, cx| { + _ = dock_area.update(cx, |dock_area, cx| { + dock_area.toggle_dock(placement, cx); + }); + } + })), + ) + } + fn render_title_bar(&self, state: TabState, cx: &mut ViewContext) -> impl IntoElement { let view = cx.view().clone(); + let left_dock_button = self.render_dock_toggle_button(DockPlacement::Left, cx); + let bottom_dock_button = self.render_dock_toggle_button(DockPlacement::Bottom, cx); + let right_dock_button = self.render_dock_toggle_button(DockPlacement::Right, cx); + if self.panels.len() == 1 { let panel = self.panels.get(0).unwrap(); let title_style = panel.title_style(cx); @@ -387,9 +460,24 @@ impl TabPanel { .h(px(30.)) .py_2() .px_3() + .when(left_dock_button.is_some(), |this| this.pl_2()) + .when(right_dock_button.is_some(), |this| this.pr_2()) .when_some(title_style, |this, theme| { this.bg(theme.background).text_color(theme.foreground) }) + .when( + left_dock_button.is_some() || bottom_dock_button.is_some(), + |this| { + this.child( + h_flex() + .flex_shrink_0() + .mr_1() + .gap_1() + .children(left_dock_button) + .children(bottom_dock_button), + ) + }, + ) .child( div() .id("tab") @@ -417,7 +505,8 @@ impl TabPanel { .flex_shrink_0() .ml_1() .gap_1() - .child(self.render_toolbar(state, cx)), + .child(self.render_toolbar(state, cx)) + .children(right_dock_button), ) .into_any_element(); } @@ -426,6 +515,25 @@ impl TabPanel { TabBar::new("tab-bar") .track_scroll(self.tab_bar_scroll_handle.clone()) + .when( + left_dock_button.is_some() || bottom_dock_button.is_some(), + |this| { + this.prefix( + h_flex() + .items_center() + .top_0() + .right_0() + .border_r_1() + .border_b_1() + .h_full() + .border_color(cx.theme().border) + .bg(cx.theme().tab_bar) + .px_2() + .children(left_dock_button) + .children(bottom_dock_button), + ) + }, + ) .children(self.panels.iter().enumerate().map(|(ix, panel)| { let mut active = ix == self.active_ix; @@ -498,7 +606,8 @@ impl TabPanel { .bg(cx.theme().tab_bar) .px_2() .gap_1() - .child(self.render_toolbar(state, cx)), + .child(self.render_toolbar(state, cx)) + .when_some(right_dock_button, |this, btn| this.child(btn)), ) .into_any_element() } diff --git a/crates/ui/src/dock/toggle_buttons.rs b/crates/ui/src/dock/toggle_buttons.rs deleted file mode 100644 index b429ace5..00000000 --- a/crates/ui/src/dock/toggle_buttons.rs +++ /dev/null @@ -1,112 +0,0 @@ -use gpui::{ - div, prelude::FluentBuilder as _, Div, InteractiveElement as _, IntoElement, - ParentElement as _, RenderOnce, Stateful, Styled, WeakView, WindowContext, -}; - -use crate::{ - button::{Button, ButtonStyle, ButtonStyled}, - button_group::ButtonGroup, - IconName, Selectable as _, Sizable, Size, -}; - -use super::{DockArea, DockPlacement}; - -#[derive(IntoElement)] -pub struct ToggleButtons { - base: Stateful
, - dock_area: WeakView, - size: Size, - style: ButtonStyle, -} - -impl ToggleButtons { - /// Create a new instance of the toggle buttons. - pub fn new(dock_area: WeakView) -> Self { - Self { - dock_area, - base: div().id("dock-toggle-buttons"), - style: ButtonStyle::Outline, - size: Size::Medium, - } - } -} - -impl Sizable for ToggleButtons { - fn with_size(mut self, size: impl Into) -> Self { - self.size = size.into(); - self - } -} -impl ButtonStyled for ToggleButtons { - fn with_style(mut self, style: ButtonStyle) -> Self { - self.style = style; - self - } -} -impl Styled for ToggleButtons { - fn style(&mut self) -> &mut gpui::StyleRefinement { - self.base.style() - } -} - -impl RenderOnce for ToggleButtons { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { - let Some(dock_area) = self.dock_area.upgrade() else { - return self.base; - }; - - let left_dock: Option = dock_area - .read(cx) - .has_dock(DockPlacement::Left) - .then(|| dock_area.read(cx).is_dock_open(DockPlacement::Left, cx)); - let right_dock: Option = dock_area - .read(cx) - .has_dock(DockPlacement::Right) - .then(|| dock_area.read(cx).is_dock_open(DockPlacement::Right, cx)); - let bottom_dock: Option = dock_area - .read(cx) - .has_dock(DockPlacement::Bottom) - .then(|| dock_area.read(cx).is_dock_open(DockPlacement::Bottom, cx)); - - self.base.child( - ButtonGroup::new("toggle-docks") - .with_style(self.style) - .with_size(self.size) - .when_some(left_dock, |this, open| { - this.child( - Button::new("toggle-left-dock") - .icon(IconName::PanelLeft) - .selected(open), - ) - }) - .when_some(bottom_dock, |this, open| { - this.child( - Button::new("toggle-bottom-dock") - .icon(IconName::PanelBottom) - .selected(open), - ) - }) - .when_some(right_dock, |this, open| { - this.child( - Button::new("toggle-right-dock") - .icon(IconName::PanelRight) - .selected(open), - ) - }) - .on_click(move |indexes, cx| { - if let Some(ix) = indexes.first() { - let placement = match ix { - 0 => DockPlacement::Left, - 1 => DockPlacement::Bottom, - 2 => DockPlacement::Right, - _ => DockPlacement::Left, - }; - - dock_area.update(cx, |this, cx| { - this.toggle_dock(placement, cx); - }) - } - }), - ) - } -}