dock: Add to support save left, right, bottom dock state. (#303)

The dock state have been updated to use `DockAreaState`.
This commit is contained in:
Jason Lee 2024-10-03 17:25:31 +08:00 committed by GitHub
parent 19bb5eee3a
commit ca5a0e02d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 671 additions and 443 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
/target /target
.DS_Store .DS_Store
/layout.json /layout.json
.vscode

View file

@ -1,4 +0,0 @@
{
"rust-analyzer.check.command": "clippy",
"cSpell.words": ["xsmall"]
}

View file

@ -1,4 +1,4 @@
use anyhow::Result; use anyhow::{Context, Result};
use gpui::*; use gpui::*;
use prelude::FluentBuilder as _; use prelude::FluentBuilder as _;
use serde::Deserialize; use serde::Deserialize;
@ -11,7 +11,7 @@ use story::{
use ui::{ use ui::{
button::{Button, ButtonStyled as _}, button::{Button, ButtonStyled as _},
color_picker::{ColorPicker, ColorPickerEvent}, color_picker::{ColorPicker, ColorPickerEvent},
dock::{DockArea, DockEvent, DockItem, DockItemState, PanelView}, dock::{DockArea, DockAreaState, DockEvent, DockItem, PanelView},
h_flex, h_flex,
popup_menu::PopupMenuExt, popup_menu::PopupMenuExt,
theme::{ActiveTheme, Colorize as _, Theme}, theme::{ActiveTheme, Colorize as _, Theme},
@ -39,13 +39,13 @@ pub struct StoryWorkspace {
dock_area: View<DockArea>, dock_area: View<DockArea>,
locale_selector: View<LocaleSelector>, locale_selector: View<LocaleSelector>,
theme_color_picker: View<ColorPicker>, theme_color_picker: View<ColorPicker>,
last_layout_state: Option<DockItemState>, last_layout_state: Option<DockAreaState>,
_save_layout_task: Option<Task<()>>, _save_layout_task: Option<Task<()>>,
} }
impl StoryWorkspace { impl StoryWorkspace {
pub fn new(_app_state: Arc<AppState>, cx: &mut ViewContext<Self>) -> Self { pub fn new(_app_state: Arc<AppState>, cx: &mut ViewContext<Self>) -> Self {
cx.observe_window_appearance(|_workspace, cx| { cx.observe_window_appearance(|_, cx| {
Theme::sync_system_appearance(cx); Theme::sync_system_appearance(cx);
}) })
.detach(); .detach();
@ -53,45 +53,48 @@ impl StoryWorkspace {
let dock_area = cx.new_view(|cx| DockArea::new("main-dock", cx)); let dock_area = cx.new_view(|cx| DockArea::new("main-dock", cx));
let weak_dock_area = dock_area.downgrade(); let weak_dock_area = dock_area.downgrade();
let dock_item = match Self::load_layout(&weak_dock_area, cx) { match Self::load_layout(dock_area.clone(), cx) {
Ok(item) => item, Ok(_) => {
println!("load layout success");
}
Err(err) => { Err(err) => {
eprintln!("load layout error: {:?}", 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<Arc<dyn PanelView>> =
vec![Arc::new(StoryContainer::panel::<ListStory>(cx))];
let bottom_panels: Vec<Arc<dyn PanelView>> = vec![
Arc::new(StoryContainer::panel::<TextStory>(cx)),
Arc::new(StoryContainer::panel::<IconStory>(cx)),
];
let right_panels: Vec<Arc<dyn PanelView>> =
vec![Arc::new(StoryContainer::panel::<ImageStory>(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<Arc<dyn PanelView>> =
vec![Arc::new(StoryContainer::panel::<ListStory>(cx))];
let bottom_panels: Vec<Arc<dyn PanelView>> = vec![
Arc::new(StoryContainer::panel::<TextStory>(cx)),
Arc::new(StoryContainer::panel::<IconStory>(cx)),
];
let right_panels: Vec<Arc<dyn PanelView>> =
vec![Arc::new(StoryContainer::panel::<ImageStory>(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 { cx.subscribe(&dock_area, |this, dock_area, ev: &DockEvent, cx| match ev {
DockEvent::LayoutChanged => this.save_layout(dock_area, cx), DockEvent::LayoutChanged => this.save_layout(dock_area, cx),
}) })
.detach(); .detach();
let dock_area1 = dock_area.clone(); cx.on_app_quit({
cx.on_app_quit(move |cx| { let dock_area = dock_area.clone();
let state = dock_area1.read(cx).dump(cx); move |cx| {
let state = dock_area.read(cx).dump(cx);
cx.background_executor().spawn(async move { cx.background_executor().spawn(async move {
// Save layout before quitting // Save layout before quitting
Self::save_state(&state).unwrap(); Self::save_state(&state).unwrap();
}) })
}
}) })
.detach(); .detach();
@ -132,7 +135,7 @@ impl StoryWorkspace {
fn save_layout(&mut self, dock_area: View<DockArea>, cx: &mut ViewContext<Self>) { fn save_layout(&mut self, dock_area: View<DockArea>, cx: &mut ViewContext<Self>) {
self._save_layout_task = Some(cx.spawn(|this, mut cx| async move { 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 _ = cx.update(|cx| {
let dock_area = dock_area.read(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..."); println!("Save layout...");
let json = serde_json::to_string_pretty(state)?; let json = serde_json::to_string_pretty(state)?;
std::fs::write("layout.json", json)?; std::fs::write("layout.json", json)?;
Ok(()) Ok(())
} }
fn load_layout(dock_area: &WeakView<DockArea>, cx: &mut WindowContext) -> Result<DockItem> { fn load_layout(dock_area: View<DockArea>, cx: &mut WindowContext) -> Result<()> {
let fname = "layout.json"; let fname = "layout.json";
let json = std::fs::read_to_string(fname)?; let json = std::fs::read_to_string(fname)?;
let state = serde_json::from_str::<DockItemState>(&json)?; let state = serde_json::from_str::<DockAreaState>(&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<DockArea>, cx: &mut WindowContext) -> DockItem { fn init_default_layout(dock_area: &WeakView<DockArea>, cx: &mut WindowContext) -> DockItem {
DockItem::split_with_sizes( DockItem::split_with_sizes(
Axis::Horizontal, Axis::Vertical,
vec![ vec![DockItem::tabs(
DockItem::split( vec![
Axis::Vertical, Arc::new(StoryContainer::panel::<ButtonStory>(cx)),
vec![ Arc::new(StoryContainer::panel::<InputStory>(cx)),
DockItem::tab(StoryContainer::panel::<IconStory>(cx), &dock_area, cx), Arc::new(StoryContainer::panel::<TextStory>(cx)),
DockItem::tab(StoryContainer::panel::<CalendarStory>(cx), &dock_area, cx), Arc::new(StoryContainer::panel::<DropdownStory>(cx)),
], Arc::new(StoryContainer::panel::<ModalStory>(cx)),
&dock_area, Arc::new(StoryContainer::panel::<PopupStory>(cx)),
cx, Arc::new(StoryContainer::panel::<SwitchStory>(cx)),
), Arc::new(StoryContainer::panel::<ProgressStory>(cx)),
DockItem::split_with_sizes( Arc::new(StoryContainer::panel::<TableStory>(cx)),
Axis::Vertical, Arc::new(StoryContainer::panel::<ImageStory>(cx)),
vec![ Arc::new(StoryContainer::panel::<IconStory>(cx)),
DockItem::tabs( Arc::new(StoryContainer::panel::<TooltipStory>(cx)),
vec![ Arc::new(StoryContainer::panel::<ProgressStory>(cx)),
Arc::new(StoryContainer::panel::<ButtonStory>(cx)), Arc::new(StoryContainer::panel::<CalendarStory>(cx)),
Arc::new(StoryContainer::panel::<InputStory>(cx)), Arc::new(StoryContainer::panel::<ResizableStory>(cx)),
Arc::new(StoryContainer::panel::<DropdownStory>(cx)), Arc::new(StoryContainer::panel::<ScrollableStory>(cx)),
Arc::new(StoryContainer::panel::<ModalStory>(cx)), ],
Arc::new(StoryContainer::panel::<PopupStory>(cx)), None,
Arc::new(StoryContainer::panel::<SwitchStory>(cx)), &dock_area,
Arc::new(StoryContainer::panel::<ProgressStory>(cx)), cx,
Arc::new(StoryContainer::panel::<TableStory>(cx)), )],
Arc::new(StoryContainer::panel::<ImageStory>(cx)), vec![None],
Arc::new(StoryContainer::panel::<ResizableStory>(cx)),
Arc::new(StoryContainer::panel::<ScrollableStory>(cx)),
],
None,
&dock_area,
cx,
),
DockItem::tabs(
vec![
Arc::new(StoryContainer::panel::<ProgressStory>(cx)),
Arc::new(StoryContainer::panel::<TextStory>(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::<TooltipStory>(cx), &dock_area, cx),
DockItem::tab(StoryContainer::panel::<CalendarStory>(cx), &dock_area, cx),
DockItem::tab(StoryContainer::panel::<ImageStory>(cx), &dock_area, cx),
],
vec![None, None, Some(px(300.))],
&dock_area,
cx,
),
],
vec![Some(px(300.)), None, Some(px(350.))],
&dock_area, &dock_area,
cx, cx,
) )

View file

@ -8,6 +8,7 @@ use gpui::{
StatefulInteractiveElement, Style, Styled as _, View, ViewContext, VisualContext as _, StatefulInteractiveElement, Style, Styled as _, View, ViewContext, VisualContext as _,
WeakView, WeakView,
}; };
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE}, resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE},
@ -20,10 +21,13 @@ use super::{DockArea, PanelView, TabPanel};
#[derive(Clone, Render)] #[derive(Clone, Render)]
struct ResizePanel; struct ResizePanel;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum DockPlacement { pub enum DockPlacement {
#[serde(rename = "left")]
Left, Left,
#[serde(rename = "bottom")]
Bottom, Bottom,
#[serde(rename = "right")]
Right, Right,
} }
@ -52,18 +56,17 @@ impl DockPlacement {
/// ///
/// This is unlike Panel, it can't be move or add any other panel. /// This is unlike Panel, it can't be move or add any other panel.
pub struct Dock { pub struct Dock {
placement: DockPlacement, pub(super) placement: DockPlacement,
dock_area: WeakView<DockArea>, dock_area: WeakView<DockArea>,
pub(crate) panel: View<TabPanel>, pub(crate) panel: View<TabPanel>,
/// 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. /// 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, pub(super) size: Pixels,
open: bool, pub(super) open: bool,
resizeable: bool,
is_resizing: bool, is_resizing: bool,
} }
impl Dock { impl Dock {
fn new( pub(crate) fn new(
dock_area: WeakView<DockArea>, dock_area: WeakView<DockArea>,
placement: DockPlacement, placement: DockPlacement,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
@ -80,7 +83,6 @@ impl Dock {
dock_area, dock_area,
panel, panel,
open: true, open: true,
resizeable: true,
size: px(200.0), size: px(200.0),
is_resizing: false, is_resizing: false,
} }
@ -98,6 +100,23 @@ impl Dock {
Self::new(dock_area, DockPlacement::Right, cx) Self::new(dock_area, DockPlacement::Right, cx)
} }
pub(super) fn from_state(
dock_area: WeakView<DockArea>,
placement: DockPlacement,
size: Pixels,
panel: View<TabPanel>,
open: bool,
) -> Self {
Self {
placement,
dock_area,
panel,
open,
size,
is_resizing: false,
}
}
pub fn set_panels(&mut self, panels: Vec<Arc<dyn PanelView>>, cx: &mut ViewContext<Self>) { pub fn set_panels(&mut self, panels: Vec<Arc<dyn PanelView>>, cx: &mut ViewContext<Self>) {
self.panel.update(cx, |tab_panel, _| { self.panel.update(cx, |tab_panel, _| {
tab_panel.panels = panels; tab_panel.panels = panels;
@ -106,12 +125,6 @@ impl Dock {
cx.notify(); 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 { pub fn is_open(&self) -> bool {
self.open self.open
} }

View file

@ -2,10 +2,10 @@ mod dock;
mod invalid_panel; mod invalid_panel;
mod panel; mod panel;
mod stack_panel; mod stack_panel;
mod state;
mod tab_panel; mod tab_panel;
use std::sync::Arc; use anyhow::{bail, Result};
pub use dock::*; pub use dock::*;
use gpui::{ use gpui::{
actions, canvas, div, prelude::FluentBuilder, AnyElement, AnyView, AppContext, Axis, Bounds, actions, canvas, div, prelude::FluentBuilder, AnyElement, AnyView, AppContext, Axis, Bounds,
@ -14,6 +14,8 @@ use gpui::{
}; };
pub use panel::*; pub use panel::*;
pub use stack_panel::*; pub use stack_panel::*;
pub use state::*;
use std::sync::Arc;
pub use tab_panel::*; pub use tab_panel::*;
pub fn init(cx: &mut AppContext) { 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<Self>) -> 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. /// Dump the dock panels layout to DockItemState.
/// ///
/// See also `DockItemState::to_item` for the load DockItem from DockItemState. /// See also [DockArea::load].
pub fn dump(&self, cx: &AppContext) -> DockItemState { pub fn dump(&self, cx: &AppContext) -> DockAreaState {
let root = self.items.view(); 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 /// Subscribe event on the panels

View file

@ -2,14 +2,13 @@ use std::{collections::HashMap, sync::Arc};
use crate::popup_menu::PopupMenu; use crate::popup_menu::PopupMenu;
use gpui::{ use gpui::{
AnyElement, AnyView, AppContext, Axis, EventEmitter, FocusHandle, FocusableView, Global, Hsla, AnyElement, AnyView, AppContext, EventEmitter, FocusHandle, FocusableView, Global, Hsla,
IntoElement, Pixels, SharedString, View, VisualContext, WeakView, WindowContext, 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 { pub enum PanelEvent {
ZoomIn, 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<DockItemState>,
pub info: DockItemInfo,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DockItemInfo {
#[serde(rename = "stack")]
Stack {
sizes: Vec<Pixels>,
/// 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<Pixels>, 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<Axis> {
match self {
Self::Stack { axis, .. } => Some(if *axis == 0 {
Axis::Horizontal
} else {
Axis::Vertical
}),
_ => None,
}
}
pub fn sizes(&self) -> Option<&Vec<Pixels>> {
match self {
Self::Stack { sizes, .. } => Some(sizes),
_ => None,
}
}
pub fn active_index(&self) -> Option<usize> {
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<DockArea>, cx: &mut WindowContext) -> DockItem {
let info = self.info.clone();
let items: Vec<DockItem> = 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::<PanelRegistry>()
.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 { pub struct PanelRegistry {
items: HashMap< pub(super) items: HashMap<
String, String,
Arc<dyn Fn(WeakView<DockArea>, DockItemInfo, &mut WindowContext) -> Box<dyn PanelView>>, Arc<dyn Fn(WeakView<DockArea>, DockItemInfo, &mut WindowContext) -> Box<dyn PanelView>>,
>, >,
@ -313,19 +170,3 @@ where
.items .items
.insert(panel_name.to_string(), Arc::new(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");
}
}

View file

@ -182,13 +182,13 @@ impl StackPanel {
} else if let Ok(stack_panel) = panel.view().downcast::<Self>() { } else if let Ok(stack_panel) = panel.view().downcast::<Self>() {
stack_panel.update(cx, |stack_panel, _| stack_panel.parent = Some(view)); stack_panel.update(cx, |stack_panel, _| stack_panel.parent = Some(view));
} }
}
});
// Subscribe to the panel's layout change event. // Subscribe to the panel's layout change event.
_ = dock_area.update(cx, |_, cx| { _ = dock_area.update(cx, |_, cx| {
if let Ok(tab_panel) = panel.view().downcast::<TabPanel>() { if let Ok(tab_panel) = panel.view().downcast::<TabPanel>() {
DockArea::subscribe_panel(&tab_panel, cx); DockArea::subscribe_panel(&tab_panel, cx);
}
});
} }
}); });

250
crates/ui/src/dock/state.rs Normal file
View file

@ -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<DockState>,
pub right_dock: Option<DockState>,
pub bottom_dock: Option<DockState>,
}
/// 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<Dock>, 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<DockArea>,
cx: &mut WindowContext,
) -> Result<View<Dock>> {
let view = self.panel.to_item(dock_area.clone(), cx).view();
if let Ok(tab_panel) = view.view().downcast::<TabPanel>() {
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<DockItemState>,
pub info: DockItemInfo,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DockItemInfo {
#[serde(rename = "stack")]
Stack {
sizes: Vec<Pixels>,
/// 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<Pixels>, 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<Axis> {
match self {
Self::Stack { axis, .. } => Some(if *axis == 0 {
Axis::Horizontal
} else {
Axis::Vertical
}),
_ => None,
}
}
pub fn sizes(&self) -> Option<&Vec<Pixels>> {
match self {
Self::Stack { sizes, .. } => Some(sizes),
_ => None,
}
}
pub fn active_index(&self) -> Option<usize> {
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<DockArea>, cx: &mut WindowContext) -> DockItem {
let info = self.info.clone();
let items: Vec<DockItem> = 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::<PanelRegistry>()
.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");
}
}

View file

@ -1,161 +1,261 @@
{ {
"panel_name": "StackPanel", "center": {
"children": [ "panel_name": "StackPanel",
{ "children": [
"panel_name": "StackPanel", {
"children": [ "panel_name": "TabPanel",
{ "children": [
"panel_name": "TabPanel", {
"children": [ "panel_name": "StoryContainer",
{ "children": [],
"panel_name": "StoryContainer", "info": {
"children": [], "panel": {
"info": { "panel": { "story_klass": "IconStory" } } "story_klass": "ButtonStory"
}
} }
], },
"info": { "tabs": { "active_index": 0 } } {
}, "panel_name": "StoryContainer",
{ "children": [],
"panel_name": "TabPanel", "info": {
"children": [ "panel": {
{ "story_klass": "InputStory"
"panel_name": "StoryContainer", }
"children": [],
"info": { "panel": { "story_klass": "CalendarStory" } }
} }
], },
"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": "TabPanel",
{ "children": [
"panel_name": "StackPanel", {
"children": [ "panel_name": "StoryContainer",
{ "children": [],
"panel_name": "TabPanel", "info": {
"children": [ "panel": {
{ "story_klass": "PopupStory"
"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 } } ],
}, "info": {
{ "tabs": {
"panel_name": "TabPanel", "active_index": 0
"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 } } ],
}, "info": {
{ "stack": {
"panel_name": "StackPanel", "sizes": [704.0, 263.0],
"children": [ "axis": 1
{ }
"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 } } "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
}
} }