From c5c8e46ac760a806b283f5de96acf78154e274d6 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Fri, 23 Aug 2024 18:01:29 +0800 Subject: [PATCH] New Dock (#172) https://github.com/user-attachments/assets/c8c55faa-8f17-4fa2-9f62-5fdd598087ef - [x] Move to insert panel to tabs middle. - [x] Zoom panel - [x] Tab popup menu (Close Panel, Split, Zoom in/Zoom out) - [x] TabBar scrollable - [ ] TabBar nav history --- Cargo.lock | 1 - crates/app/src/story_workspace.rs | 79 +++-- crates/story/Cargo.toml | 1 - crates/story/src/lib.rs | 173 ++-------- crates/story/src/resizable_story.rs | 3 - crates/ui/locales/ui.yml | 13 + crates/ui/src/button.rs | 9 +- crates/ui/src/dock/mod.rs | 62 ++++ crates/ui/src/dock/panel.rs | 53 +++ crates/ui/src/dock/stack_panel.rs | 272 +++++++++++++++ crates/ui/src/dock/tab_panel.rs | 502 ++++++++++++++++++++++++++++ crates/ui/src/lib.rs | 1 + crates/ui/src/resizable/panel.rs | 264 ++++++++++----- crates/ui/src/styled.rs | 24 +- crates/ui/src/tab/tab.rs | 4 +- crates/ui/src/tab/tab_bar.rs | 16 +- crates/ui/src/theme.rs | 12 +- 17 files changed, 1198 insertions(+), 291 deletions(-) create mode 100644 crates/ui/src/dock/mod.rs create mode 100644 crates/ui/src/dock/panel.rs create mode 100644 crates/ui/src/dock/stack_panel.rs create mode 100644 crates/ui/src/dock/tab_panel.rs diff --git a/Cargo.lock b/Cargo.lock index e91a1681..bd2fe162 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4915,7 +4915,6 @@ dependencies = [ "regex", "serde", "ui", - "workspace", ] [[package]] diff --git a/crates/app/src/story_workspace.rs b/crates/app/src/story_workspace.rs index 47c3dbee..86c2c22e 100644 --- a/crates/app/src/story_workspace.rs +++ b/crates/app/src/story_workspace.rs @@ -6,11 +6,12 @@ use story::{ ModalStory, PopupStory, ProgressStory, ResizableStory, ScrollableStory, StoryContainer, SwitchStory, TableStory, TextStory, TooltipStory, }; -use workspace::{TitleBar, Workspace}; +use workspace::TitleBar; use std::sync::Arc; use ui::{ button::Button, + dock::{DockArea, StackPanel, TabPanel}, drawer::Drawer, h_flex, modal::Modal, @@ -38,26 +39,48 @@ pub fn init(_app_state: Arc, cx: &mut AppContext) { } pub struct StoryWorkspace { - workspace: View, locale_selector: View, + // stack_panel: View, + dock_area: View, } impl StoryWorkspace { - pub fn new( - _app_state: Arc, - workspace: View, - cx: &mut ViewContext, - ) -> Self { + pub fn new(_app_state: Arc, cx: &mut ViewContext) -> Self { cx.observe_window_appearance(|_workspace, cx| { Theme::sync_system_appearance(cx); }) .detach(); + let stack_panel = cx.new_view(|cx| StackPanel::new(Axis::Horizontal, cx)); + let dock_area = cx.new_view(|cx| DockArea::new(stack_panel.clone(), cx)); + let weak_dock_area = dock_area.downgrade(); + + let tab_panel = cx.new_view(|cx| TabPanel::new(weak_dock_area.clone(), cx)); + let right_tab_panel = cx.new_view(|cx| TabPanel::new(weak_dock_area.clone(), cx)); + let right_tab_panel1 = cx.new_view(|cx| TabPanel::new(weak_dock_area.clone(), cx)); + + stack_panel.update(cx, |view, cx| { + view.add_panel(tab_panel.clone(), None, weak_dock_area.clone(), cx); + + let stock_panel1 = cx.new_view(|cx| StackPanel::new(Axis::Vertical, cx)); + view.add_panel( + stock_panel1.clone(), + Some(px(400.)), + weak_dock_area.clone(), + cx, + ); + + stock_panel1.update(cx, |view, cx| { + view.add_panel(right_tab_panel.clone(), None, weak_dock_area.clone(), cx); + view.add_panel(right_tab_panel1.clone(), None, weak_dock_area.clone(), cx); + }) + }); + StoryContainer::add_pane( "Buttons", "Displays a button or a component that looks like a button.", ButtonStory::view(cx).into(), - workspace.clone(), + tab_panel.clone(), cx, ) .detach(); @@ -66,7 +89,7 @@ impl StoryWorkspace { "Input", "A control that allows the user to input text.", InputStory::view(cx).into(), - workspace.clone(), + tab_panel.clone(), cx, ) .detach(); @@ -75,7 +98,7 @@ impl StoryWorkspace { "Text", "Links, paragraphs, checkboxes, and more.", TextStory::view(cx).into(), - workspace.clone(), + tab_panel.clone(), cx, ) .detach(); @@ -84,7 +107,7 @@ impl StoryWorkspace { "Switch", "A control that allows the user to toggle between two states.", SwitchStory::view(cx).into(), - workspace.clone(), + tab_panel.clone(), cx, ) .detach(); @@ -93,7 +116,7 @@ impl StoryWorkspace { "Dropdowns", "Displays a list of options for the user to pick from—triggered by a button.", DropdownStory::new(cx).into(), - workspace.clone(), + tab_panel.clone(), cx, ) .detach(); @@ -102,7 +125,7 @@ impl StoryWorkspace { "Modal", "Modal & Drawer use examples", ModalStory::view(cx).into(), - workspace.clone(), + tab_panel.clone(), cx, ) .detach(); @@ -111,7 +134,7 @@ impl StoryWorkspace { "Popup", "A popup displays content on top of the main page.", PopupStory::view(cx).into(), - workspace.clone(), + tab_panel.clone(), cx, ) .detach(); @@ -120,7 +143,7 @@ impl StoryWorkspace { "Tooltip", "Displays a short message when users hover over an element.", TooltipStory::view(cx).into(), - workspace.clone(), + tab_panel.clone(), cx, ) .detach(); @@ -129,7 +152,7 @@ impl StoryWorkspace { "List", "A list displays a series of items.", ListStory::view(cx).into(), - workspace.clone(), + tab_panel.clone(), cx, ) .detach(); @@ -138,7 +161,7 @@ impl StoryWorkspace { "Icon", "Icon use examples", IconStory::view(cx).into(), - workspace.clone(), + tab_panel.clone(), cx, ) .detach(); @@ -147,14 +170,14 @@ impl StoryWorkspace { "Image", "Render SVG image and Chart", ImageStory::view(cx).into(), - workspace.clone(), + right_tab_panel1.clone(), cx, ) .detach(); // StoryContainer::add_panel( // WebViewStory::view(cx).into(), - // workspace.clone(), + // stack_panel.clone(), // DockPosition::Right, // px(450.), // cx, @@ -164,7 +187,7 @@ impl StoryWorkspace { "Table", "Powerful table and datagrids built.", TableStory::view(cx).into(), - workspace.clone(), + tab_panel.clone(), cx, ) .detach(); @@ -173,7 +196,7 @@ impl StoryWorkspace { "Progress", "Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.", ProgressStory::view(cx).into(), - workspace.clone(), + tab_panel.clone(), cx, ) .detach(); @@ -182,7 +205,7 @@ impl StoryWorkspace { "Resizable", "Accessible resizable panel groups and layouts with keyboard support.", ResizableStory::view(cx).into(), - workspace.clone(), + tab_panel.clone(), cx, ) .detach(); @@ -191,7 +214,7 @@ impl StoryWorkspace { "Scrollable", "A scrollable area with scroll bar.", ScrollableStory::view(cx).into(), - workspace.clone(), + tab_panel.clone(), cx, ) .detach(); @@ -200,14 +223,15 @@ impl StoryWorkspace { "Calendar", "A calendar component.", CalendarStory::view(cx).into(), - workspace.clone(), + right_tab_panel.clone(), cx, ) .detach(); let locale_selector = cx.new_view(LocaleSelector::new); + Self { - workspace, + dock_area, locale_selector, } } @@ -235,8 +259,7 @@ impl StoryWorkspace { }; let window = cx.open_window(options, |cx| { - let workspace = cx.new_view(|cx| Workspace::new(None, cx)); - let story_view = cx.new_view(|cx| Self::new(app_state.clone(), workspace, cx)); + let story_view = cx.new_view(|cx| Self::new(app_state.clone(), cx)); cx.new_view(|cx| Root::new(story_view.into(), cx)) })?; @@ -366,7 +389,7 @@ impl Render for StoryWorkspace { ), ), ) - .child(self.workspace.clone()) + .child(self.dock_area.clone()) .when(!has_active_modal, |this| { this.when_some(active_drawer, |this, builder| { let drawer = Drawer::new(cx); diff --git a/crates/story/Cargo.toml b/crates/story/Cargo.toml index 191cdab5..c74e06f2 100644 --- a/crates/story/Cargo.toml +++ b/crates/story/Cargo.toml @@ -8,7 +8,6 @@ ui.workspace = true gpui.workspace = true fake = "2.9.2" anyhow = "1" -workspace.workspace = true charts-rs = "0.3" regex = "1" chrono = "0.4" diff --git a/crates/story/src/lib.rs b/crates/story/src/lib.rs index e6887c36..3b1c557b 100644 --- a/crates/story/src/lib.rs +++ b/crates/story/src/lib.rs @@ -16,6 +16,8 @@ mod text_story; mod tooltip_story; mod webview_story; +use std::sync::Arc; + pub use button_story::ButtonStory; pub use calendar_story::CalendarStory; pub use dropdown_story::DropdownStory; @@ -35,18 +37,19 @@ pub use tooltip_story::TooltipStory; pub use webview_story::WebViewStory; use gpui::{ - div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, Div, EventEmitter, - FocusableView, InteractiveElement, IntoElement, ParentElement, Pixels, Render, SharedString, + div, prelude::FluentBuilder as _, px, AnyView, AppContext, Div, EventEmitter, FocusableView, + InteractiveElement, IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled as _, Task, View, ViewContext, VisualContext, WindowContext, }; -use workspace::{ - dock::{DockPosition, Panel, PanelEvent}, - item::{Item, ItemEvent}, - Workspace, WorkspaceId, -}; use anyhow::Result; -use ui::{divider::Divider, h_flex, label::Label, v_flex}; +use ui::{ + divider::Divider, + dock::{Panel, PanelEvent, TabPanel}, + h_flex, + label::Label, + v_flex, +}; pub fn init(cx: &mut AppContext) { input_story::init(cx); @@ -75,10 +78,8 @@ pub struct StoryContainer { focus_handle: gpui::FocusHandle, name: SharedString, description: SharedString, - position: DockPosition, width: Option, height: Option, - active: bool, story: Option, } @@ -93,43 +94,6 @@ pub enum ContainerEvent { Close, } -impl Item for StoryContainer { - type Event = ContainerEvent; - - fn tab_content( - &self, - _params: workspace::item::TabContentParams, - _cx: &WindowContext, - ) -> AnyElement { - Label::new(self.name.clone()).into_any_element() - } - - fn deactivated(&mut self, _cx: &mut ViewContext) { - self.active = false; - } - - fn workspace_deactivated(&mut self, _cx: &mut ViewContext) { - self.active = false; - } - - fn clone_on_split( - &self, - _: Option, - cx: &mut ViewContext, - ) -> Option> { - Some(cx.new_view(|cx| { - Self::new(self.name.clone(), self.description.clone(), cx) - .story(self.story.clone().unwrap()) - })) - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { - match event { - ContainerEvent::Close => f(ItemEvent::CloseItem), - } - } -} - impl EventEmitter for StoryContainer {} impl StoryContainer { @@ -146,8 +110,6 @@ impl StoryContainer { description: description.into(), width: None, height: None, - position: DockPosition::Left, - active: false, story: None, } } @@ -156,36 +118,21 @@ impl StoryContainer { name: impl Into, description: impl Into, story: AnyView, - workspace: View, + tab_panel: View, cx: &mut WindowContext, ) -> Task>> { - let pane = workspace.read(cx).active_pane().clone(); let name = name.into(); let description = description.into(); cx.spawn(|mut cx| async move { - pane.update(&mut cx, |pane, cx| { + tab_panel.update(&mut cx, |panel, cx| { let view = cx.new_view(|cx| Self::new(name, description, cx).story(story)); - - pane.add_item(Box::new(view.clone()), true, true, None, cx); + panel.add_panel(Arc::new(view.clone()), cx); view }) }) } - pub fn add_panel( - story: AnyView, - workspace: View, - position: DockPosition, - size: gpui::Pixels, - cx: &mut WindowContext, - ) { - workspace.update(cx, |workspace, cx| { - let panel = cx.new_view(|cx| MyPanel::new(story, position, size, cx)); - workspace.add_panel(panel, cx) - }); - } - pub fn width(mut self, width: gpui::Pixels) -> Self { self.width = Some(width); self @@ -196,17 +143,19 @@ impl StoryContainer { self } - pub fn position(mut self, position: DockPosition) -> Self { - self.position = position; - self - } - pub fn story(mut self, story: AnyView) -> Self { self.story = Some(story); self } } +impl Panel for StoryContainer { + fn title(&self, _cx: &WindowContext) -> SharedString { + self.name.clone() + } +} + +impl EventEmitter for StoryContainer {} impl Render for StoryContainer { fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { v_flex() @@ -219,7 +168,6 @@ impl Render for StoryContainer { .flex_col() .gap_4() .p_4() - .child(Label::new(self.name.clone()).text_size(px(24.0))) .child(Label::new(self.description.clone()).text_size(px(16.0))) .child(Divider::horizontal().label("This is a divider")), ) @@ -235,82 +183,3 @@ impl Render for StoryContainer { }) } } - -struct MyPanel { - focus_handle: gpui::FocusHandle, - view: AnyView, - _position: DockPosition, - width: Option, - height: Option, -} - -impl MyPanel { - fn new(view: AnyView, position: DockPosition, size: Pixels, cx: &mut WindowContext) -> Self { - let mut this = Self { - focus_handle: cx.focus_handle(), - view, - _position: position, - width: None, - height: None, - }; - this.update_size(size); - this - } - - fn update_size(&mut self, size: Pixels) { - match self._position { - DockPosition::Bottom => self.height = Some(size), - DockPosition::Left | DockPosition::Right => self.width = Some(size), - } - } -} - -impl Panel for MyPanel { - fn persistent_name() -> &'static str { - "my-panel" - } - - fn can_position(&self, position: DockPosition) -> bool { - match self._position { - DockPosition::Bottom => matches!(position, DockPosition::Bottom), - DockPosition::Left | DockPosition::Right => { - matches!(position, DockPosition::Left | DockPosition::Right) - } - } - } - - fn position(&self, _cx: &WindowContext) -> DockPosition { - self._position - } - - fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { - self._position = position; - cx.notify() - } - - fn size(&self, _cx: &WindowContext) -> gpui::Pixels { - match self._position { - DockPosition::Bottom => self.height.unwrap_or(px(100.)), - DockPosition::Left | DockPosition::Right => self.width.unwrap_or(px(100.)), - } - } - - fn set_size(&mut self, size: Option, cx: &mut ViewContext) { - if let Some(size) = size { - self.update_size(size) - } - cx.notify(); - } -} - -impl EventEmitter for MyPanel {} -impl FocusableView for MyPanel { - fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} -impl Render for MyPanel { - fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { - div().id("my-panel").size_full().child(self.view.clone()) - } -} diff --git a/crates/story/src/resizable_story.rs b/crates/story/src/resizable_story.rs index c622fb20..5506c578 100644 --- a/crates/story/src/resizable_story.rs +++ b/crates/story/src/resizable_story.rs @@ -52,7 +52,6 @@ impl ResizableStory { resizable_panel() .size(px(300.)) .min_size(px(100.)) - .grow() .content(|cx| panel_box("Right (Grow)", cx)), cx, ), @@ -63,7 +62,6 @@ impl ResizableStory { .size(px(150.)) .max_size(px(550.)) .min_size(px(100.)) - .grow() .content(|cx| panel_box("Center (Grow)", cx)), cx, ) @@ -90,7 +88,6 @@ impl ResizableStory { .size(px(400.)) .max_size(px(550.)) .min_size(px(100.)) - .grow() .content(|cx| panel_box("Right (Grow)", cx)), cx, ) diff --git a/crates/ui/locales/ui.yml b/crates/ui/locales/ui.yml index 4fca3e2d..5e032477 100644 --- a/crates/ui/locales/ui.yml +++ b/crates/ui/locales/ui.yml @@ -86,3 +86,16 @@ Dropdown: en: "Please select" zh-CN: "请选择" zh-HK: "請選擇" +Dock: + Unnamed: + en: Unnamed + zh-CN: 未命名 + zh-HK: 未命名 + Zoom In: + en: Zoom In + zh-CN: 放大 + zh-HK: 放大 + Zoom Out: + en: Zoom Out + zh-CN: 缩小 + zh-HK: 縮小 diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index 2a915178..1e011e4d 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -5,9 +5,9 @@ use crate::{ Disableable, Icon, Selectable, Sizable, Size, }; use gpui::{ - div, prelude::FluentBuilder as _, px, AnyElement, ClickEvent, Div, ElementId, FocusHandle, - Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce, - SharedString, StatefulInteractiveElement as _, Styled, WindowContext, + div, prelude::FluentBuilder as _, px, relative, AnyElement, ClickEvent, Div, ElementId, + FocusHandle, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, + RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, WindowContext, }; pub enum ButtonRounded { @@ -372,6 +372,7 @@ impl RenderOnce for Button { Size::Small => this.text_sm(), _ => this.text_base(), }) + .line_height(relative(1.)) .when(!self.loading, |this| { this.when_some(self.icon, |this, icon| { this.child(icon.with_size(icon_size)) @@ -457,7 +458,7 @@ impl ButtonStyle { ButtonStyle::Primary => cx.theme().primary_hover, ButtonStyle::Secondary | ButtonStyle::Outline => cx.theme().secondary_hover, ButtonStyle::Danger => cx.theme().destructive_hover, - ButtonStyle::Ghost => cx.theme().secondary, + ButtonStyle::Ghost => cx.theme().secondary_hover, ButtonStyle::Link => cx.theme().transparent, ButtonStyle::Text => cx.theme().transparent, ButtonStyle::Custom(colors) => colors.hover, diff --git a/crates/ui/src/dock/mod.rs b/crates/ui/src/dock/mod.rs new file mode 100644 index 00000000..bbec7efd --- /dev/null +++ b/crates/ui/src/dock/mod.rs @@ -0,0 +1,62 @@ +mod panel; +mod stack_panel; +mod tab_panel; + +use gpui::{ + actions, div, prelude::FluentBuilder, AnyView, InteractiveElement as _, IntoElement, + ParentElement as _, Render, Styled, View, ViewContext, +}; +pub use panel::*; +pub use stack_panel::*; +pub use tab_panel::*; + +use crate::theme::ActiveTheme; + +actions!(dock, [ToggleZoom]); + +/// The main area of the dock. +pub struct DockArea { + root: View, + zoom_view: Option, +} + +impl DockArea { + pub fn new(root: View, _cx: &mut ViewContext) -> Self { + Self { + root, + zoom_view: None, + } + } + + /// Toggles the zoom view. + pub fn toggle_zoom(&mut self, panel: View

, cx: &mut ViewContext) { + if self.zoom_view.is_some() { + self.zoom_view = None; + } else { + self.zoom_view = Some(panel.into()); + } + cx.notify(); + } +} + +impl Render for DockArea { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .id("dock-area") + .flex() + .flex_grow() + .flex_shrink() + .overflow_hidden() + .map(|this| match self.zoom_view.clone() { + Some(view) => this.bg(cx.theme().tab_bar).p_3().child( + div() + .size_full() + .border_1() + .border_color(cx.theme().border) + .shadow_lg() + .child(view), + ), + None => this.child(self.root.clone()), + }) + } +} diff --git a/crates/ui/src/dock/panel.rs b/crates/ui/src/dock/panel.rs new file mode 100644 index 00000000..95ea1669 --- /dev/null +++ b/crates/ui/src/dock/panel.rs @@ -0,0 +1,53 @@ +use gpui::{AnyView, EventEmitter, FocusableView, SharedString, View, WindowContext}; +use rust_i18n::t; + +use super::PanelEvent; + +pub trait Panel: EventEmitter + FocusableView { + /// The title of the panel, default is `None`. + fn title(&self, _cx: &WindowContext) -> SharedString { + t!("Dock.Unnamed").into() + } + + /// Whether the panel can be closed, default is `true`. + fn closeable(&self, _cx: &WindowContext) -> bool { + true + } +} + +pub trait PanelView: 'static + Send + Sync { + /// The title of the panel, default is `None`. + fn title(&self, _cx: &WindowContext) -> SharedString { + t!("Dock.Unnamed").into() + } + + fn view(&self) -> AnyView; +} + +impl PanelView for View { + fn title(&self, cx: &WindowContext) -> SharedString { + self.read(cx).title(cx) + } + + fn view(&self) -> AnyView { + self.clone().into() + } +} + +impl From<&dyn PanelView> for AnyView { + fn from(handle: &dyn PanelView) -> Self { + handle.view() + } +} + +impl From<&dyn PanelView> for View { + fn from(value: &dyn PanelView) -> Self { + value.view().downcast::().unwrap() + } +} + +impl PartialEq for dyn PanelView { + fn eq(&self, other: &Self) -> bool { + self.view() == other.view() + } +} diff --git a/crates/ui/src/dock/stack_panel.rs b/crates/ui/src/dock/stack_panel.rs new file mode 100644 index 00000000..6d80364c --- /dev/null +++ b/crates/ui/src/dock/stack_panel.rs @@ -0,0 +1,272 @@ +use std::sync::Arc; + +use crate::{ + resizable::{h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelGroup}, + theme::ActiveTheme, + Placement, +}; + +use super::{DockArea, Panel, PanelEvent, PanelView, TabPanel}; +use gpui::{ + div, prelude::FluentBuilder as _, px, Axis, DismissEvent, Entity, EventEmitter, FocusHandle, + FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, View, ViewContext, + VisualContext, WeakView, +}; +use smallvec::SmallVec; + +pub struct StackPanel { + pub(super) parent: Option>, + pub(super) axis: Axis, + focus_handle: FocusHandle, + panels: SmallVec<[Arc; 2]>, + panel_group: View, +} + +impl Panel for StackPanel {} + +impl StackPanel { + pub fn new(axis: Axis, cx: &mut ViewContext) -> Self { + Self { + axis, + parent: None, + focus_handle: cx.focus_handle(), + panels: SmallVec::new(), + panel_group: cx.new_view(|_| { + if axis == Axis::Horizontal { + h_resizable() + } else { + v_resizable() + } + }), + } + } + + /// The first level of the stack panel is root, will not have a parent. + fn is_root(&self) -> bool { + self.parent.is_none() + } + + pub(super) fn panels_len(&self) -> usize { + self.panels.len() + } + + /// Return the index of the panel. + pub fn index_of_panel

(&self, panel: View

) -> Option + where + P: Panel, + { + let entity_id = panel.entity_id(); + self.panels + .iter() + .position(|p| p.view().entity_id() == entity_id) + } + + /// Add a panel at the end of the stack. + pub fn add_panel

( + &mut self, + panel: View

, + size: Option, + dock_area: WeakView, + cx: &mut ViewContext, + ) where + P: Panel, + { + self.insert_panel(panel, self.panels.len(), size, dock_area, cx); + } + + pub fn add_panel_at

( + &mut self, + panel: View

, + ix: usize, + placement: Placement, + dock_area: WeakView, + cx: &mut ViewContext, + ) where + P: Panel, + { + match placement { + Placement::Top | Placement::Left => self.insert_panel_before(panel, ix, dock_area, cx), + Placement::Right | Placement::Bottom => { + self.insert_panel_after(panel, ix, dock_area, cx) + } + } + } + + /// Insert a panel at the index. + pub fn insert_panel_before

( + &mut self, + panel: View

, + ix: usize, + dock_area: WeakView, + cx: &mut ViewContext, + ) where + P: Panel, + { + self.insert_panel(panel, ix, None, dock_area, cx); + } + + /// Insert a panel after the index. + pub fn insert_panel_after

( + &mut self, + panel: View

, + ix: usize, + dock_area: WeakView, + cx: &mut ViewContext, + ) where + P: Panel, + { + self.insert_panel(panel, ix + 1, None, dock_area, cx); + } + + fn new_resizable_panel

(panel: View

, size: Option) -> ResizablePanel + where + P: Panel, + { + resizable_panel() + .content_view(panel.view()) + .min_size(px(100.)) + .when_some(size, |this, size| this.size(size)) + } + + fn insert_panel

( + &mut self, + panel: View

, + ix: usize, + size: Option, + dock_area: WeakView, + cx: &mut ViewContext, + ) where + P: Panel, + { + // If the panel is already in the stack, return. + if let Some(_) = self.index_of_panel(panel.clone()) { + return; + } + + let dock_area = dock_area.clone(); + cx.subscribe(&panel, move |_, panel, event, cx| match event { + PanelEvent::ZoomIn | PanelEvent::ZoomOut => { + if let Some(dock) = dock_area.upgrade() { + dock.update(cx, |dock, cx| { + dock.toggle_zoom(panel.clone(), cx); + }); + } + } + }) + .detach(); + + cx.spawn(|view, mut cx| { + let panel = panel.clone(); + async move { + if let Some(view) = view.upgrade() { + cx.update(|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.clone())); + } else if let Ok(stack_panel) = panel.view().downcast::() { + stack_panel.update(cx, |stack_panel, _| { + stack_panel.parent = Some(view.clone()) + }); + } + }) + } else { + Ok(()) + } + } + }) + .detach(); + + self.panels.insert(ix, Arc::new(panel.clone())); + self.panel_group.update(cx, |view, cx| { + view.insert_child(Self::new_resizable_panel(panel, size), ix, cx) + }); + + cx.notify(); + } + + /// Remove panel from the stack. + pub fn remove_panel

(&mut self, panel: View

, cx: &mut ViewContext) + where + P: Panel, + { + if let Some(ix) = self.index_of_panel(panel) { + self.panels.remove(ix); + self.panel_group.update(cx, |view, cx| { + view.remove_child(ix, cx); + }); + + self.remove_self_if_empty(cx); + } else { + println!("Panel not found in stack panel."); + } + } + + /// Replace the old panel with the new panel at same index. + pub(super) fn replace_panel

( + &mut self, + old_panel: View

, + new_panel: View, + cx: &mut ViewContext, + ) where + P: Panel, + { + if let Some(ix) = self.index_of_panel(old_panel) { + self.panels[ix] = Arc::new(new_panel.clone()); + self.panel_group.update(cx, |view, cx| { + view.replace_child(Self::new_resizable_panel(new_panel.clone(), None), ix, cx); + }); + } + } + + /// If children is empty, remove self from parent view. + pub(crate) fn remove_self_if_empty(&mut self, cx: &mut ViewContext) { + if self.is_root() { + return; + } + + if !self.panels.is_empty() { + return; + } + + let view = cx.view().clone(); + if let Some(parent) = self.parent.as_ref() { + parent.update(cx, |parent, cx| { + parent.remove_panel(view, cx); + }); + } + + cx.notify(); + } + + /// Remove all panels from the stack. + pub(super) fn remove_all_panels(&mut self, cx: &mut ViewContext) { + self.panels.clear(); + self.panel_group + .update(cx, |view, cx| view.remove_all_children(cx)); + } + + /// Change the axis of the stack panel. + pub(super) fn set_axis(&mut self, axis: Axis, cx: &mut ViewContext) { + self.axis = axis; + self.panel_group + .update(cx, |view, cx| view.set_axis(axis, cx)); + cx.notify(); + } +} + +impl FocusableView for StackPanel { + fn focus_handle(&self, _cx: &gpui::AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} +impl EventEmitter for StackPanel {} +impl EventEmitter for StackPanel {} +impl Render for StackPanel { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .size_full() + .overflow_hidden() + .bg(cx.theme().tab_bar) + .child(self.panel_group.clone()) + } +} diff --git a/crates/ui/src/dock/tab_panel.rs b/crates/ui/src/dock/tab_panel.rs new file mode 100644 index 00000000..6519a4a9 --- /dev/null +++ b/crates/ui/src/dock/tab_panel.rs @@ -0,0 +1,502 @@ +use std::sync::Arc; + +use gpui::{ + div, prelude::FluentBuilder, rems, AnchorCorner, AppContext, DefiniteLength, DismissEvent, + DragMoveEvent, Empty, EventEmitter, FocusHandle, FocusableView, InteractiveElement as _, + IntoElement, ParentElement, Render, ScrollHandle, StatefulInteractiveElement, Styled, View, + ViewContext, VisualContext as _, WeakView, +}; +use rust_i18n::t; + +use crate::{ + button::Button, + h_flex, + popup_menu::PopupMenuExt, + tab::{Tab, TabBar}, + theme::ActiveTheme, + v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt, +}; + +use super::{DockArea, Panel, PanelView, StackPanel, ToggleZoom}; + +pub enum PanelEvent { + ZoomIn, + ZoomOut, +} + +#[derive(Clone)] +pub(crate) struct DragPanel { + pub(crate) panel: Arc, + pub(crate) tab_panel: View, +} + +impl DragPanel { + pub(crate) fn new(panel: Arc, tab_panel: View) -> Self { + Self { panel, tab_panel } + } +} + +impl Render for DragPanel { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .cursor_grab() + .py_1() + .px_3() + .w_24() + .overflow_hidden() + .whitespace_nowrap() + .border_1() + .border_color(cx.theme().border) + .rounded_md() + .bg(cx.theme().tab_active) + .opacity(0.75) + .child(self.panel.title(cx)) + } +} + +pub struct TabPanel { + focus_handle: FocusHandle, + dock_area: WeakView, + stack_panel: Option>, + panels: Vec>, + active_ix: usize, + tab_bar_scroll_handle: ScrollHandle, + + is_zoomed: bool, + + /// When drag move, will get the placement of the panel to be split + will_split_placement: Option, +} + +impl TabPanel { + pub fn new(dock_area: WeakView, cx: &mut ViewContext) -> Self { + Self { + focus_handle: cx.focus_handle(), + dock_area, + stack_panel: None, + panels: Vec::new(), + active_ix: 0, + tab_bar_scroll_handle: ScrollHandle::new(), + will_split_placement: None, + is_zoomed: false, + } + } + + pub(super) fn set_parent(&mut self, parent: View) { + self.stack_panel = Some(parent); + } + + /// Return current active_panel View + pub fn active_panel(&self) -> Option> { + self.panels.get(self.active_ix).cloned() + } + + fn set_active_ix(&mut self, ix: usize, cx: &mut ViewContext) { + self.active_ix = ix; + self.tab_bar_scroll_handle.scroll_to_item(ix); + cx.notify(); + } + + /// Add a panel to the end of the tabs + pub fn add_panel(&mut self, panel: Arc, cx: &mut ViewContext) { + if self + .panels + .iter() + .any(|p| p.view().entity_id() == panel.view().entity_id()) + { + return; + } + + self.panels.push(panel); + // set the active panel to the new panel + self.set_active_ix(self.panels.len() - 1, cx); + cx.notify(); + } + + fn insert_panel_at( + &mut self, + panel: Arc, + ix: usize, + cx: &mut ViewContext, + ) { + if self + .panels + .iter() + .any(|p| p.view().entity_id() == panel.view().entity_id()) + { + return; + } + + self.panels.insert(ix, panel); + self.set_active_ix(ix, cx); + cx.notify(); + } + + /// Remove a panel from the tab panel + pub fn remove_panel(&mut self, panel: Arc, cx: &mut ViewContext) { + self.detach_panel(panel, cx); + self.remove_self_if_empty(cx) + } + + fn detach_panel(&mut self, panel: Arc, cx: &mut ViewContext) { + let panel_view = panel.view(); + self.panels.retain(|p| p.view() != panel_view); + if self.active_ix >= self.panels.len() { + self.set_active_ix(self.panels.len().saturating_sub(1), cx) + } + } + + /// Check to remove self from the parent StackPanel, if there is no panel left + fn remove_self_if_empty(&self, cx: &mut ViewContext) { + if !self.panels.is_empty() { + return; + } + + let tab_view = cx.view().clone(); + if let Some(stack_panel) = self.stack_panel.as_ref() { + stack_panel.update(cx, |view, cx| { + view.remove_panel(tab_view, cx); + }) + } + } + + fn render_menu_button(&self, cx: &mut ViewContext) -> impl IntoElement { + let is_zoomed = self.is_zoomed; + + h_flex() + .gap_2() + .occlude() + .items_center() + .when(self.is_zoomed, |this| { + this.child( + Button::new("zoom", cx) + .icon(IconName::Minimize) + .xsmall() + .ghost() + .on_click( + cx.listener(|view, _, cx| view.on_action_toggle_zoom(&ToggleZoom, cx)), + ), + ) + }) + .child( + Button::new("menu", cx) + .icon(IconName::Ellipsis) + .xsmall() + .ghost() + .popup_menu(move |this, _| { + this.menu( + if is_zoomed { + t!("Dock.Zoom Out") + } else { + t!("Dock.Zoom In") + }, + Box::new(ToggleZoom), + ) + }) + .anchor(AnchorCorner::TopRight), + ) + } + + fn render_tabs(&self, cx: &mut ViewContext) -> impl IntoElement { + let view = cx.view().clone(); + + if self.panels.len() == 1 { + let panel = self.panels.get(0).unwrap(); + + return h_flex() + .id("tab") + .justify_between() + .items_center() + .py_2() + .px_3() + .line_height(rems(1.0)) + .child(panel.title(cx)) + .child(self.render_menu_button(cx)) + .on_drag( + DragPanel { + panel: panel.clone(), + tab_panel: view, + }, + |drag, cx| { + cx.stop_propagation(); + cx.new_view(|_| drag.clone()) + }, + ) + .into_any_element(); + } + + let tabs_count = self.panels.len(); + + TabBar::new("tab-bar") + .track_scroll(self.tab_bar_scroll_handle.clone()) + .children(self.panels.iter().enumerate().map(|(ix, panel)| { + let active = ix == self.active_ix; + Tab::new(("tab", ix), panel.title(cx)) + .selected(active) + .on_click(cx.listener(move |view, _, cx| { + view.set_active_ix(ix, cx); + })) + .on_drag(DragPanel::new(panel.clone(), view.clone()), |drag, cx| { + cx.stop_propagation(); + cx.new_view(|_| drag.clone()) + }) + .drag_over::(|this, _, cx| { + this.rounded_l_none() + .border_l_2() + .border_r_0() + .border_color(cx.theme().drag_border) + }) + .on_drop(cx.listener(move |this, drag: &DragPanel, cx| { + this.will_split_placement = None; + this.on_drop(drag, Some(ix), cx) + })) + })) + .child( + // empty space to allow move to last tab right + div() + .id("tab-bar-empty-space") + .h_full() + .flex_grow() + .min_w_16() + .drag_over::(|this, _, cx| this.bg(cx.theme().drop_target)) + .on_drop(cx.listener(move |this, drag: &DragPanel, cx| { + this.will_split_placement = None; + + let ix = if drag.tab_panel == view { + Some(tabs_count - 1) + } else { + None + }; + + this.on_drop(drag, ix, cx) + })), + ) + .suffix( + h_flex() + .items_center() + .top_0() + .right_0() + .border_l_1() + .h_full() + .border_color(cx.theme().border) + .bg(cx.theme().tab_bar) + .px_3() + .child(self.render_menu_button(cx)), + ) + .into_any_element() + } + + fn render_active_panel(&self, cx: &mut ViewContext) -> impl IntoElement { + self.active_panel() + .map(|panel| { + div() + .id("tab-content") + .group("") + .overflow_y_scroll() + .flex_1() + .child(panel.view()) + .on_drag_move(cx.listener(Self::on_panel_drag_move)) + .child( + div() + .invisible() + .absolute() + .bg(cx.theme().drop_target) + .map(|this| match self.will_split_placement { + Some(placement) => { + let size = DefiniteLength::Fraction(0.25); + match placement { + Placement::Left => this.left_0().top_0().bottom_0().w(size), + Placement::Right => { + this.right_0().top_0().bottom_0().w(size) + } + Placement::Top => this.top_0().left_0().right_0().h(size), + Placement::Bottom => { + this.bottom_0().left_0().right_0().h(size) + } + } + } + None => this.top_0().left_0().size_full(), + }) + .group_drag_over::("", |this| this.visible()) + .on_drop(cx.listener(|this, drag: &DragPanel, cx| { + this.on_drop(drag, None, cx) + })), + ) + .into_any_element() + }) + .unwrap_or(Empty {}.into_any_element()) + } + + /// Calculate the split direction based on the current mouse position + fn on_panel_drag_move(&mut self, drag: &DragMoveEvent, cx: &mut ViewContext) { + let bounds = drag.bounds; + let position = drag.event.position; + + // Check the mouse position to determine the split direction + if position.x < bounds.left() + bounds.size.width * 0.25 { + self.will_split_placement = Some(Placement::Left); + } else if position.x > bounds.left() + bounds.size.width * 0.75 { + self.will_split_placement = Some(Placement::Right); + } else if position.y < bounds.top() + bounds.size.height * 0.25 { + self.will_split_placement = Some(Placement::Top); + } else if position.y > bounds.top() + bounds.size.height * 0.75 { + self.will_split_placement = Some(Placement::Bottom); + } else { + // center to merge into the current tab + self.will_split_placement = None; + } + cx.notify() + } + + fn on_drop(&mut self, drag: &DragPanel, ix: Option, cx: &mut ViewContext) { + let panel = drag.panel.clone(); + let is_same_tab = drag.tab_panel == *cx.view(); + + // If target is same tab, and it is only one panel, do nothing. + if is_same_tab && ix.is_none() { + if self.will_split_placement.is_none() { + return; + } else { + if self.panels.len() == 1 { + return; + } + } + } + + // Here is looks like remove_panel on a same item, but it differnece. + // + // We must to split it to remove_panel, unless it will be crash by error: + // Cannot update ui::dock::tab_panel::TabPanel while it is already being updated + if is_same_tab { + self.detach_panel(panel.clone(), cx); + } else { + let _ = drag.tab_panel.update(cx, |view, cx| { + view.detach_panel(panel.clone(), cx); + view.remove_self_if_empty(cx); + }); + } + + // Insert into new tabs + if let Some(placement) = self.will_split_placement { + self.split_panel(panel, placement, cx); + } else { + if let Some(ix) = ix { + self.insert_panel_at(panel, ix, cx) + } else { + self.add_panel(panel, cx) + } + } + + self.remove_self_if_empty(cx); + } + + /// Add panel with split placement + fn split_panel( + &self, + panel: Arc, + placement: Placement, + cx: &mut ViewContext, + ) { + let dock_area = self.dock_area.clone(); + // wrap the panel in a TabPanel + let new_tab_panel = cx.new_view(|cx| Self::new(dock_area.clone(), cx)); + new_tab_panel.update(cx, |view, cx| { + view.add_panel(panel, cx); + }); + + let stack_panel = self.stack_panel.as_ref().unwrap(); + let parent_axis = stack_panel.read(cx).axis; + let ix = stack_panel + .read(cx) + .index_of_panel(cx.view().clone()) + .unwrap_or_default(); + + if parent_axis.is_vertical() && placement.is_vertical() { + stack_panel.update(cx, |view, cx| { + view.add_panel_at(new_tab_panel, ix, placement, dock_area.clone(), cx); + }); + } else if parent_axis.is_horizontal() && placement.is_horizontal() { + stack_panel.update(cx, |view, cx| { + view.add_panel_at(new_tab_panel, ix, placement, dock_area.clone(), cx); + }); + } else { + // 1. Create new StackPanel with new axis + // 2. Move cx.view() from parent StackPanel to the new StackPanel + // 3. Add the new TabPanel to the new StackPanel at the correct index + // 4. Add new StackPanel to the parent StackPanel at the correct index + let tab_panel = cx.view().clone(); + + // Try to use the old stack panel, not just create a new one, to avoid too many nested stack panels + let new_stack_panel = if stack_panel.read(cx).panels_len() <= 1 { + stack_panel.update(cx, |view, cx| { + view.remove_all_panels(cx); + view.set_axis(placement.axis(), cx); + }); + stack_panel.clone() + } else { + cx.new_view(|cx| { + let mut panel = StackPanel::new(placement.axis(), cx); + panel.parent = Some(stack_panel.clone()); + panel + }) + }; + + new_stack_panel.update(cx, |view, cx| match placement { + Placement::Left | Placement::Top => { + view.add_panel(new_tab_panel, None, dock_area.clone(), cx); + view.add_panel(tab_panel.clone(), None, dock_area.clone(), cx); + } + Placement::Right | Placement::Bottom => { + view.add_panel(tab_panel.clone(), None, dock_area.clone(), cx); + view.add_panel(new_tab_panel, None, dock_area.clone(), cx); + } + }); + + if *stack_panel != new_stack_panel { + stack_panel.update(cx, |view, cx| { + view.replace_panel(tab_panel.clone(), new_stack_panel.clone(), cx); + }); + } + + cx.spawn(|_, mut cx| async move { + cx.update(|cx| tab_panel.update(cx, |view, cx| view.remove_self_if_empty(cx))) + }) + .detach() + } + } + + fn on_action_toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext) { + self.is_zoomed = !self.is_zoomed; + if self.is_zoomed { + cx.emit(PanelEvent::ZoomIn) + } else { + cx.emit(PanelEvent::ZoomOut) + } + } +} + +impl Panel for TabPanel {} +impl FocusableView for TabPanel { + fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle { + // FIXME: Delegate to the active panel + self.focus_handle.clone() + } +} +impl EventEmitter for TabPanel {} +impl EventEmitter for TabPanel {} +impl Render for TabPanel { + fn render(&mut self, cx: &mut ViewContext) -> impl gpui::IntoElement { + v_flex() + .id("tab-panel") + .track_focus(&self.focus_handle) + .on_action(cx.listener(Self::on_action_toggle_zoom)) + .size_full() + .flex_grow() + .flex_shrink() + .flex_none() + .overflow_hidden() + .bg(cx.theme().background) + .child(self.render_tabs(cx)) + .child(self.render_active_panel(cx)) + } +} diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index bb0241bc..84648918 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -12,6 +12,7 @@ pub mod checkbox; pub mod clipboard; pub mod context_menu; pub mod divider; +pub mod dock; pub mod drawer; pub mod dropdown; pub mod history; diff --git a/crates/ui/src/resizable/panel.rs b/crates/ui/src/resizable/panel.rs index 48e75a1d..29dc9a9c 100644 --- a/crates/ui/src/resizable/panel.rs +++ b/crates/ui/src/resizable/panel.rs @@ -1,13 +1,16 @@ use std::rc::Rc; use gpui::{ - canvas, div, prelude::FluentBuilder as _, px, AnyElement, AnyView, Axis, Bounds, DragMoveEvent, - EntityId, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, Render, - StatefulInteractiveElement, Styled, View, ViewContext, VisualContext as _, WindowContext, + canvas, deferred, div, prelude::FluentBuilder, px, AnyElement, AnyView, Axis, Bounds, + DragMoveEvent, EntityId, InteractiveElement as _, IntoElement, MouseButton, ParentElement, + Pixels, Render, StatefulInteractiveElement, Styled, View, ViewContext, VisualContext as _, + WindowContext, }; use crate::{h_flex, theme::ActiveTheme, v_flex, AxisExt}; +const HANDLE_PADDING: Pixels = px(4.); + #[derive(Clone, Render)] pub struct DragPanel(pub (EntityId, usize, Axis)); @@ -27,7 +30,7 @@ impl ResizablePanelGroup { axis: Axis::Horizontal, sizes: Vec::new(), panels: Vec::new(), - handle_size: px(3.), + handle_size: px(1.), size: px(20.), resizing_panel_ix: None, } @@ -39,6 +42,11 @@ impl ResizablePanelGroup { self } + pub(crate) fn set_axis(&mut self, axis: Axis, cx: &mut ViewContext) { + self.axis = axis; + cx.notify(); + } + /// Set the size of the resize handle, default is 3px. /// /// The handle size will inherit the parent group handle size, if you insert a group into another group. @@ -49,10 +57,7 @@ impl ResizablePanelGroup { /// Add a resizable panel to the group. pub fn child(mut self, panel: ResizablePanel, cx: &mut WindowContext) -> Self { - let mut panel = panel; - panel.axis = self.axis; - self.sizes.push(panel.size); - self.panels.push(cx.new_view(|_| panel)); + self.add_child(panel, cx); self } @@ -76,63 +81,133 @@ impl ResizablePanelGroup { self } + pub fn add_child(&mut self, panel: ResizablePanel, cx: &mut WindowContext) { + let mut panel = panel; + panel.axis = self.axis; + self.sizes.push(panel.size); + self.panels.push(cx.new_view(|_| panel)); + } + + pub fn insert_child(&mut self, panel: ResizablePanel, ix: usize, cx: &mut ViewContext) { + let mut panel = panel; + panel.axis = self.axis; + self.sizes.insert(ix, panel.size); + self.panels.insert(ix, cx.new_view(|_| panel)); + cx.notify() + } + + /// Replace a child panel with a new panel at the given index. + pub(crate) fn replace_child( + &mut self, + panel: ResizablePanel, + ix: usize, + cx: &mut ViewContext, + ) { + let mut panel = panel; + panel.axis = self.axis; + self.sizes[ix] = panel.size; + self.panels[ix] = cx.new_view(|_| panel); + cx.notify() + } + + pub fn remove_child(&mut self, ix: usize, cx: &mut ViewContext) { + self.sizes.remove(ix); + self.panels.remove(ix); + cx.notify() + } + + pub(crate) fn remove_all_children(&mut self, cx: &mut ViewContext) { + self.sizes.clear(); + self.panels.clear(); + cx.notify() + } + fn render_resize_handle(&self, ix: usize, cx: &mut ViewContext) -> impl IntoElement { let axis = self.axis; - let handle_size = self.handle_size; - let is_resizing = self.resizing_panel_ix == Some(ix); - div() - .id(("resizable-handle", ix)) - .occlude() - .hover(|this| this.bg(cx.theme().drag_border)) - .when(is_resizing, |this| this.bg(cx.theme().drag_border)) - .when(self.axis.is_horizontal(), |this| { - this.cursor_col_resize().top_0().h_full().w(handle_size) - }) - .when(self.axis.is_vertical(), |this| { - this.cursor_row_resize().left_0().w_full().h(handle_size) - }) - .on_drag_move(cx.listener( - move |view, e: &DragMoveEvent, cx| match e.drag(cx) { - DragPanel((entity_id, ix, axis)) => { - if cx.entity_id() != *entity_id { + let neg_offset = -HANDLE_PADDING; + + deferred( + div() + .id(("resizable-handle", ix)) + .occlude() + .absolute() + .flex_shrink_0() + .when(self.axis.is_horizontal(), |this| { + this.cursor_col_resize() + .top_0() + .right(neg_offset) + .h_full() + .w(px(0.)) + .px(HANDLE_PADDING) + }) + .when(self.axis.is_vertical(), |this| { + this.cursor_row_resize() + .bottom(neg_offset) + .left_0() + .w_full() + .h(px(0.)) + .py(HANDLE_PADDING) + }) + .child( + div() + .bg(cx.theme().border) + .when(self.axis.is_horizontal(), |this| { + this.h_full().w(self.handle_size) + }) + .when(self.axis.is_vertical(), |this| { + this.w_full().h(self.handle_size) + }), + ) + .on_drag_move(cx.listener(move |view, e: &DragMoveEvent, cx| { + match e.drag(cx) { + DragPanel((entity_id, ix, axis)) => { + if cx.entity_id() != *entity_id { + return; + } + + let ix = *ix; + view.resizing_panel_ix = Some(ix); + let panel = view + .panels + .get(ix) + .expect("BUG: invalid panel index") + .read(cx); + + view.sync_real_panel_sizes(cx); + match axis { + Axis::Horizontal => view.resize_panels( + ix, + e.event.position.x - panel.bounds.left(), + cx, + ), + Axis::Vertical => { + view.resize_panels( + ix, + e.event.position.y - panel.bounds.top(), + cx, + ); + } + } + } + } + })) + .on_mouse_up_out( + MouseButton::Left, + cx.listener(|view, _, _| { + if view.resizing_panel_ix.is_none() { return; } - let ix = *ix; - view.resizing_panel_ix = Some(ix); - let panel = view - .panels - .get(ix) - .expect("BUG: invalid panel index") - .read(cx); - - view.sync_real_panel_sizes(cx); - match axis { - Axis::Horizontal => { - view.resize_panels(ix, e.event.position.x - panel.bounds.left(), cx) - } - Axis::Vertical => { - view.resize_panels(ix, e.event.position.y - panel.bounds.top(), cx); - } - } - } - }, - )) - .on_mouse_up_out( - MouseButton::Left, - cx.listener(|view, _, _| { - if view.resizing_panel_ix.is_none() { - return; - } - - view.resizing_panel_ix = None; + view.resizing_panel_ix = None; + }), + ) + .on_drag(DragPanel((cx.entity_id(), ix, axis)), |drag_panel, cx| { + cx.stop_propagation(); + cx.new_view(|_| drag_panel.clone()) }), - ) - .on_drag(DragPanel((cx.entity_id(), ix, axis)), |drag_panel, cx| { - cx.stop_propagation(); - cx.new_view(|_| drag_panel.clone()) - }) + ) + .with_priority(0) } fn sync_real_panel_sizes(&mut self, cx: &WindowContext) { @@ -154,23 +229,35 @@ impl ResizablePanelGroup { } let size = size.floor(); + // 1. The `size` is the new size for the `ix` offset panel will be. + // 2. Limit `size` with the panel min and max size. + // 3. Get the `ix` panel changed size. + // 4. If the changed size is less than 1px, do nothing. + // 5. Update the next panel size with it old size minus the changed size. + // 6. When the old_size is small than the min_size, get the overflow size and then reduce other panels size with the overflow size. + let old_size = self.sizes[ix]; let new_size = self.panels[ix].read(cx).limit_size(size); if new_size < size { return; } - let changed_size = new_size - old_size; + let changed_size = (new_size - old_size).floor(); // If change size is less than 1px, do nothing. if changed_size > px(-1.0) && changed_size < px(1.0) { return; } - self.sizes[ix] = new_size; - let next_size = self.sizes[ix + 1]; - self.sizes[ix + 1] = self.panels[ix + 1] - .read(cx) - .limit_size(next_size - changed_size); + let next_size = self.sizes[ix + 1] - changed_size; + let next_new_size = self.panels[ix + 1].read(cx).limit_size(next_size); + let overflow_size = next_new_size - next_size; + if overflow_size != px(0.) { + return; + } + + self.sizes[ix] = new_size; + self.panels[ix].update(cx, |this, _| this.size = new_size); + self.sizes[ix + 1] = next_new_size; for (i, panel) in self.panels.iter_mut().enumerate() { let size = self.sizes[i]; @@ -181,21 +268,33 @@ impl ResizablePanelGroup { impl Render for ResizablePanelGroup { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let mut children: Vec = vec![]; - for (ix, panel) in self.panels.iter().enumerate() { - children.push(panel.clone().into_any_element()); - if ix < self.panels.len() - 1 { - children.push(self.render_resize_handle(ix, cx).into_any_element()); - } - } - let container = if self.axis.is_horizontal() { h_flex() } else { v_flex() }; - container.size_full().children(children) + container + .size_full() + .flex_grow() + .flex_shrink() + // .map(|this| { + // use crate::StyledExt as _; + // match self.axis { + // Axis::Horizontal => this.debug_red(), + // Axis::Vertical => this.debug_blue(), + // } + // }) + .children(self.panels.iter().enumerate().map(|(ix, panel)| { + if ix < self.panels.len() - 1 { + let handle = self.render_resize_handle(ix, cx); + panel.update(cx, |view, _| { + view.resize_handle = Some(handle.into_any_element()) + }); + } + + panel.clone() + })) } } @@ -208,8 +307,7 @@ pub struct ResizablePanel { content_view: Option, /// The bounds of the resizable panel, when render the bounds will be updated. bounds: Bounds, - - grow: bool, + resize_handle: Option, } impl ResizablePanel { @@ -222,7 +320,7 @@ impl ResizablePanel { content_builder: None, content_view: None, bounds: Bounds::default(), - grow: false, + resize_handle: None, } } @@ -269,23 +367,20 @@ impl ResizablePanel { size } - - /// Set the panel to grow to fill the remaining space. - pub fn grow(mut self) -> Self { - self.grow = true; - self - } } +impl FluentBuilder for ResizablePanel {} + impl Render for ResizablePanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let view = cx.view().clone(); let size = self.limit_size(self.size); div() - .size_full() .relative() - .when(self.grow, |this| this.flex_grow()) + .size_full() + .flex_grow() + .flex_shrink() .when(self.axis.is_vertical(), |this| this.h(size)) .when(self.axis.is_horizontal(), |this| this.w(size)) .child({ @@ -298,5 +393,6 @@ impl Render for ResizablePanel { }) .when_some(self.content_builder.clone(), |this, c| this.child(c(cx))) .when_some(self.content_view.clone(), |this, c| this.child(c)) + .when_some(self.resize_handle.take(), |this, c| this.child(c)) } } diff --git a/crates/ui/src/styled.rs b/crates/ui/src/styled.rs index 2be8c025..21ef7e12 100644 --- a/crates/ui/src/styled.rs +++ b/crates/ui/src/styled.rs @@ -1,3 +1,5 @@ +use std::fmt::{self, Display, Formatter}; + use crate::{ scroll::{Scrollable, ScrollbarAxis}, theme::{ActiveTheme, Colorize}, @@ -331,18 +333,36 @@ pub enum Placement { Right, } +impl Display for Placement { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Placement::Top => write!(f, "Top"), + Placement::Bottom => write!(f, "Bottom"), + Placement::Left => write!(f, "Left"), + Placement::Right => write!(f, "Right"), + } + } +} + impl Placement { pub fn is_horizontal(&self) -> bool { match self { - Placement::Top | Placement::Bottom => true, + Placement::Left | Placement::Right => true, _ => false, } } pub fn is_vertical(&self) -> bool { match self { - Placement::Left | Placement::Right => true, + Placement::Top | Placement::Bottom => true, _ => false, } } + + pub fn axis(&self) -> Axis { + match self { + Placement::Top | Placement::Bottom => Axis::Vertical, + Placement::Left | Placement::Right => Axis::Horizontal, + } + } } diff --git a/crates/ui/src/tab/tab.rs b/crates/ui/src/tab/tab.rs index 3ed191d4..a1794d75 100644 --- a/crates/ui/src/tab/tab.rs +++ b/crates/ui/src/tab/tab.rs @@ -17,10 +17,10 @@ pub struct Tab { } impl Tab { - pub fn new(id: impl Into, label: impl Into) -> Self { + pub fn new(id: impl Into, label: impl IntoElement) -> Self { Self { base: div().id(id.into()).gap_1().py_1p5().px_3().h_8(), - label: label.into(), + label: label.into_any_element(), disabled: false, selected: false, prefix: None, diff --git a/crates/ui/src/tab/tab_bar.rs b/crates/ui/src/tab/tab_bar.rs index 0414e8b7..be6ae6fc 100644 --- a/crates/ui/src/tab/tab_bar.rs +++ b/crates/ui/src/tab/tab_bar.rs @@ -2,7 +2,7 @@ use crate::h_flex; use crate::theme::ActiveTheme; use gpui::prelude::FluentBuilder as _; use gpui::{ - div, AnyElement, Div, IntoElement, ParentElement, RenderOnce, ScrollHandle, SharedString, + div, AnyElement, Div, ElementId, IntoElement, ParentElement, RenderOnce, ScrollHandle, StatefulInteractiveElement as _, Styled, WindowContext, }; use gpui::{px, InteractiveElement}; @@ -11,7 +11,7 @@ use smallvec::SmallVec; #[derive(IntoElement)] pub struct TabBar { base: Div, - id: SharedString, + id: ElementId, scroll_handle: ScrollHandle, prefix: Option, suffix: Option, @@ -19,7 +19,7 @@ pub struct TabBar { } impl TabBar { - pub fn new(id: impl Into) -> Self { + pub fn new(id: impl Into) -> Self { Self { base: div().h_8().px(px(-1.)), id: id.into(), @@ -37,14 +37,14 @@ impl TabBar { } /// Set the prefix element of the TabBar - pub fn prefix(mut self, prefix: impl Into) -> Self { - self.prefix = Some(prefix.into()); + pub fn prefix(mut self, prefix: impl IntoElement) -> Self { + self.prefix = Some(prefix.into_any_element()); self } /// Set the suffix element of the TabBar - pub fn suffix(mut self, suffix: impl Into) -> Self { - self.suffix = Some(suffix.into()); + pub fn suffix(mut self, suffix: impl IntoElement) -> Self { + self.suffix = Some(suffix.into_any_element()); self } } @@ -71,9 +71,9 @@ impl RenderOnce for TabBar { .flex() .flex_none() .items_center() - .bg(theme.tab_bar) .border_b_1() .border_color(cx.theme().border) + .bg(theme.tab_bar) .text_color(theme.tab_foreground) .when_some(self.prefix, |this, prefix| this.child(prefix)) // The child will append to this level diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index 26d1d257..539c4294 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -179,9 +179,9 @@ impl Colors { primary_active: hsl(223.0, 5.9, 45.0), primary_foreground: hsl(223.0, 0.0, 98.0), secondary: hsl(240.0, 4.8, 95.9), - secondary_hover: hsl(240.0, 4.8, 99.), - secondary_active: hsl(240.0, 5.9, 94.0), - secondary_foreground: hsl(240.0, 59.0, 10.0), + secondary_hover: hsl(240.0, 5.8, 10.).opacity(0.05), + secondary_active: hsl(240.0, 5.9, 10.).opacity(0.1), + secondary_foreground: hsl(240.0, 59.0, 10.), destructive: hsl(0.0, 84.2, 60.2), destructive_hover: hsl(0.0, 84.2, 65.0), destructive_active: hsl(0.0, 84.2, 47.0), @@ -220,8 +220,8 @@ impl Colors { primary_active: hsl(223.0, 0.0, 60.0), primary_foreground: hsl(223.0, 5.9, 10.0), secondary: hsl(240.0, 3.7, 15.9), - secondary_hover: hsl(240.0, 3.7, 20.9), - secondary_active: hsl(240.0, 3.7, 8.9), + secondary_hover: hsl(240.0, 3.7, 20.9).opacity(0.5), + secondary_active: hsl(240.0, 3.7, 20.9).opacity(0.8), secondary_foreground: hsl(0.0, 0.0, 98.0), destructive: hsl(0.0, 62.8, 30.6), destructive_hover: hsl(0.0, 62.8, 35.6), @@ -365,7 +365,7 @@ impl From for Theme { panel: colors.panel, selection: colors.selection, drag_border: crate::blue_500(), - drop_target: hsl(240.0, 65., 44.0).opacity(0.15), + drop_target: hsl(220.0, 65., 44.0).opacity(0.15), tab_bar: colors.tab_bar, tab: gpui::transparent_black(), tab_active: colors.background,