From bf134b2ff9428b98119ac540833b00082345858e Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 9 Sep 2024 13:30:22 +0800 Subject: [PATCH] Add to support dump/load Dock layout. (#226) --- .gitignore | 1 + Cargo.lock | 2 + crates/app/Cargo.toml | 1 + crates/app/src/story_workspace.rs | 109 +++++++++++------ crates/story/Cargo.toml | 1 + crates/story/src/lib.rs | 95 ++++++++++++++- crates/ui/src/dock/mod.rs | 69 +++++++---- crates/ui/src/dock/panel.rs | 195 +++++++++++++++++++++++++++++- crates/ui/src/dock/stack_panel.rs | 61 ++++++++-- crates/ui/src/dock/tab_panel.rs | 81 ++++++++----- crates/ui/src/lib.rs | 7 +- crates/ui/src/resizable/panel.rs | 24 +++- 12 files changed, 529 insertions(+), 117 deletions(-) diff --git a/.gitignore b/.gitignore index 05923927..63e701f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target .DS_Store +layout.json diff --git a/Cargo.lock b/Cargo.lock index ebbb6506..0e7c904c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2294,6 +2294,7 @@ dependencies = [ "log", "rust-embed", "serde", + "serde_json", "story", "ui", "workspace", @@ -4854,6 +4855,7 @@ dependencies = [ "gpui", "regex", "serde", + "serde_json", "ui", ] diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 99cda77b..6731a10a 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -12,6 +12,7 @@ workspace.workspace = true ui.workspace = true story.workspace = true serde.workspace = true +serde_json.workspace = true [lints] workspace = true diff --git a/crates/app/src/story_workspace.rs b/crates/app/src/story_workspace.rs index 246e9c69..2d47df69 100644 --- a/crates/app/src/story_workspace.rs +++ b/crates/app/src/story_workspace.rs @@ -1,18 +1,17 @@ +use anyhow::Result; use gpui::*; use prelude::FluentBuilder as _; use private::serde::Deserialize; +use std::sync::Arc; use story::{ ButtonStory, CalendarStory, DropdownStory, IconStory, ImageStory, InputStory, ListStory, ModalStory, PopupStory, ProgressStory, ResizableStory, ScrollableStory, StoryContainer, SwitchStory, TableStory, TextStory, TooltipStory, }; -use workspace::TitleBar; - -use std::sync::Arc; use ui::{ button::Button, color_picker::{ColorPicker, ColorPickerEvent}, - dock::{DockArea, DockItem}, + dock::{DockArea, DockEvent, DockItem, DockItemState}, drawer::Drawer, h_flex, modal::Modal, @@ -20,6 +19,7 @@ use ui::{ theme::{ActiveTheme, Colorize as _, Theme}, ContextModal, IconName, Root, Sizable, }; +use workspace::TitleBar; use crate::app_state::AppState; @@ -52,8 +52,73 @@ impl StoryWorkspace { .detach(); let dock_area = cx.new_view(|cx| DockArea::new("main-dock", cx)); + let weak_dock_area = dock_area.downgrade(); - let dock_item = DockItem::split_with_sizes( + let dock_item = match Self::load_layout(&weak_dock_area, cx) { + Ok(item) => item, + Err(err) => { + eprintln!("load layout error: {:?}", err); + Self::init_default_layout(&weak_dock_area, cx) + } + }; + + dock_area.update(cx, |view, cx| view.set_root(dock_item, cx)); + + cx.subscribe(&dock_area, |_, dock_area, ev: &DockEvent, cx| match ev { + DockEvent::LayoutChanged => { + // Make debounce + + println!("Saving layout..."); + let json = dock_area.read(cx).dump(cx).unwrap(); + // Save layout json to app dir layout.json + std::fs::write("layout.json", json).unwrap(); + } + }) + .detach(); + + let locale_selector = cx.new_view(LocaleSelector::new); + + let theme_color_picker = cx.new_view(|cx| { + let mut picker = ColorPicker::new("theme-color-picker", cx) + .xsmall() + .anchor(AnchorCorner::TopRight) + .label("Primary Color"); + picker.set_value(cx.theme().primary, cx); + picker + }); + cx.subscribe( + &theme_color_picker, + |_, _, ev: &ColorPickerEvent, cx| match ev { + ColorPickerEvent::Change(color) => { + if let Some(color) = color { + let theme = cx.global_mut::(); + theme.primary = *color; + theme.primary_hover = color.lighten(0.1); + theme.primary_active = color.darken(0.1); + cx.refresh(); + } + } + }, + ) + .detach(); + + Self { + dock_area, + locale_selector, + theme_color_picker, + } + } + + fn load_layout(dock_area: &WeakView, cx: &mut WindowContext) -> Result { + let fname = "layout.json"; + let json = std::fs::read_to_string(fname)?; + let state = serde_json::from_str::(&json)?; + + return Ok(state.to_item(dock_area.clone(), cx)); + } + + fn init_default_layout(dock_area: &WeakView, cx: &mut WindowContext) -> DockItem { + DockItem::split_with_sizes( Axis::Horizontal, vec![ DockItem::split( @@ -116,41 +181,7 @@ impl StoryWorkspace { vec![Some(px(300.)), None, Some(px(350.))], &dock_area, cx, - ); - - dock_area.update(cx, |view, cx| view.set_root(dock_item, cx)); - - let locale_selector = cx.new_view(LocaleSelector::new); - - let theme_color_picker = cx.new_view(|cx| { - let mut picker = ColorPicker::new("theme-color-picker", cx) - .xsmall() - .anchor(AnchorCorner::TopRight) - .label("Primary Color"); - picker.set_value(cx.theme().primary, cx); - picker - }); - cx.subscribe( - &theme_color_picker, - |_, _, ev: &ColorPickerEvent, cx| match ev { - ColorPickerEvent::Change(color) => { - if let Some(color) = color { - let theme = cx.global_mut::(); - theme.primary = *color; - theme.primary_hover = color.lighten(0.1); - theme.primary_active = color.darken(0.1); - cx.refresh(); - } - } - }, ) - .detach(); - - Self { - dock_area, - locale_selector, - theme_color_picker, - } } pub fn new_local( diff --git a/crates/story/Cargo.toml b/crates/story/Cargo.toml index 9400193e..d5f3ca50 100644 --- a/crates/story/Cargo.toml +++ b/crates/story/Cargo.toml @@ -11,6 +11,7 @@ charts-rs = "0.3" regex = "1" chrono = "0.4" serde = "1" +serde_json = "1" [lints] workspace = true diff --git a/crates/story/src/lib.rs b/crates/story/src/lib.rs index 682f6076..8cfe4331 100644 --- a/crates/story/src/lib.rs +++ b/crates/story/src/lib.rs @@ -28,6 +28,7 @@ pub use popup_story::PopupStory; pub use progress_story::ProgressStory; pub use resizable_story::ResizableStory; pub use scrollable_story::ScrollableStory; +use serde::{Deserialize, Serialize}; pub use switch_story::SwitchStory; pub use table_story::TableStory; pub use text_story::TextStory; @@ -42,7 +43,7 @@ use gpui::{ use ui::{ divider::Divider, - dock::{Panel, PanelEvent, TitleStyle}, + dock::{register_panel, DockItemInfo, DockItemState, Panel, PanelEvent, TitleStyle}, h_flex, label::Label, notification::Notification, @@ -55,6 +56,24 @@ pub fn init(cx: &mut AppContext) { input_story::init(cx); dropdown_story::init(cx); popup_story::init(cx); + + register_panel(cx, "StoryContainer", |_, info, cx| { + let story_state = match info { + DockItemInfo::Custom(value) => StoryState::from_value(value), + _ => { + unreachable!("Invalid DockItemInfo: {:?}", info) + } + }; + + let view = cx.new_view(|cx| { + let (title, description, story) = story_state.to_story(cx); + let mut container = StoryContainer::new(cx).story(story, story_state.story_klass); + container.name = title.into(); + container.description = description.into(); + container + }); + Box::new(view) + }); } actions!(story, [PanelInfo]); @@ -95,7 +114,7 @@ pub enum ContainerEvent { pub trait Story { fn klass() -> &'static str { - std::any::type_name::() + std::any::type_name::().split("::").last().unwrap() } fn title() -> &'static str; @@ -114,7 +133,7 @@ pub trait Story { impl EventEmitter for StoryContainer {} impl StoryContainer { - pub fn new(closeable: bool, cx: &mut WindowContext) -> Self { + pub fn new(cx: &mut WindowContext) -> Self { let focus_handle = cx.focus_handle(); Self { @@ -126,7 +145,7 @@ impl StoryContainer { height: None, story: None, story_klass: None, - closeable, + closeable: true, } } @@ -138,7 +157,8 @@ impl StoryContainer { let story_klass = S::klass(); let view = cx.new_view(|cx| { - let mut story = Self::new(S::closeable(), cx).story(story, story_klass); + let mut story = Self::new(cx).story(story, story_klass); + story.closeable = S::closeable(); story.name = name.into(); story.description = description.into(); story.title_bg = S::title_bg(); @@ -172,7 +192,63 @@ impl StoryContainer { } } +#[derive(Debug, Serialize, Deserialize)] +pub struct StoryState { + pub story_klass: SharedString, +} + +impl StoryState { + fn to_value(&self) -> serde_json::Value { + serde_json::json!({ + "story_klass": self.story_klass, + }) + } + + fn from_value(value: serde_json::Value) -> Self { + serde_json::from_value(value).unwrap() + } + + fn to_story(&self, cx: &mut WindowContext) -> (&'static str, &'static str, AnyView) { + macro_rules! story { + ($klass:tt) => { + ( + $klass::title(), + $klass::description(), + $klass::view(cx).into(), + ) + }; + } + + match self.story_klass.to_string().as_str() { + "ButtonStory" => story!(ButtonStory), + "CalendarStory" => story!(CalendarStory), + "DropdownStory" => story!(DropdownStory), + "IconStory" => story!(IconStory), + "ImageStory" => story!(ImageStory), + "InputStory" => story!(InputStory), + "ListStory" => story!(ListStory), + "ModalStory" => story!(ModalStory), + "PopupStory" => story!(PopupStory), + "ProgressStory" => story!(ProgressStory), + "ResizableStory" => story!(ResizableStory), + "ScrollableStory" => story!(ScrollableStory), + "SwitchStory" => story!(SwitchStory), + "TableStory" => story!(TableStory), + "TextStory" => story!(TextStory), + "TooltipStory" => story!(TooltipStory), + "WebViewStory" => story!(WebViewStory), + _ => { + unreachable!("Invalid story klass: {}", self.story_klass) + } + } + } +} + impl Panel for StoryContainer { + fn panel_name(&self) -> &'static str { + "StoryContainer" + } + fn title(&self, _cx: &WindowContext) -> SharedString { self.name.clone() } @@ -196,6 +272,15 @@ impl Panel for StoryContainer { menu.track_focus(&self.focus_handle) .menu("Info", Box::new(PanelInfo)) } + + fn dump(&self, _cx: &AppContext) -> DockItemState { + let mut state = DockItemState::new(self.panel_name()); + let story_state = StoryState { + story_klass: self.story_klass.clone().unwrap(), + }; + state.info = DockItemInfo::custom(story_state.to_value()); + state + } } impl EventEmitter for StoryContainer {} diff --git a/crates/ui/src/dock/mod.rs b/crates/ui/src/dock/mod.rs index 0a185b18..02a613f6 100644 --- a/crates/ui/src/dock/mod.rs +++ b/crates/ui/src/dock/mod.rs @@ -5,16 +5,29 @@ mod tab_panel; use std::sync::Arc; use gpui::{ - actions, div, prelude::FluentBuilder, AnyElement, AnyView, Axis, InteractiveElement as _, - IntoElement, ParentElement as _, Pixels, Render, SharedString, Styled, View, ViewContext, - VisualContext, WindowContext, + actions, div, prelude::FluentBuilder, AnyElement, AnyView, AppContext, Axis, EventEmitter, + InteractiveElement as _, IntoElement, ParentElement as _, Pixels, Render, SharedString, Styled, + View, ViewContext, VisualContext, WeakView, WindowContext, }; pub use panel::*; pub use stack_panel::*; pub use tab_panel::*; +pub fn init(cx: &mut AppContext) { + stack_panel::init(cx); + tab_panel::init(cx); +} + actions!(dock, [ToggleZoom, ClosePanel]); +pub enum DockEvent { + /// The layout of the dock has changed, subscribers this to save the layout. + /// + /// This event is emitted when every time the layout of the dock has changed, + /// So it emits may be too frequently, you may want to debounce the event. + LayoutChanged, +} + /// The main area of the dock. pub struct DockArea { id: SharedString, @@ -43,7 +56,7 @@ impl DockItem { pub fn split( axis: Axis, items: Vec, - dock_area: &View, + dock_area: &WeakView, cx: &mut WindowContext, ) -> Self { let sizes = vec![None; items.len()]; @@ -58,7 +71,7 @@ impl DockItem { axis: Axis, items: Vec, sizes: Vec>, - dock_area: &View, + dock_area: &WeakView, cx: &mut WindowContext, ) -> Self { let mut items = items; @@ -67,13 +80,13 @@ impl DockItem { for (i, item) in items.iter_mut().enumerate() { let view = item.view(); let size = sizes.get(i).copied().flatten(); - stack_panel.add_panel(view.clone(), size, dock_area.downgrade(), cx) + stack_panel.add_panel(view.clone(), size, dock_area.clone(), cx) } for (i, item) in items.iter().enumerate() { let view = item.view(); let size = sizes.get(i).copied().flatten(); - stack_panel.add_panel(view.clone(), size, dock_area.downgrade(), cx) + stack_panel.add_panel(view.clone(), size, dock_area.clone(), cx) } stack_panel @@ -92,7 +105,7 @@ impl DockItem { pub fn tabs( items: Vec>, active_ix: Option, - dock_area: &View, + dock_area: &WeakView, cx: &mut WindowContext, ) -> Self { let mut new_items: Vec> = vec![]; @@ -104,7 +117,7 @@ impl DockItem { pub fn tab( item: View

, - dock_area: &View, + dock_area: &WeakView, cx: &mut WindowContext, ) -> Self { Self::new_tabs(vec![Arc::new(item.clone())], None, dock_area, cx) @@ -113,16 +126,16 @@ impl DockItem { fn new_tabs( items: Vec>, active_ix: Option, - dock_area: &View, + dock_area: &WeakView, cx: &mut WindowContext, ) -> Self { let active_ix = active_ix.unwrap_or(0); let tab_panel = cx.new_view(|cx| { - let mut tab_panel = TabPanel::new(None, dock_area.downgrade(), cx); + let mut tab_panel = TabPanel::new(None, dock_area.clone(), cx); for item in items.iter() { tab_panel.add_panel(item.clone(), cx) } - + tab_panel.active_ix = active_ix; tab_panel }); @@ -178,20 +191,21 @@ impl DockArea { cx.notify(); } + /// Dump the dock panels layout to JSON string. + pub fn dump(&self, cx: &AppContext) -> Result { + let root = self.items.view(); + let state = root.dump(cx); + serde_json::to_string_pretty(&state) + } + /// Subscribe event on the panels #[allow(clippy::only_used_in_recursion)] fn subscribe_item(&self, item: &DockItem, cx: &mut ViewContext) { - let dock_area = cx.view(); - /// Subscribe zoom event on the panel - fn subscribe_zoom( - view: &View

, - dock_area: View, - cx: &mut ViewContext, - ) { + fn subscribe_zoom(view: &View

, cx: &mut ViewContext) { cx.subscribe(view, move |_, panel, event, cx| match event { PanelEvent::ZoomIn => { - let dock_area = dock_area.clone(); + let dock_area = cx.view().clone(); let panel = panel.clone(); cx.spawn(|_, mut cx| async move { let _ = cx.update(|cx| { @@ -204,7 +218,7 @@ impl DockArea { .detach(); } PanelEvent::ZoomOut => { - let dock_area = dock_area.clone(); + let dock_area = cx.view().clone(); cx.spawn(|_, mut cx| async move { let _ = cx.update(|cx| { let _ = dock_area.update(cx, |view, cx| view.set_zoomed_out(cx)); @@ -212,20 +226,27 @@ impl DockArea { }) .detach() } + PanelEvent::LayoutChanged => cx.emit(DockEvent::LayoutChanged), }) .detach(); } match item { - DockItem::Split { items, .. } => { + DockItem::Split { items, view, .. } => { for item in items { self.subscribe_item(item, cx); } + + cx.subscribe(view, move |_, _, event, cx| match event { + PanelEvent::LayoutChanged => cx.emit(DockEvent::LayoutChanged), + _ => {} + }) + .detach(); } DockItem::Tabs { view, .. } => { // We need, only subscribe to the zoom events on the TabPanel // Because we always wrap the DockItem::Panel in a DockItem::Tabs - subscribe_zoom(view, dock_area.clone(), cx); + subscribe_zoom(view, cx); } } } @@ -252,7 +273,7 @@ impl DockArea { } } } - +impl EventEmitter for DockArea {} impl Render for DockArea { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { // println!("Rendering dock area"); diff --git a/crates/ui/src/dock/panel.rs b/crates/ui/src/dock/panel.rs index fea5dd8a..ee86a000 100644 --- a/crates/ui/src/dock/panel.rs +++ b/crates/ui/src/dock/panel.rs @@ -1,9 +1,21 @@ -use gpui::{AnyView, EventEmitter, FocusableView, Hsla, SharedString, View, WindowContext}; -use rust_i18n::t; +use std::collections::HashMap; use crate::popup_menu::PopupMenu; +use gpui::{ + AnyView, AppContext, Axis, EventEmitter, FocusableView, Global, Hsla, Pixels, SharedString, + View, WeakView, WindowContext, +}; +use itertools::Itertools; +use rust_i18n::t; +use serde::{Deserialize, Serialize}; -use super::PanelEvent; +use super::{DockArea, DockItem}; + +pub enum PanelEvent { + ZoomIn, + ZoomOut, + LayoutChanged, +} pub struct TitleStyle { pub background: Hsla, @@ -11,6 +23,12 @@ pub struct TitleStyle { } pub trait Panel: EventEmitter + FocusableView { + /// The name of the panel used to serialize, deserialize and identify the panel. + /// + /// This is used to identify the panel when deserializing the panel. + /// Once you have defined a panel name, this must not be changed. + fn panel_name(&self) -> &'static str; + /// The title of the panel, default is `None`. fn title(&self, _cx: &WindowContext) -> SharedString { t!("Dock.Unnamed").into() @@ -30,6 +48,9 @@ pub trait Panel: EventEmitter + FocusableView { fn popup_menu(&self, this: PopupMenu, _cx: &WindowContext) -> PopupMenu { this } + + /// Dump the panel, used to serialize the panel. + fn dump(&self, cx: &AppContext) -> DockItemState; } pub trait PanelView: 'static + Send + Sync { @@ -42,6 +63,8 @@ pub trait PanelView: 'static + Send + Sync { fn popup_menu(&self, menu: PopupMenu, cx: &WindowContext) -> PopupMenu; fn view(&self) -> AnyView; + + fn dump(&self, cx: &AppContext) -> DockItemState; } impl PanelView for View { @@ -64,6 +87,10 @@ impl PanelView for View { fn view(&self) -> AnyView { self.clone().into() } + + fn dump(&self, cx: &AppContext) -> DockItemState { + self.read(cx).dump(cx) + } } impl From<&dyn PanelView> for AnyView { @@ -83,3 +110,165 @@ impl PartialEq for dyn PanelView { self.view() == other.view() } } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DockItemState { + pub panel_name: String, + pub children: Vec, + pub info: DockItemInfo, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DockItemInfo { + #[serde(rename = "stack")] + Stack { + sizes: Vec, + /// The axis of the stack, 0 is horizontal, 1 is vertical + axis: usize, + }, + #[serde(rename = "tabs")] + Tabs { active_index: usize }, + #[serde(rename = "custom")] + Custom(serde_json::Value), +} + +impl DockItemInfo { + pub fn stack(sizes: Vec, axis: Axis) -> Self { + Self::Stack { + sizes, + axis: if axis == Axis::Horizontal { 0 } else { 1 }, + } + } + + pub fn tabs(active_index: usize) -> Self { + Self::Tabs { active_index } + } + + pub fn custom(value: serde_json::Value) -> Self { + Self::Custom(value) + } + + pub fn axis(&self) -> Option { + match self { + Self::Stack { axis, .. } => Some(if *axis == 0 { + Axis::Horizontal + } else { + Axis::Vertical + }), + _ => None, + } + } + + pub fn sizes(&self) -> Option<&Vec> { + match self { + Self::Stack { sizes, .. } => Some(sizes), + _ => None, + } + } + + pub fn active_index(&self) -> Option { + match self { + Self::Tabs { active_index } => Some(*active_index), + _ => None, + } + } +} + +impl DockItemState { + pub fn new(panel_name: &str) -> Self { + Self { + panel_name: panel_name.to_string(), + children: Vec::new(), + info: DockItemInfo::Tabs { active_index: 0 }, + } + } + + pub fn add_child(&mut self, panel: DockItemState) { + self.children.push(panel); + } + + pub fn to_item(&self, dock_area: WeakView, cx: &mut WindowContext) -> DockItem { + // TODO: Use the empty panel if the panel is not registered, for the compatibility. + + let info = self.info.clone(); + let f = *cx + .global::() + .items + .get(&self.panel_name) + .unwrap_or_else(|| { + panic!( + "The {} panel type is not registed in PanelRegistry.", + self.panel_name + ) + }); + + let items: Vec = self + .children + .iter() + .map(|child| child.to_item(dock_area.clone(), cx)) + .collect(); + + match info { + DockItemInfo::Stack { sizes, axis } => { + let axis = if axis == 0 { + Axis::Horizontal + } else { + Axis::Vertical + }; + let sizes = sizes.iter().map(|s| Some(*s)).collect_vec(); + DockItem::split_with_sizes(axis, items, sizes, &dock_area, cx) + } + DockItemInfo::Tabs { active_index } => { + if items.len() == 1 { + return items[0].clone(); + } + + let items = items + .iter() + .flat_map(|item| match item { + DockItem::Tabs { items, .. } => items.clone(), + _ => { + unreachable!("Invalid DockItem type in DockItemInfo::Tabs") + } + }) + .collect_vec(); + + DockItem::tabs(items, Some(active_index), &dock_area, cx) + } + DockItemInfo::Custom(_) => { + let view = f(dock_area.clone(), info.clone(), cx); + DockItem::tabs(vec![view.into()], None, &dock_area, cx) + } + } + } +} + +pub struct PanelRegistry { + items: HashMap< + String, + fn(WeakView, DockItemInfo, &mut WindowContext) -> Box, + >, +} +impl PanelRegistry { + pub fn new() -> Self { + Self { + items: HashMap::new(), + } + } +} +impl Global for PanelRegistry {} + +/// Register the Panel init by panel_name to global registry. +pub fn register_panel( + cx: &mut AppContext, + panel_name: &str, + deserialize: fn(WeakView, DockItemInfo, &mut WindowContext) -> Box, +) { + if let None = cx.try_global::() { + cx.set_global(PanelRegistry::new()); + } + + cx.global_mut::() + .items + .insert(panel_name.to_string(), deserialize); +} diff --git a/crates/ui/src/dock/stack_panel.rs b/crates/ui/src/dock/stack_panel.rs index ba4ff04e..492c8eda 100644 --- a/crates/ui/src/dock/stack_panel.rs +++ b/crates/ui/src/dock/stack_panel.rs @@ -1,13 +1,17 @@ use std::sync::Arc; use crate::{ + dock::DockItemInfo, h_flex, - resizable::{h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelGroup}, + resizable::{ + h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelEvent, + ResizablePanelGroup, + }, theme::ActiveTheme, Placement, }; -use super::{DockArea, Panel, PanelEvent, PanelView, TabPanel}; +use super::{register_panel, DockArea, DockItemState, Panel, PanelEvent, PanelView, TabPanel}; use gpui::{ prelude::FluentBuilder as _, AppContext, Axis, DismissEvent, EventEmitter, FocusHandle, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, View, ViewContext, @@ -15,6 +19,14 @@ use gpui::{ }; use smallvec::SmallVec; +pub fn init(cx: &mut AppContext) { + register_panel(cx, "StackPanel", |_, info, cx| { + let axis = info.axis().unwrap_or(Axis::Horizontal); + let view = cx.new_view(|cx| StackPanel::new(axis, cx)); + Box::new(view) + }) +} + pub struct StackPanel { pub(super) parent: Option>, pub(super) axis: Axis, @@ -24,25 +36,48 @@ pub struct StackPanel { } impl Panel for StackPanel { + fn panel_name(&self) -> &'static str { + "StackPanel" + } + fn title(&self, _cx: &gpui::WindowContext) -> gpui::SharedString { "StackPanel".into() } + + fn dump(&self, cx: &AppContext) -> DockItemState { + let sizes = self.panel_group.read(cx).sizes(); + let mut state = DockItemState::new(self.panel_name()); + for panel in &self.panels { + state.add_child(panel.dump(cx)); + state.info = DockItemInfo::stack(sizes.clone(), self.axis); + } + + state + } } impl StackPanel { pub fn new(axis: Axis, cx: &mut ViewContext) -> Self { + let panel_group = cx.new_view(|cx| { + if axis == Axis::Horizontal { + h_resizable(cx) + } else { + v_resizable(cx) + } + }); + + // Bubble up the resize event. + cx.subscribe(&panel_group, |_, _, _: &ResizablePanelEvent, cx| { + cx.emit(PanelEvent::LayoutChanged) + }) + .detach(); + Self { axis, parent: None, focus_handle: cx.focus_handle(), panels: SmallVec::new(), - panel_group: cx.new_view(|cx| { - if axis == Axis::Horizontal { - h_resizable(cx) - } else { - v_resizable(cx) - } - }), + panel_group, } } @@ -166,9 +201,10 @@ impl StackPanel { self.panels.insert(ix, panel.clone()); self.panel_group.update(cx, |view, cx| { - view.insert_child(Self::new_resizable_panel(panel, size), ix, cx) + view.insert_child(Self::new_resizable_panel(panel.clone(), size), ix, cx) }); + cx.emit(PanelEvent::LayoutChanged); cx.notify(); } @@ -180,6 +216,7 @@ impl StackPanel { view.remove_child(ix, cx); }); + cx.emit(PanelEvent::LayoutChanged); self.remove_self_if_empty(cx); } else { println!("Panel not found in stack panel."); @@ -202,6 +239,7 @@ impl StackPanel { cx, ); }); + cx.emit(PanelEvent::LayoutChanged); } } @@ -218,10 +256,11 @@ impl StackPanel { let view = cx.view().clone(); if let Some(parent) = self.parent.as_ref() { parent.update(cx, |parent, cx| { - parent.remove_panel(Arc::new(view), cx); + parent.remove_panel(Arc::new(view.clone()), cx); }); } + cx.emit(PanelEvent::LayoutChanged); cx.notify(); } diff --git a/crates/ui/src/dock/tab_panel.rs b/crates/ui/src/dock/tab_panel.rs index 2d5aba1d..d2d75fd6 100644 --- a/crates/ui/src/dock/tab_panel.rs +++ b/crates/ui/src/dock/tab_panel.rs @@ -10,6 +10,7 @@ use rust_i18n::t; use crate::{ button::Button, + dock::DockItemInfo, h_flex, popup_menu::{PopupMenu, PopupMenuExt}, tab::{Tab, TabBar}, @@ -18,12 +19,16 @@ use crate::{ v_flex, AxisExt, IconName, Placement, Selectable, Sizable, }; -use super::{ClosePanel, DockArea, Panel, PanelView, StackPanel, ToggleZoom}; +use super::{ + register_panel, ClosePanel, DockArea, DockItemState, Panel, PanelEvent, PanelView, StackPanel, + ToggleZoom, +}; -#[derive(Debug)] -pub enum PanelEvent { - ZoomIn, - ZoomOut, +pub fn init(cx: &mut AppContext) { + register_panel(cx, "TabPanel", |dock_area, _, cx| { + let view = cx.new_view(|cx| TabPanel::new(None, dock_area, cx)); + Box::new(view) + }) } #[derive(Clone)] @@ -72,6 +77,41 @@ pub struct TabPanel { will_split_placement: Option, } +impl Panel for TabPanel { + fn panel_name(&self) -> &'static str { + "TabPanel" + } + + fn title(&self, cx: &WindowContext) -> gpui::SharedString { + self.active_panel() + .map(|panel| panel.title(cx)) + .unwrap_or("Empty Tab".into()) + } + + fn closeable(&self, cx: &WindowContext) -> bool { + self.active_panel() + .map(|panel| panel.closeable(cx)) + .unwrap_or(false) + } + + fn popup_menu(&self, menu: PopupMenu, cx: &WindowContext) -> PopupMenu { + if let Some(panel) = self.active_panel() { + panel.popup_menu(menu, cx) + } else { + menu + } + } + + fn dump(&self, cx: &AppContext) -> DockItemState { + let mut state = DockItemState::new(self.panel_name()); + for panel in self.panels.iter() { + state.add_child(panel.dump(cx)); + state.info = DockItemInfo::tabs(self.active_ix); + } + state + } +} + impl TabPanel { pub fn new( stack_panel: Option>, @@ -102,6 +142,7 @@ impl TabPanel { 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.emit(PanelEvent::LayoutChanged); cx.notify(); } @@ -118,6 +159,7 @@ impl TabPanel { self.panels.push(panel); // set the active panel to the new panel self.set_active_ix(self.panels.len() - 1, cx); + cx.emit(PanelEvent::LayoutChanged); cx.notify(); } @@ -140,6 +182,7 @@ impl TabPanel { .ok() }) .detach(); + cx.emit(PanelEvent::LayoutChanged); cx.notify(); } @@ -159,13 +202,15 @@ impl TabPanel { self.panels.insert(ix, panel); self.set_active_ix(ix, cx); + cx.emit(PanelEvent::LayoutChanged); 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) + self.remove_self_if_empty(cx); + cx.emit(PanelEvent::LayoutChanged); } fn detach_panel(&mut self, panel: Arc, cx: &mut ViewContext) { @@ -443,6 +488,7 @@ impl TabPanel { } self.remove_self_if_empty(cx); + cx.emit(PanelEvent::LayoutChanged); } /// Add panel with split placement @@ -534,6 +580,8 @@ impl TabPanel { }) .detach() } + + cx.emit(PanelEvent::LayoutChanged); } fn on_action_toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext) { @@ -552,27 +600,6 @@ impl TabPanel { } } -impl Panel for TabPanel { - fn title(&self, cx: &WindowContext) -> gpui::SharedString { - self.active_panel() - .map(|panel| panel.title(cx)) - .unwrap_or("Empty Tab".into()) - } - - fn closeable(&self, cx: &WindowContext) -> bool { - self.active_panel() - .map(|panel| panel.closeable(cx)) - .unwrap_or(false) - } - - fn popup_menu(&self, menu: PopupMenu, cx: &WindowContext) -> PopupMenu { - if let Some(panel) = self.active_panel() { - panel.popup_menu(menu, cx) - } else { - menu - } - } -} impl FocusableView for TabPanel { fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { self.focus_handle.clone() diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 838b795b..597d3c13 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -58,13 +58,14 @@ pub use svg_img::*; /// Initialize the UI module. pub fn init(cx: &mut gpui::AppContext) { + context_menu::init(cx); + date_picker::init(cx); + dock::init(cx); + dropdown::init(cx); input::init(cx); list::init(cx); - dropdown::init(cx); - date_picker::init(cx); popover::init(cx); popup_menu::init(cx); - context_menu::init(cx); table::init(cx); webview::init(cx) } diff --git a/crates/ui/src/resizable/panel.rs b/crates/ui/src/resizable/panel.rs index 5c6facd7..16b14d5d 100644 --- a/crates/ui/src/resizable/panel.rs +++ b/crates/ui/src/resizable/panel.rs @@ -2,9 +2,9 @@ use std::rc::Rc; use gpui::{ canvas, div, prelude::FluentBuilder, px, Along, AnyElement, AnyView, Axis, Bounds, Element, - Entity, EntityId, InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, - ParentElement, Pixels, Render, StatefulInteractiveElement, Style, Styled, View, ViewContext, - VisualContext as _, WindowContext, + Entity, EntityId, EventEmitter, InteractiveElement as _, IntoElement, MouseMoveEvent, + MouseUpEvent, ParentElement, Pixels, Render, StatefulInteractiveElement, Style, Styled, View, + ViewContext, VisualContext as _, WindowContext, }; use crate::{h_flex, theme::ActiveTheme, v_flex, AxisExt}; @@ -12,6 +12,10 @@ use crate::{h_flex, theme::ActiveTheme, v_flex, AxisExt}; const PANEL_MIN_SIZE: Pixels = px(100.); const HANDLE_PADDING: Pixels = px(4.); +pub enum ResizablePanelEvent { + Resized, +} + #[derive(Clone, Render)] pub struct DragPanel(pub (EntityId, usize, Axis)); @@ -89,6 +93,11 @@ impl ResizablePanelGroup { self } + /// Returns the sizes of the resizable panels. + pub(crate) fn sizes(&self) -> Vec { + self.sizes.clone() + } + pub fn add_child(&mut self, panel: ResizablePanel, cx: &mut ViewContext) { let mut panel = panel; panel.axis = self.axis; @@ -189,6 +198,11 @@ impl ResizablePanelGroup { ) } + fn done_resizing(&mut self, cx: &mut ViewContext) { + cx.emit(ResizablePanelEvent::Resized); + self.resizing_panel_ix = None; + } + fn sync_real_panel_sizes(&mut self, cx: &WindowContext) { for (i, panel) in self.panels.iter().enumerate() { self.sizes[i] = panel.read(cx).bounds.size.along(self.axis) @@ -256,7 +270,7 @@ impl ResizablePanelGroup { } } } - +impl EventEmitter for ResizablePanelGroup {} impl Render for ResizablePanelGroup { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let view = cx.view().clone(); @@ -477,7 +491,7 @@ impl Element for ResizePanelGroupElement { let view = self.view.clone(); move |_: &MouseUpEvent, phase, cx| { if phase.bubble() { - view.update(cx, |view, _| view.resizing_panel_ix = None); + view.update(cx, |view, cx| view.done_resizing(cx)); } } })