diff --git a/Cargo.lock b/Cargo.lock index 0d95a04c..9cfa5778 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2355,6 +2355,7 @@ dependencies = [ "anyhow", "gpui", "log", + "rand 0.8.5", "rust-embed", "serde", "serde_json", diff --git a/assets/icons/layout-dashboard.svg b/assets/icons/layout-dashboard.svg new file mode 100644 index 00000000..15cb8de8 --- /dev/null +++ b/assets/icons/layout-dashboard.svg @@ -0,0 +1,24 @@ + diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index dd4e4dd9..3f46c5d4 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -12,6 +12,7 @@ ui.workspace = true story.workspace = true serde.workspace = true serde_json.workspace = true +rand = "0.8" [lints] workspace = true diff --git a/crates/app/src/story_workspace.rs b/crates/app/src/story_workspace.rs index 4183e9f9..da25213a 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}, + dock::{DockArea, DockAreaState, DockEvent, DockItem, DockPlacement}, h_flex, popup_menu::PopupMenuExt, theme::{ActiveTheme, Theme}, @@ -31,7 +31,10 @@ struct SelectLocale(SharedString); #[derive(Clone, PartialEq, Eq, Deserialize)] struct SelectFont(usize); -impl_actions!(story, [SelectLocale, SelectFont]); +#[derive(Clone, PartialEq, Eq, Deserialize)] +struct AddPanel(DockPlacement); + +impl_actions!(story, [SelectLocale, SelectFont, AddPanel]); actions!(workspace, [Open, CloseWindow]); @@ -352,6 +355,35 @@ impl StoryWorkspace { Ok(window) }) } + + fn on_action_add_panel(&mut self, action: &AddPanel, cx: &mut ViewContext) { + // Random pick up a panel to add + let panel = match rand::random::() % 18 { + 0 => Arc::new(StoryContainer::panel::(cx)), + 1 => Arc::new(StoryContainer::panel::(cx)), + 2 => Arc::new(StoryContainer::panel::(cx)), + 3 => Arc::new(StoryContainer::panel::(cx)), + 4 => Arc::new(StoryContainer::panel::(cx)), + 5 => Arc::new(StoryContainer::panel::(cx)), + 6 => Arc::new(StoryContainer::panel::(cx)), + 7 => Arc::new(StoryContainer::panel::(cx)), + 8 => Arc::new(StoryContainer::panel::(cx)), + 9 => Arc::new(StoryContainer::panel::(cx)), + 10 => Arc::new(StoryContainer::panel::(cx)), + 11 => Arc::new(StoryContainer::panel::(cx)), + 12 => Arc::new(StoryContainer::panel::(cx)), + 13 => Arc::new(StoryContainer::panel::(cx)), + 14 => Arc::new(StoryContainer::panel::(cx)), + 15 => Arc::new(StoryContainer::panel::(cx)), + 16 => Arc::new(StoryContainer::panel::(cx)), + // 17 => Arc::new(StoryContainer::panel::(cx)), + _ => Arc::new(StoryContainer::panel::(cx)), + }; + + self.dock_area.update(cx, |dock_area, cx| { + dock_area.add_panel(panel, action.0, cx); + }); + } } pub fn open_new( @@ -377,6 +409,8 @@ impl Render for StoryWorkspace { let notifications_count = cx.notifications().len(); div() + .id("story-workspace") + .on_action(cx.listener(Self::on_action_add_panel)) .relative() .size_full() .flex() @@ -393,6 +427,32 @@ impl Render for StoryWorkspace { .px_2() .gap_2() .child(self.theme_color_picker.clone()) + .child( + Button::new("add-panel") + .icon(IconName::LayoutDashboard) + .small() + .ghost() + .popup_menu(|menu, _| { + menu.menu( + "Add Panel to Center", + Box::new(AddPanel(DockPlacement::Center)), + ) + .separator() + .menu( + "Add Panel to Left", + Box::new(AddPanel(DockPlacement::Left)), + ) + .menu( + "Add Panel to Right", + Box::new(AddPanel(DockPlacement::Right)), + ) + .menu( + "Add Panel to Bottom", + Box::new(AddPanel(DockPlacement::Bottom)), + ) + }) + .anchor(AnchorCorner::TopRight), + ) .child( Button::new("theme-mode") .map(|this| { @@ -414,7 +474,9 @@ impl Render for StoryWorkspace { .small() .ghost() .on_click(|_, cx| { - cx.open_url("https://github.com/huacnlee/gpui-component") + cx.open_url( + "https://github.com/longbridgeapp/gpui-component", + ) }), ) .child( diff --git a/crates/story/src/popup_story.rs b/crates/story/src/popup_story.rs index 7f7f3d3d..c79a4fd1 100644 --- a/crates/story/src/popup_story.rs +++ b/crates/story/src/popup_story.rs @@ -291,7 +291,7 @@ impl Render for PopupStory { menu.link_with_icon( "GitHub Repository", IconName::GitHub, - "https://github.com/huacnlee/gpui-component", + "https://github.com/longbridgeapp/gpui-component", ) .separator() .link("GPUI", "https://gpui.rs") diff --git a/crates/story/src/text_story.rs b/crates/story/src/text_story.rs index eb90530e..d00e9950 100644 --- a/crates/story/src/text_story.rs +++ b/crates/story/src/text_story.rs @@ -105,7 +105,7 @@ impl Render for TextStory { .child(Link::new("link3").child(h_flex().gap_1().child(IconName::GitHub).child("GitHub")).on_click(cx.listener(|_, _, cx| { cx.open_url("https://google.com") }))) - .child(div().w(px(250.)).child(Link::new("link4").child("https://github.com/huacnlee/gpui-component").href("https://github.com/huacnlee/gpui-component"))) + .child(div().w(px(250.)).child(Link::new("link4").child("https://github.com/longbridgeapp/gpui-component").href("https://github.com/longbridgeapp/gpui-component"))) ) ) .child( diff --git a/crates/ui/src/dock/dock.rs b/crates/ui/src/dock/dock.rs index 759c3b3e..c94f9475 100644 --- a/crates/ui/src/dock/dock.rs +++ b/crates/ui/src/dock/dock.rs @@ -1,5 +1,7 @@ //! Dock is a fixed container that places at left, bottom, right of the Windows. +use std::sync::Arc; + use gpui::{ div, prelude::FluentBuilder as _, px, Axis, Element, InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, @@ -14,13 +16,15 @@ use crate::{ AxisExt as _, StyledExt, }; -use super::{DockArea, DockItem, TabPanel}; +use super::{DockArea, DockItem, PanelView, TabPanel}; #[derive(Clone, Render)] struct ResizePanel; #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub enum DockPlacement { + #[serde(rename = "center")] + Center, #[serde(rename = "left")] Left, #[serde(rename = "bottom")] @@ -34,6 +38,7 @@ impl DockPlacement { match self { Self::Left | Self::Right => Axis::Horizontal, Self::Bottom => Axis::Vertical, + Self::Center => unreachable!(), } } @@ -204,6 +209,12 @@ impl Dock { cx.notify(); } + /// Add item to the Dock. + pub fn add_panel(&mut self, panel: Arc, cx: &mut ViewContext) { + self.panel.add_panel(panel, &self.dock_area, cx); + cx.notify(); + } + fn render_resize_handle(&mut self, cx: &mut ViewContext) -> impl IntoElement { let axis = self.placement.axis(); let neg_offset = -HANDLE_PADDING; @@ -270,6 +281,7 @@ impl Dock { DockPlacement::Left => mouse_position.x - area_bounds.left(), DockPlacement::Right => area_bounds.right() - mouse_position.x, DockPlacement::Bottom => area_bounds.bottom() - mouse_position.y, + DockPlacement::Center => unreachable!(), }; self.size = size.max(PANEL_MIN_SIZE); @@ -293,6 +305,7 @@ impl Render for Dock { .map(|this| match self.placement { DockPlacement::Left | DockPlacement::Right => this.h_flex().h_full().w(self.size), DockPlacement::Bottom => this.w_full().h(self.size), + DockPlacement::Center => unreachable!(), }) // Bottom Dock should keep the title bar, then user can click the Toggle button .when(!self.open && self.placement.is_bottom(), |this| { diff --git a/crates/ui/src/dock/mod.rs b/crates/ui/src/dock/mod.rs index 7d13d2b2..c5a96d52 100644 --- a/crates/ui/src/dock/mod.rs +++ b/crates/ui/src/dock/mod.rs @@ -202,6 +202,41 @@ impl DockItem { } } + /// Add a panel to the dock item. + pub fn add_panel( + &mut self, + panel: Arc, + dock_area: &WeakView, + cx: &mut WindowContext, + ) { + match self { + Self::Tabs { view, items, .. } => { + items.push(panel.clone()); + view.update(cx, |tab_panel, cx| { + tab_panel.add_panel(panel, cx); + }); + } + Self::Split { view, items, .. } => { + // Iter items to add panel to the first tabs + for item in items.into_iter() { + if let DockItem::Tabs { view, .. } = item { + view.update(cx, |tab_panel, cx| { + tab_panel.add_panel(panel.clone(), cx); + }); + return; + } + } + + // Unable to find tabs, create new tabs + let new_item = Self::tabs(vec![panel.clone()], None, dock_area, cx); + items.push(new_item.clone()); + view.update(cx, |stack_panel, cx| { + stack_panel.add_panel(new_item.view(), None, dock_area.clone(), cx); + }); + } + } + } + pub fn set_collapsed(&self, collapsed: bool, cx: &mut WindowContext) { match self { DockItem::Tabs { view, .. } => { @@ -364,6 +399,7 @@ impl DockArea { DockPlacement::Left => self.left_dock.is_some(), DockPlacement::Bottom => self.bottom_dock.is_some(), DockPlacement::Right => self.right_dock.is_some(), + DockPlacement::Center => false, } } @@ -385,6 +421,7 @@ impl DockArea { .as_ref() .map(|dock| dock.read(cx).is_open()) .unwrap_or(false), + DockPlacement::Center => false, } } @@ -393,7 +430,9 @@ impl DockArea { DockPlacement::Left => &self.left_dock, DockPlacement::Bottom => &self.bottom_dock, DockPlacement::Right => &self.right_dock, + DockPlacement::Center => return, }; + if let Some(dock) = dock { dock.update(cx, |view, cx| { view.toggle_open(cx); @@ -401,6 +440,59 @@ impl DockArea { } } + /// Add a panel item to the dock area at the given placement. + /// + /// If the left, bottom, right dock is not present, it will set the dock at the placement. + pub fn add_panel( + &mut self, + panel: Arc, + placement: DockPlacement, + cx: &mut ViewContext, + ) { + let weak_self = cx.view().downgrade(); + match placement { + DockPlacement::Left => { + if let Some(dock) = self.left_dock.as_ref() { + dock.update(cx, |dock, cx| dock.add_panel(panel, cx)) + } else { + self.set_left_dock( + DockItem::tabs(vec![panel], None, &weak_self, cx), + None, + true, + cx, + ); + } + } + DockPlacement::Bottom => { + if let Some(dock) = self.bottom_dock.as_ref() { + dock.update(cx, |dock, cx| dock.add_panel(panel, cx)) + } else { + self.set_bottom_dock( + DockItem::tabs(vec![panel], None, &weak_self, cx), + None, + true, + cx, + ); + } + } + DockPlacement::Right => { + if let Some(dock) = self.right_dock.as_ref() { + dock.update(cx, |dock, cx| dock.add_panel(panel, cx)) + } else { + self.set_right_dock( + DockItem::tabs(vec![panel], None, &weak_self, cx), + None, + true, + cx, + ); + } + } + DockPlacement::Center => { + self.items.add_panel(panel, &cx.view().downgrade(), cx); + } + } + } + /// Load the state of the DockArea from the DockAreaState. /// /// See also [DockeArea::dump]. diff --git a/crates/ui/src/dock/tab_panel.rs b/crates/ui/src/dock/tab_panel.rs index f89ecaee..ecbbf545 100644 --- a/crates/ui/src/dock/tab_panel.rs +++ b/crates/ui/src/dock/tab_panel.rs @@ -400,6 +400,7 @@ impl TabPanel { dock_area.bottom_dock.is_some() && toggle_button_panels.bottom == Some(view_entity_id) } + DockPlacement::Center => unreachable!(), } { return None; } @@ -428,6 +429,7 @@ impl TabPanel { IconName::PanelBottomOpen } } + DockPlacement::Center => unreachable!(), }; Some( diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index 36975b31..d096a655 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -36,6 +36,7 @@ pub enum IconName { HeartOff, Inbox, Info, + LayoutDashboard, Loader, LoaderCircle, Maximize, @@ -60,10 +61,10 @@ pub enum IconName { ThumbsDown, ThumbsUp, TriangleAlert, + WindowClose, WindowMaximize, WindowMinimize, WindowRestore, - WindowClose, } impl IconName { @@ -99,6 +100,7 @@ impl IconName { IconName::HeartOff => "icons/heart-off.svg", IconName::Inbox => "icons/inbox.svg", IconName::Info => "icons/info.svg", + IconName::LayoutDashboard => "icons/layout-dashboard.svg", IconName::Loader => "icons/loader.svg", IconName::LoaderCircle => "icons/loader-circle.svg", IconName::Maximize => "icons/maximize.svg", @@ -123,10 +125,10 @@ impl IconName { IconName::ThumbsDown => "icons/thumbs-down.svg", IconName::ThumbsUp => "icons/thumbs-up.svg", IconName::TriangleAlert => "icons/triangle-alert.svg", + IconName::WindowClose => "icons/window-close.svg", IconName::WindowMaximize => "icons/window-maximize.svg", IconName::WindowMinimize => "icons/window-minimize.svg", IconName::WindowRestore => "icons/window-restore.svg", - IconName::WindowClose => "icons/window-close.svg", } .into() }