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()
}