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:
parent
19bb5eee3a
commit
ca5a0e02d5
9 changed files with 671 additions and 443 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
/target
|
||||
.DS_Store
|
||||
/layout.json
|
||||
.vscode
|
||||
|
|
|
|||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"rust-analyzer.check.command": "clippy",
|
||||
"cSpell.words": ["xsmall"]
|
||||
}
|
||||
|
|
@ -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<DockArea>,
|
||||
locale_selector: View<LocaleSelector>,
|
||||
theme_color_picker: View<ColorPicker>,
|
||||
last_layout_state: Option<DockItemState>,
|
||||
last_layout_state: Option<DockAreaState>,
|
||||
_save_layout_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
impl StoryWorkspace {
|
||||
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);
|
||||
})
|
||||
.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<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 {
|
||||
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<DockArea>, cx: &mut ViewContext<Self>) {
|
||||
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<DockArea>, cx: &mut WindowContext) -> Result<DockItem> {
|
||||
fn load_layout(dock_area: View<DockArea>, cx: &mut WindowContext) -> Result<()> {
|
||||
let fname = "layout.json";
|
||||
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 {
|
||||
DockItem::split_with_sizes(
|
||||
Axis::Horizontal,
|
||||
vec![
|
||||
DockItem::split(
|
||||
Axis::Vertical,
|
||||
vec![
|
||||
DockItem::tab(StoryContainer::panel::<IconStory>(cx), &dock_area, cx),
|
||||
DockItem::tab(StoryContainer::panel::<CalendarStory>(cx), &dock_area, cx),
|
||||
],
|
||||
&dock_area,
|
||||
cx,
|
||||
),
|
||||
DockItem::split_with_sizes(
|
||||
Axis::Vertical,
|
||||
vec![
|
||||
DockItem::tabs(
|
||||
vec![
|
||||
Arc::new(StoryContainer::panel::<ButtonStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<InputStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<DropdownStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<ModalStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<PopupStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<SwitchStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<ProgressStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<TableStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<ImageStory>(cx)),
|
||||
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.))],
|
||||
Axis::Vertical,
|
||||
vec![DockItem::tabs(
|
||||
vec![
|
||||
Arc::new(StoryContainer::panel::<ButtonStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<InputStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<TextStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<DropdownStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<ModalStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<PopupStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<SwitchStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<ProgressStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<TableStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<ImageStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<IconStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<TooltipStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<ProgressStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<CalendarStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<ResizableStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<ScrollableStory>(cx)),
|
||||
],
|
||||
None,
|
||||
&dock_area,
|
||||
cx,
|
||||
)],
|
||||
vec![None],
|
||||
&dock_area,
|
||||
cx,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<DockArea>,
|
||||
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.
|
||||
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<DockArea>,
|
||||
placement: DockPlacement,
|
||||
cx: &mut ViewContext<Self>,
|
||||
|
|
@ -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<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>) {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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.
|
||||
///
|
||||
/// 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
|
||||
|
|
|
|||
|
|
@ -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<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 {
|
||||
items: HashMap<
|
||||
pub(super) items: HashMap<
|
||||
String,
|
||||
Arc<dyn Fn(WeakView<DockArea>, DockItemInfo, &mut WindowContext) -> Box<dyn PanelView>>,
|
||||
>,
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,13 +182,13 @@ impl StackPanel {
|
|||
} else if let Ok(stack_panel) = panel.view().downcast::<Self>() {
|
||||
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::<TabPanel>() {
|
||||
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::<TabPanel>() {
|
||||
DockArea::subscribe_panel(&tab_panel, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
250
crates/ui/src/dock/state.rs
Normal file
250
crates/ui/src/dock/state.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
406
crates/ui/tests/fixtures/layout.json
vendored
406
crates/ui/tests/fixtures/layout.json
vendored
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue