From 840a54850ae3d6652fea5bb2a7ca854d12d2a5e9 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 9 Sep 2024 19:19:27 +0800 Subject: [PATCH] Update register_panel to use trait object Fn. (#229) --- .gitignore | 2 +- crates/app/src/story_workspace.rs | 2 +- crates/story/src/lib.rs | 4 +- crates/ui/src/dock/invalid_panel.rs | 49 ++++++++ crates/ui/src/dock/mod.rs | 4 +- crates/ui/src/dock/panel.rs | 82 +++++++++----- crates/ui/src/dock/stack_panel.rs | 10 +- crates/ui/src/dock/tab_panel.rs | 10 +- crates/ui/tests/fixtures/layout.json | 161 +++++++++++++++++++++++++++ 9 files changed, 272 insertions(+), 52 deletions(-) create mode 100644 crates/ui/src/dock/invalid_panel.rs create mode 100644 crates/ui/tests/fixtures/layout.json diff --git a/.gitignore b/.gitignore index 63e701f3..62b8f2fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /target .DS_Store -layout.json +/layout.json diff --git a/crates/app/src/story_workspace.rs b/crates/app/src/story_workspace.rs index 8be51155..bfb395e3 100644 --- a/crates/app/src/story_workspace.rs +++ b/crates/app/src/story_workspace.rs @@ -140,7 +140,7 @@ impl StoryWorkspace { fn save_state(state: &DockItemState) -> Result<()> { println!("Save layout..."); - let json = serde_json::to_string(state)?; + let json = serde_json::to_string_pretty(state)?; std::fs::write("layout.json", json)?; Ok(()) } diff --git a/crates/story/src/lib.rs b/crates/story/src/lib.rs index 8cfe4331..c5eab444 100644 --- a/crates/story/src/lib.rs +++ b/crates/story/src/lib.rs @@ -59,7 +59,7 @@ pub fn init(cx: &mut AppContext) { register_panel(cx, "StoryContainer", |_, info, cx| { let story_state = match info { - DockItemInfo::Custom(value) => StoryState::from_value(value), + DockItemInfo::Panel(value) => StoryState::from_value(value), _ => { unreachable!("Invalid DockItemInfo: {:?}", info) } @@ -278,7 +278,7 @@ impl Panel for StoryContainer { let story_state = StoryState { story_klass: self.story_klass.clone().unwrap(), }; - state.info = DockItemInfo::custom(story_state.to_value()); + state.info = DockItemInfo::panel(story_state.to_value()); state } } diff --git a/crates/ui/src/dock/invalid_panel.rs b/crates/ui/src/dock/invalid_panel.rs new file mode 100644 index 00000000..8de1cf90 --- /dev/null +++ b/crates/ui/src/dock/invalid_panel.rs @@ -0,0 +1,49 @@ +use gpui::{ + AppContext, EventEmitter, FocusHandle, FocusableView, ParentElement as _, Render, SharedString, + Styled as _, WindowContext, +}; + +use crate::theme::ActiveTheme as _; + +use super::{Panel, PanelEvent}; + +pub(crate) struct InvalidPanel { + name: SharedString, + focus_handle: FocusHandle, +} + +impl InvalidPanel { + pub(crate) fn new(name: &str, cx: &mut WindowContext) -> Self { + Self { + focus_handle: cx.focus_handle(), + name: SharedString::from(name.to_owned()), + } + } +} +impl Panel for InvalidPanel { + fn panel_name(&self) -> &'static str { + "InvalidPanel" + } +} +impl EventEmitter for InvalidPanel {} +impl FocusableView for InvalidPanel { + fn focus_handle(&self, _: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} +impl Render for InvalidPanel { + fn render(&mut self, cx: &mut gpui::ViewContext) -> impl gpui::IntoElement { + gpui::div() + .size_full() + .my_6() + .flex() + .flex_col() + .items_center() + .justify_center() + .text_color(cx.theme().muted_foreground) + .child(format!( + "The `{}` panel type is not registed in PanelRegistry.", + self.name.clone() + )) + } +} diff --git a/crates/ui/src/dock/mod.rs b/crates/ui/src/dock/mod.rs index ae96bc41..d0dd3860 100644 --- a/crates/ui/src/dock/mod.rs +++ b/crates/ui/src/dock/mod.rs @@ -1,3 +1,4 @@ +mod invalid_panel; mod panel; mod stack_panel; mod tab_panel; @@ -14,8 +15,7 @@ pub use stack_panel::*; pub use tab_panel::*; pub fn init(cx: &mut AppContext) { - stack_panel::init(cx); - tab_panel::init(cx); + cx.set_global(PanelRegistry::new()); } actions!(dock, [ToggleZoom, ClosePanel]); diff --git a/crates/ui/src/dock/panel.rs b/crates/ui/src/dock/panel.rs index b2a7261a..bf194e86 100644 --- a/crates/ui/src/dock/panel.rs +++ b/crates/ui/src/dock/panel.rs @@ -1,15 +1,15 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; use crate::popup_menu::PopupMenu; use gpui::{ AnyView, AppContext, Axis, EventEmitter, FocusableView, Global, Hsla, Pixels, SharedString, - View, WeakView, WindowContext, + View, VisualContext, WeakView, WindowContext, }; use itertools::Itertools; use rust_i18n::t; use serde::{Deserialize, Serialize}; -use super::{DockArea, DockItem}; +use super::{invalid_panel::InvalidPanel, DockArea, DockItem}; pub enum PanelEvent { ZoomIn, @@ -130,8 +130,8 @@ pub enum DockItemInfo { }, #[serde(rename = "tabs")] Tabs { active_index: usize }, - #[serde(rename = "custom")] - Custom(serde_json::Value), + #[serde(rename = "panel")] + Panel(serde_json::Value), } impl DockItemInfo { @@ -146,8 +146,8 @@ impl DockItemInfo { Self::Tabs { active_index } } - pub fn custom(value: serde_json::Value) -> Self { - Self::Custom(value) + pub fn panel(value: serde_json::Value) -> Self { + Self::Panel(value) } pub fn axis(&self) -> Option { @@ -176,12 +176,21 @@ impl DockItemInfo { } } +impl Default for DockItemState { + fn default() -> Self { + Self { + panel_name: "".to_string(), + children: Vec::new(), + info: DockItemInfo::Panel(serde_json::Value::Null), + } + } +} + 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 }, + ..Default::default() } } @@ -193,16 +202,6 @@ impl DockItemState { // 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 @@ -237,8 +236,19 @@ impl DockItemState { DockItem::tabs(items, Some(active_index), &dock_area, cx) } - DockItemInfo::Custom(_) => { - let view = f(dock_area.clone(), info.clone(), cx); + DockItemInfo::Panel(_) => { + let view = if let Some(f) = cx + .global::() + .items + .get(&self.panel_name) + .cloned() + { + f(dock_area.clone(), info.clone(), cx) + } else { + // Show an invalid panel if the panel is not registered. + Box::new(cx.new_view(|cx| InvalidPanel::new(&self.panel_name, cx))) + }; + DockItem::tabs(vec![view.into()], None, &dock_area, cx) } } @@ -248,7 +258,7 @@ impl DockItemState { pub struct PanelRegistry { items: HashMap< String, - fn(WeakView, DockItemInfo, &mut WindowContext) -> Box, + Arc, DockItemInfo, &mut WindowContext) -> Box>, >, } impl PanelRegistry { @@ -261,16 +271,32 @@ impl PanelRegistry { 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, -) { +pub fn register_panel(cx: &mut AppContext, panel_name: &str, deserialize: F) +where + F: Fn(WeakView, DockItemInfo, &mut WindowContext) -> Box + 'static, +{ if let None = cx.try_global::() { cx.set_global(PanelRegistry::new()); } cx.global_mut::() .items - .insert(panel_name.to_string(), deserialize); + .insert(panel_name.to_string(), Arc::new(deserialize)); +} + + +#[cfg(test)] +mod tests { +use super::*; + #[test] + fn test_deserialize_item_state() { + let json = include_str!("../../tests/fixtures/layout.json"); + let state: DockItemState = serde_json::from_str(json).unwrap(); + assert_eq!(state.panel_name, "StackPanel"); + assert_eq!(state.children.len(), 3); + assert_eq!(state.children[0].panel_name, "StackPanel"); + assert_eq!(state.children[1].children.len(), 2); + assert_eq!(state.children[1].children[0].panel_name, "TabPanel"); + assert_eq!(state.children[1].panel_name, "StackPanel"); + } } diff --git a/crates/ui/src/dock/stack_panel.rs b/crates/ui/src/dock/stack_panel.rs index 492c8eda..f368001b 100644 --- a/crates/ui/src/dock/stack_panel.rs +++ b/crates/ui/src/dock/stack_panel.rs @@ -11,7 +11,7 @@ use crate::{ Placement, }; -use super::{register_panel, DockArea, DockItemState, Panel, PanelEvent, PanelView, TabPanel}; +use super::{DockArea, DockItemState, Panel, PanelEvent, PanelView, TabPanel}; use gpui::{ prelude::FluentBuilder as _, AppContext, Axis, DismissEvent, EventEmitter, FocusHandle, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, View, ViewContext, @@ -19,14 +19,6 @@ 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, diff --git a/crates/ui/src/dock/tab_panel.rs b/crates/ui/src/dock/tab_panel.rs index d2d75fd6..8383b5b6 100644 --- a/crates/ui/src/dock/tab_panel.rs +++ b/crates/ui/src/dock/tab_panel.rs @@ -20,17 +20,9 @@ use crate::{ }; use super::{ - register_panel, ClosePanel, DockArea, DockItemState, Panel, PanelEvent, PanelView, StackPanel, - ToggleZoom, + ClosePanel, DockArea, DockItemState, Panel, PanelEvent, PanelView, StackPanel, ToggleZoom, }; -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)] pub(crate) struct DragPanel { pub(crate) panel: Arc, diff --git a/crates/ui/tests/fixtures/layout.json b/crates/ui/tests/fixtures/layout.json new file mode 100644 index 00000000..5486dc56 --- /dev/null +++ b/crates/ui/tests/fixtures/layout.json @@ -0,0 +1,161 @@ +{ + "panel_name": "StackPanel", + "children": [ + { + "panel_name": "StackPanel", + "children": [ + { + "panel_name": "TabPanel", + "children": [ + { + "panel_name": "StoryContainer", + "children": [], + "info": { "panel": { "story_klass": "IconStory" } } + } + ], + "info": { "tabs": { "active_index": 0 } } + }, + { + "panel_name": "TabPanel", + "children": [ + { + "panel_name": "StoryContainer", + "children": [], + "info": { "panel": { "story_klass": "CalendarStory" } } + } + ], + "info": { "tabs": { "active_index": 0 } } + } + ], + "info": { "stack": { "sizes": [584.0, 583.0], "axis": 1 } } + }, + { + "panel_name": "StackPanel", + "children": [ + { + "panel_name": "TabPanel", + "children": [ + { + "panel_name": "StoryContainer", + "children": [], + "info": { "panel": { "story_klass": "ButtonStory" } } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { "panel": { "story_klass": "InputStory" } } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { "panel": { "story_klass": "DropdownStory" } } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { "panel": { "story_klass": "ModalStory" } } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { "panel": { "story_klass": "PopupStory" } } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { "panel": { "story_klass": "ListStory" } } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { "panel": { "story_klass": "SwitchStory" } } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { "panel": { "story_klass": "ProgressStory" } } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { "panel": { "story_klass": "TableStory" } } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { "panel": { "story_klass": "ImageStory" } } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { "panel": { "story_klass": "ResizableStory" } } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { "panel": { "story_klass": "ScrollableStory" } } + } + ], + "info": { "tabs": { "active_index": 0 } } + }, + { + "panel_name": "TabPanel", + "children": [ + { + "panel_name": "StoryContainer", + "children": [], + "info": { "panel": { "story_klass": "ProgressStory" } } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { "panel": { "story_klass": "TextStory" } } + } + ], + "info": { "tabs": { "active_index": 0 } } + } + ], + "info": { "stack": { "sizes": [584.0, 583.0], "axis": 1 } } + }, + { + "panel_name": "StackPanel", + "children": [ + { + "panel_name": "TabPanel", + "children": [ + { + "panel_name": "StoryContainer", + "children": [], + "info": { "panel": { "story_klass": "TooltipStory" } } + } + ], + "info": { "tabs": { "active_index": 0 } } + }, + { + "panel_name": "TabPanel", + "children": [ + { + "panel_name": "StoryContainer", + "children": [], + "info": { "panel": { "story_klass": "CalendarStory" } } + } + ], + "info": { "tabs": { "active_index": 0 } } + }, + { + "panel_name": "TabPanel", + "children": [ + { + "panel_name": "StoryContainer", + "children": [], + "info": { "panel": { "story_klass": "ImageStory" } } + } + ], + "info": { "tabs": { "active_index": 0 } } + } + ], + "info": { "stack": { "sizes": [434.0, 433.0, 300.0], "axis": 1 } } + } + ], + "info": { "stack": { "sizes": [300.0, 950.0, 350.0], "axis": 0 } } +}