From ca5a0e02d5dddc5e108e62fd1f02a7bbdef7966c Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 3 Oct 2024 17:25:31 +0800 Subject: [PATCH] dock: Add to support save left, right, bottom dock state. (#303) The dock state have been updated to use `DockAreaState`. --- .gitignore | 1 + .vscode/settings.json | 4 - crates/app/src/story_workspace.rs | 167 +++++------ crates/ui/src/dock/dock.rs | 39 ++- crates/ui/src/dock/mod.rs | 64 ++++- crates/ui/src/dock/panel.rs | 171 +---------- crates/ui/src/dock/stack_panel.rs | 12 +- crates/ui/src/dock/state.rs | 250 +++++++++++++++++ crates/ui/tests/fixtures/layout.json | 406 +++++++++++++++++---------- 9 files changed, 671 insertions(+), 443 deletions(-) delete mode 100644 .vscode/settings.json create mode 100644 crates/ui/src/dock/state.rs diff --git a/.gitignore b/.gitignore index 62b8f2fc..133a2bb7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target .DS_Store /layout.json +.vscode diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 11301faf..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rust-analyzer.check.command": "clippy", - "cSpell.words": ["xsmall"] -} diff --git a/crates/app/src/story_workspace.rs b/crates/app/src/story_workspace.rs index 0e00c91b..9889812a 100644 --- a/crates/app/src/story_workspace.rs +++ b/crates/app/src/story_workspace.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use gpui::*; use prelude::FluentBuilder as _; use serde::Deserialize; @@ -11,7 +11,7 @@ use story::{ use ui::{ button::{Button, ButtonStyled as _}, color_picker::{ColorPicker, ColorPickerEvent}, - dock::{DockArea, DockEvent, DockItem, DockItemState, PanelView}, + dock::{DockArea, DockAreaState, DockEvent, DockItem, PanelView}, h_flex, popup_menu::PopupMenuExt, theme::{ActiveTheme, Colorize as _, Theme}, @@ -39,13 +39,13 @@ pub struct StoryWorkspace { dock_area: View, locale_selector: View, theme_color_picker: View, - last_layout_state: Option, + last_layout_state: Option, _save_layout_task: Option>, } impl StoryWorkspace { pub fn new(_app_state: Arc, cx: &mut ViewContext) -> Self { - cx.observe_window_appearance(|_workspace, cx| { + cx.observe_window_appearance(|_, cx| { Theme::sync_system_appearance(cx); }) .detach(); @@ -53,45 +53,48 @@ impl StoryWorkspace { let dock_area = cx.new_view(|cx| DockArea::new("main-dock", cx)); let weak_dock_area = dock_area.downgrade(); - let dock_item = match Self::load_layout(&weak_dock_area, cx) { - Ok(item) => item, + match Self::load_layout(dock_area.clone(), cx) { + Ok(_) => { + println!("load layout success"); + } Err(err) => { eprintln!("load layout error: {:?}", err); - Self::init_default_layout(&weak_dock_area, cx) + let dock_item = Self::init_default_layout(&weak_dock_area, cx); + + let left_panels: Vec> = + vec![Arc::new(StoryContainer::panel::(cx))]; + + let bottom_panels: Vec> = vec![ + Arc::new(StoryContainer::panel::(cx)), + Arc::new(StoryContainer::panel::(cx)), + ]; + + let right_panels: Vec> = + vec![Arc::new(StoryContainer::panel::(cx))]; + + _ = dock_area.update(cx, |view, cx| { + view.set_root(dock_item, cx); + view.set_left_dock(left_panels, Some(px(350.)), cx); + view.set_bottom_dock(bottom_panels, Some(px(200.)), cx); + view.set_right_dock(right_panels, Some(px(320.)), cx); + }); } }; - let left_panels: Vec> = - vec![Arc::new(StoryContainer::panel::(cx))]; - - let bottom_panels: Vec> = vec![ - Arc::new(StoryContainer::panel::(cx)), - Arc::new(StoryContainer::panel::(cx)), - ]; - - let right_panels: Vec> = - vec![Arc::new(StoryContainer::panel::(cx))]; - - dock_area.update(cx, |view, cx| { - view.set_root(dock_item, cx); - view.set_left_dock(left_panels, Some(px(350.)), cx); - view.set_bottom_dock(bottom_panels, Some(px(200.)), cx); - view.set_right_dock(right_panels, Some(px(320.)), cx); - }); - cx.subscribe(&dock_area, |this, dock_area, ev: &DockEvent, cx| match ev { DockEvent::LayoutChanged => this.save_layout(dock_area, cx), }) .detach(); - let dock_area1 = dock_area.clone(); - cx.on_app_quit(move |cx| { - let state = dock_area1.read(cx).dump(cx); - - cx.background_executor().spawn(async move { - // Save layout before quitting - Self::save_state(&state).unwrap(); - }) + cx.on_app_quit({ + let dock_area = dock_area.clone(); + move |cx| { + let state = dock_area.read(cx).dump(cx); + cx.background_executor().spawn(async move { + // Save layout before quitting + Self::save_state(&state).unwrap(); + }) + } }) .detach(); @@ -132,7 +135,7 @@ impl StoryWorkspace { fn save_layout(&mut self, dock_area: View, cx: &mut ViewContext) { self._save_layout_task = Some(cx.spawn(|this, mut cx| async move { - Timer::after(Duration::from_secs(1)).await; + Timer::after(Duration::from_secs(10)).await; let _ = cx.update(|cx| { let dock_area = dock_area.read(cx); @@ -151,82 +154,52 @@ impl StoryWorkspace { })); } - fn save_state(state: &DockItemState) -> Result<()> { + fn save_state(state: &DockAreaState) -> Result<()> { println!("Save layout..."); let json = serde_json::to_string_pretty(state)?; std::fs::write("layout.json", json)?; Ok(()) } - fn load_layout(dock_area: &WeakView, cx: &mut WindowContext) -> Result { + fn load_layout(dock_area: View, cx: &mut WindowContext) -> Result<()> { let fname = "layout.json"; let json = std::fs::read_to_string(fname)?; - let state = serde_json::from_str::(&json)?; + let state = serde_json::from_str::(&json)?; - return Ok(state.to_item(dock_area.clone(), cx)); + dock_area.update(cx, |dock_area, cx| { + dock_area.load(state, cx).context("load layout")?; + + Ok::<(), anyhow::Error>(()) + }) } fn init_default_layout(dock_area: &WeakView, cx: &mut WindowContext) -> DockItem { DockItem::split_with_sizes( - Axis::Horizontal, - vec![ - DockItem::split( - Axis::Vertical, - vec![ - DockItem::tab(StoryContainer::panel::(cx), &dock_area, cx), - DockItem::tab(StoryContainer::panel::(cx), &dock_area, cx), - ], - &dock_area, - cx, - ), - DockItem::split_with_sizes( - Axis::Vertical, - vec![ - DockItem::tabs( - vec![ - Arc::new(StoryContainer::panel::(cx)), - Arc::new(StoryContainer::panel::(cx)), - Arc::new(StoryContainer::panel::(cx)), - Arc::new(StoryContainer::panel::(cx)), - Arc::new(StoryContainer::panel::(cx)), - Arc::new(StoryContainer::panel::(cx)), - Arc::new(StoryContainer::panel::(cx)), - Arc::new(StoryContainer::panel::(cx)), - Arc::new(StoryContainer::panel::(cx)), - Arc::new(StoryContainer::panel::(cx)), - Arc::new(StoryContainer::panel::(cx)), - ], - None, - &dock_area, - cx, - ), - DockItem::tabs( - vec![ - Arc::new(StoryContainer::panel::(cx)), - Arc::new(StoryContainer::panel::(cx)), - ], - None, - &dock_area, - cx, - ), - ], - vec![None, None, Some(px(300.))], - &dock_area, - cx, - ), - DockItem::split_with_sizes( - Axis::Vertical, - vec![ - DockItem::tab(StoryContainer::panel::(cx), &dock_area, cx), - DockItem::tab(StoryContainer::panel::(cx), &dock_area, cx), - DockItem::tab(StoryContainer::panel::(cx), &dock_area, cx), - ], - vec![None, None, Some(px(300.))], - &dock_area, - cx, - ), - ], - vec![Some(px(300.)), None, Some(px(350.))], + Axis::Vertical, + vec![DockItem::tabs( + vec![ + Arc::new(StoryContainer::panel::(cx)), + Arc::new(StoryContainer::panel::(cx)), + Arc::new(StoryContainer::panel::(cx)), + Arc::new(StoryContainer::panel::(cx)), + Arc::new(StoryContainer::panel::(cx)), + Arc::new(StoryContainer::panel::(cx)), + Arc::new(StoryContainer::panel::(cx)), + Arc::new(StoryContainer::panel::(cx)), + Arc::new(StoryContainer::panel::(cx)), + Arc::new(StoryContainer::panel::(cx)), + Arc::new(StoryContainer::panel::(cx)), + Arc::new(StoryContainer::panel::(cx)), + Arc::new(StoryContainer::panel::(cx)), + Arc::new(StoryContainer::panel::(cx)), + Arc::new(StoryContainer::panel::(cx)), + Arc::new(StoryContainer::panel::(cx)), + ], + None, + &dock_area, + cx, + )], + vec![None], &dock_area, cx, ) diff --git a/crates/ui/src/dock/dock.rs b/crates/ui/src/dock/dock.rs index 23bdf9d1..cec31c94 100644 --- a/crates/ui/src/dock/dock.rs +++ b/crates/ui/src/dock/dock.rs @@ -8,6 +8,7 @@ use gpui::{ StatefulInteractiveElement, Style, Styled as _, View, ViewContext, VisualContext as _, WeakView, }; +use serde::{Deserialize, Serialize}; use crate::{ resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE}, @@ -20,10 +21,13 @@ use super::{DockArea, PanelView, TabPanel}; #[derive(Clone, Render)] struct ResizePanel; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub enum DockPlacement { + #[serde(rename = "left")] Left, + #[serde(rename = "bottom")] Bottom, + #[serde(rename = "right")] Right, } @@ -52,18 +56,17 @@ impl DockPlacement { /// /// This is unlike Panel, it can't be move or add any other panel. pub struct Dock { - placement: DockPlacement, + pub(super) placement: DockPlacement, dock_area: WeakView, pub(crate) panel: View, /// The size is means the width or height of the Dock, if the placement is left or right, the size is width, otherwise the size is height. - size: Pixels, - open: bool, - resizeable: bool, + pub(super) size: Pixels, + pub(super) open: bool, is_resizing: bool, } impl Dock { - fn new( + pub(crate) fn new( dock_area: WeakView, placement: DockPlacement, cx: &mut ViewContext, @@ -80,7 +83,6 @@ impl Dock { dock_area, panel, open: true, - resizeable: true, size: px(200.0), is_resizing: false, } @@ -98,6 +100,23 @@ impl Dock { Self::new(dock_area, DockPlacement::Right, cx) } + pub(super) fn from_state( + dock_area: WeakView, + placement: DockPlacement, + size: Pixels, + panel: View, + open: bool, + ) -> Self { + Self { + placement, + dock_area, + panel, + open, + size, + is_resizing: false, + } + } + pub fn set_panels(&mut self, panels: Vec>, cx: &mut ViewContext) { self.panel.update(cx, |tab_panel, _| { tab_panel.panels = panels; @@ -106,12 +125,6 @@ impl Dock { cx.notify(); } - /// Set the Dock to be resizeable, default: true - pub fn resizeable(mut self, resizeable: bool) -> Self { - self.resizeable = resizeable; - self - } - pub fn is_open(&self) -> bool { self.open } diff --git a/crates/ui/src/dock/mod.rs b/crates/ui/src/dock/mod.rs index a9d69a46..7d374543 100644 --- a/crates/ui/src/dock/mod.rs +++ b/crates/ui/src/dock/mod.rs @@ -2,10 +2,10 @@ mod dock; mod invalid_panel; mod panel; mod stack_panel; +mod state; mod tab_panel; -use std::sync::Arc; - +use anyhow::{bail, Result}; pub use dock::*; use gpui::{ actions, canvas, div, prelude::FluentBuilder, AnyElement, AnyView, AppContext, Axis, Bounds, @@ -14,6 +14,8 @@ use gpui::{ }; pub use panel::*; pub use stack_panel::*; +pub use state::*; +use std::sync::Arc; pub use tab_panel::*; pub fn init(cx: &mut AppContext) { @@ -293,12 +295,64 @@ impl DockArea { } } + /// Load the state of the DockArea from the DockAreaState. + /// + /// See also [DockeArea::dump]. + pub fn load(&mut self, state: DockAreaState, cx: &mut ViewContext) -> Result<()> { + let weak_self = cx.view().downgrade(); + + if let Some(left_dock) = state.left_dock { + match left_dock.to_dock(weak_self.clone(), cx) { + Ok(dock) => self.left_dock = Some(dock), + Err(err) => bail!("failed to load left dock: {}", err), + } + } + + if let Some(right_dock) = state.right_dock { + match right_dock.to_dock(weak_self.clone(), cx) { + Ok(dock) => self.right_dock = Some(dock), + Err(err) => bail!("failed to load right dock: {}", err), + } + } + + if let Some(bottom_dock) = state.bottom_dock { + match bottom_dock.to_dock(weak_self.clone(), cx) { + Ok(dock) => self.bottom_dock = Some(dock), + Err(err) => bail!("failed to load bottom dock: {}", err), + } + } + + self.items = state.center.to_item(weak_self, cx); + + Ok(()) + } + /// Dump the dock panels layout to DockItemState. /// - /// See also `DockItemState::to_item` for the load DockItem from DockItemState. - pub fn dump(&self, cx: &AppContext) -> DockItemState { + /// See also [DockArea::load]. + pub fn dump(&self, cx: &AppContext) -> DockAreaState { let root = self.items.view(); - root.dump(cx) + let center = root.dump(cx); + + let left_dock = self + .left_dock + .as_ref() + .map(|dock| DockState::new(dock.clone(), cx)); + let right_dock = self + .right_dock + .as_ref() + .map(|dock| DockState::new(dock.clone(), cx)); + let bottom_dock = self + .bottom_dock + .as_ref() + .map(|dock| DockState::new(dock.clone(), cx)); + + DockAreaState { + center, + left_dock, + right_dock, + bottom_dock, + } } /// Subscribe event on the panels diff --git a/crates/ui/src/dock/panel.rs b/crates/ui/src/dock/panel.rs index cee6afba..64fc6671 100644 --- a/crates/ui/src/dock/panel.rs +++ b/crates/ui/src/dock/panel.rs @@ -2,14 +2,13 @@ use std::{collections::HashMap, sync::Arc}; use crate::popup_menu::PopupMenu; use gpui::{ - AnyElement, AnyView, AppContext, Axis, EventEmitter, FocusHandle, FocusableView, Global, Hsla, - IntoElement, Pixels, SharedString, View, VisualContext, WeakView, WindowContext, + AnyElement, AnyView, AppContext, EventEmitter, FocusHandle, FocusableView, Global, Hsla, + IntoElement, SharedString, View, WeakView, WindowContext, }; -use itertools::Itertools; -use rust_i18n::t; -use serde::{Deserialize, Serialize}; -use super::{invalid_panel::InvalidPanel, DockArea, DockItem}; +use rust_i18n::t; + +use super::{DockArea, DockItemInfo, DockItemState}; pub enum PanelEvent { ZoomIn, @@ -143,150 +142,8 @@ impl PartialEq for dyn PanelView { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct DockItemState { - pub panel_name: String, - pub children: Vec, - pub info: DockItemInfo, -} - -#[derive(Debug, Clone, PartialEq, 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 = "panel")] - Panel(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 panel(value: serde_json::Value) -> Self { - Self::Panel(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 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(), - ..Default::default() - } - } - - pub fn add_child(&mut self, panel: DockItemState) { - self.children.push(panel); - } - - pub fn to_item(&self, dock_area: WeakView, cx: &mut WindowContext) -> DockItem { - let info = self.info.clone(); - - 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::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, info.clone(), cx)), - ) - }; - - DockItem::tabs(vec![view.into()], None, &dock_area, cx) - } - } - } -} - pub struct PanelRegistry { - items: HashMap< + pub(super) items: HashMap< String, Arc, DockItemInfo, &mut WindowContext) -> Box>, >, @@ -313,19 +170,3 @@ where .items .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 1b520211..ef9c0fb4 100644 --- a/crates/ui/src/dock/stack_panel.rs +++ b/crates/ui/src/dock/stack_panel.rs @@ -182,13 +182,13 @@ impl StackPanel { } else if let Ok(stack_panel) = panel.view().downcast::() { stack_panel.update(cx, |stack_panel, _| stack_panel.parent = Some(view)); } - } - }); - // Subscribe to the panel's layout change event. - _ = dock_area.update(cx, |_, cx| { - if let Ok(tab_panel) = panel.view().downcast::() { - DockArea::subscribe_panel(&tab_panel, cx); + // Subscribe to the panel's layout change event. + _ = dock_area.update(cx, |_, cx| { + if let Ok(tab_panel) = panel.view().downcast::() { + DockArea::subscribe_panel(&tab_panel, cx); + } + }); } }); diff --git a/crates/ui/src/dock/state.rs b/crates/ui/src/dock/state.rs new file mode 100644 index 00000000..88970983 --- /dev/null +++ b/crates/ui/src/dock/state.rs @@ -0,0 +1,250 @@ +use anyhow::{bail, Result}; +use gpui::{AppContext, Axis, Pixels, View, VisualContext as _, WeakView, WindowContext}; +use itertools::Itertools as _; +use serde::{Deserialize, Serialize}; + +use super::{ + invalid_panel::InvalidPanel, Dock, DockArea, DockItem, DockPlacement, PanelRegistry, PanelView, + TabPanel, +}; + +/// Used to serialize and deserialize the DockArea +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct DockAreaState { + pub center: DockItemState, + pub left_dock: Option, + pub right_dock: Option, + pub bottom_dock: Option, +} + +/// Used to serialize and deserialize the Dock +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct DockState { + panel: DockItemState, + placement: DockPlacement, + size: Pixels, + open: bool, +} + +impl DockState { + pub fn new(dock: View, cx: &AppContext) -> Self { + let dock = dock.read(cx); + + Self { + placement: dock.placement, + size: dock.size, + open: dock.open, + panel: dock.panel.dump(cx), + } + } + + /// Convert the DockState to Dock + pub fn to_dock( + &self, + dock_area: WeakView, + cx: &mut WindowContext, + ) -> Result> { + let view = self.panel.to_item(dock_area.clone(), cx).view(); + if let Ok(tab_panel) = view.view().downcast::() { + let dock = cx.new_view(|_| { + Dock::from_state( + dock_area.clone(), + self.placement, + self.size, + tab_panel, + self.open, + ) + }); + + Ok(dock) + } else { + bail!("Invalid panel, failed to downcast to TabPanel") + } + } +} + +/// Used to serialize and deserialize the DockerItem +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct DockItemState { + pub panel_name: String, + pub children: Vec, + pub info: DockItemInfo, +} + +#[derive(Debug, Clone, PartialEq, 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 = "panel")] + Panel(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 panel(value: serde_json::Value) -> Self { + Self::Panel(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 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(), + ..Default::default() + } + } + + pub fn add_child(&mut self, panel: DockItemState) { + self.children.push(panel); + } + + pub fn to_item(&self, dock_area: WeakView, cx: &mut WindowContext) -> DockItem { + let info = self.info.clone(); + + 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::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, info.clone(), cx)), + ) + }; + + DockItem::tabs(vec![view.into()], None, &dock_area, cx) + } + } + } +} + +#[cfg(test)] +mod tests { + use gpui::px; + + use super::*; + #[test] + fn test_deserialize_item_state() { + let json = include_str!("../../tests/fixtures/layout.json"); + let state: DockAreaState = serde_json::from_str(json).unwrap(); + assert_eq!(state.center.panel_name, "StackPanel"); + assert_eq!(state.center.children.len(), 2); + assert_eq!(state.center.children[0].panel_name, "TabPanel"); + assert_eq!(state.center.children[1].children.len(), 1); + assert_eq!( + state.center.children[1].children[0].panel_name, + "StoryContainer" + ); + assert_eq!(state.center.children[1].panel_name, "TabPanel"); + + let left_dock = state.left_dock.unwrap(); + assert_eq!(left_dock.open, true); + assert_eq!(left_dock.size, px(350.0)); + assert_eq!(left_dock.placement, DockPlacement::Left); + assert_eq!(left_dock.panel.panel_name, "TabPanel"); + assert_eq!(left_dock.panel.children.len(), 1); + assert_eq!(left_dock.panel.children[0].panel_name, "StoryContainer"); + + let bottom_dock = state.bottom_dock.unwrap(); + assert_eq!(bottom_dock.open, true); + assert_eq!(bottom_dock.size, px(200.0)); + assert_eq!(bottom_dock.panel.panel_name, "TabPanel"); + assert_eq!(bottom_dock.panel.children.len(), 2); + assert_eq!(bottom_dock.panel.children[0].panel_name, "StoryContainer"); + + let right_dock = state.right_dock.unwrap(); + assert_eq!(right_dock.open, true); + assert_eq!(right_dock.size, px(320.0)); + assert_eq!(right_dock.panel.panel_name, "TabPanel"); + assert_eq!(right_dock.panel.children.len(), 1); + assert_eq!(right_dock.panel.children[0].panel_name, "StoryContainer"); + } +} diff --git a/crates/ui/tests/fixtures/layout.json b/crates/ui/tests/fixtures/layout.json index 5486dc56..f1205509 100644 --- a/crates/ui/tests/fixtures/layout.json +++ b/crates/ui/tests/fixtures/layout.json @@ -1,161 +1,261 @@ { - "panel_name": "StackPanel", - "children": [ - { - "panel_name": "StackPanel", - "children": [ - { - "panel_name": "TabPanel", - "children": [ - { - "panel_name": "StoryContainer", - "children": [], - "info": { "panel": { "story_klass": "IconStory" } } + "center": { + "panel_name": "StackPanel", + "children": [ + { + "panel_name": "TabPanel", + "children": [ + { + "panel_name": "StoryContainer", + "children": [], + "info": { + "panel": { + "story_klass": "ButtonStory" + } } - ], - "info": { "tabs": { "active_index": 0 } } - }, - { - "panel_name": "TabPanel", - "children": [ - { - "panel_name": "StoryContainer", - "children": [], - "info": { "panel": { "story_klass": "CalendarStory" } } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { + "panel": { + "story_klass": "InputStory" + } } - ], - "info": { "tabs": { "active_index": 0 } } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { + "panel": { + "story_klass": "TextStory" + } + } + }, + { + "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": "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": "IconStory" + } + } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { + "panel": { + "story_klass": "TooltipStory" + } + } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { + "panel": { + "story_klass": "ProgressStory" + } + } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { + "panel": { + "story_klass": "CalendarStory" + } + } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { + "panel": { + "story_klass": "ResizableStory" + } + } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { + "panel": { + "story_klass": "ScrollableStory" + } + } + } + ], + "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" } } + }, + { + "panel_name": "TabPanel", + "children": [ + { + "panel_name": "StoryContainer", + "children": [], + "info": { + "panel": { + "story_klass": "PopupStory" + } } - ], - "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": { + "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": [704.0, 263.0], + "axis": 1 + } } - ], - "info": { "stack": { "sizes": [300.0, 950.0, 350.0], "axis": 0 } } + }, + "left_dock": { + "panel": { + "panel_name": "TabPanel", + "children": [ + { + "panel_name": "StoryContainer", + "children": [], + "info": { + "panel": { + "story_klass": "ListStory" + } + } + } + ], + "info": { + "tabs": { + "active_index": 0 + } + } + }, + "placement": "left", + "size": 350.0, + "open": true, + "resizeable": true + }, + "right_dock": { + "panel": { + "panel_name": "TabPanel", + "children": [ + { + "panel_name": "StoryContainer", + "children": [], + "info": { + "panel": { + "story_klass": "ImageStory" + } + } + } + ], + "info": { + "tabs": { + "active_index": 0 + } + } + }, + "placement": "right", + "size": 320.0, + "open": true, + "resizeable": true + }, + "bottom_dock": { + "panel": { + "panel_name": "TabPanel", + "children": [ + { + "panel_name": "StoryContainer", + "children": [], + "info": { + "panel": { + "story_klass": "TextStory" + } + } + }, + { + "panel_name": "StoryContainer", + "children": [], + "info": { + "panel": { + "story_klass": "IconStory" + } + } + } + ], + "info": { + "tabs": { + "active_index": 0 + } + } + }, + "placement": "bottom", + "size": 200.0, + "open": true, + "resizeable": true + } }