gpui_component/dock/
state.rs

1use gpui::{point, px, size, App, AppContext, Axis, Bounds, Entity, Pixels, WeakEntity, Window};
2use itertools::Itertools as _;
3use serde::{Deserialize, Serialize};
4
5use super::{Dock, DockArea, DockItem, DockPlacement, Panel, PanelRegistry};
6
7/// Used to serialize and deserialize the DockArea
8#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
9pub struct DockAreaState {
10    /// The version is used to mark this persisted state is compatible with the current version
11    /// For example, some times we many totally changed the structure of the Panel,
12    /// then we can compare the version to decide whether we can use the state or ignore.
13    #[serde(default)]
14    pub version: Option<usize>,
15    pub center: PanelState,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub left_dock: Option<DockState>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub right_dock: Option<DockState>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub bottom_dock: Option<DockState>,
22}
23
24/// Used to serialize and deserialize the Dock
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
26pub struct DockState {
27    panel: PanelState,
28    placement: DockPlacement,
29    size: Pixels,
30    open: bool,
31}
32
33impl DockState {
34    pub fn new(dock: Entity<Dock>, cx: &App) -> Self {
35        let dock = dock.read(cx);
36
37        Self {
38            placement: dock.placement,
39            size: dock.size,
40            open: dock.open,
41            panel: dock.panel.view().dump(cx),
42        }
43    }
44
45    /// Convert the DockState to Dock
46    pub fn to_dock(
47        &self,
48        dock_area: WeakEntity<DockArea>,
49        window: &mut Window,
50        cx: &mut App,
51    ) -> Entity<Dock> {
52        let item = self.panel.to_item(dock_area.clone(), window, cx);
53        cx.new(|cx| {
54            Dock::from_state(
55                dock_area.clone(),
56                self.placement,
57                self.size,
58                item,
59                self.open,
60                window,
61                cx,
62            )
63        })
64    }
65}
66
67/// Used to serialize and deserialize the DockerItem
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
69pub struct PanelState {
70    pub panel_name: String,
71    pub children: Vec<PanelState>,
72    pub info: PanelInfo,
73}
74
75#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
76pub struct TileMeta {
77    pub bounds: Bounds<Pixels>,
78    pub z_index: usize,
79}
80
81impl Default for TileMeta {
82    fn default() -> Self {
83        Self {
84            bounds: Bounds {
85                origin: point(px(10.), px(10.)),
86                size: size(px(200.), px(200.)),
87            },
88            z_index: 0,
89        }
90    }
91}
92
93impl From<Bounds<Pixels>> for TileMeta {
94    fn from(bounds: Bounds<Pixels>) -> Self {
95        Self { bounds, z_index: 0 }
96    }
97}
98
99#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
100pub enum PanelInfo {
101    #[serde(rename = "stack")]
102    Stack {
103        sizes: Vec<Pixels>,
104        axis: usize, // 0 for horizontal, 1 for vertical
105    },
106    #[serde(rename = "tabs")]
107    Tabs { active_index: usize },
108    #[serde(rename = "panel")]
109    Panel(serde_json::Value),
110    #[serde(rename = "tiles")]
111    Tiles { metas: Vec<TileMeta> },
112}
113
114impl PanelInfo {
115    pub fn stack(sizes: Vec<Pixels>, axis: Axis) -> Self {
116        Self::Stack {
117            sizes,
118            axis: if axis == Axis::Horizontal { 0 } else { 1 },
119        }
120    }
121
122    pub fn tabs(active_index: usize) -> Self {
123        Self::Tabs { active_index }
124    }
125
126    pub fn panel(info: serde_json::Value) -> Self {
127        Self::Panel(info)
128    }
129
130    pub fn tiles(metas: Vec<TileMeta>) -> Self {
131        Self::Tiles { metas }
132    }
133
134    pub fn axis(&self) -> Option<Axis> {
135        match self {
136            Self::Stack { axis, .. } => Some(if *axis == 0 {
137                Axis::Horizontal
138            } else {
139                Axis::Vertical
140            }),
141            _ => None,
142        }
143    }
144
145    pub fn sizes(&self) -> Option<&Vec<Pixels>> {
146        match self {
147            Self::Stack { sizes, .. } => Some(sizes),
148            _ => None,
149        }
150    }
151
152    pub fn active_index(&self) -> Option<usize> {
153        match self {
154            Self::Tabs { active_index } => Some(*active_index),
155            _ => None,
156        }
157    }
158}
159
160impl Default for PanelState {
161    fn default() -> Self {
162        Self {
163            panel_name: "".to_string(),
164            children: Vec::new(),
165            info: PanelInfo::Panel(serde_json::Value::Null),
166        }
167    }
168}
169
170impl PanelState {
171    pub fn new<P: Panel>(panel: &P) -> Self {
172        Self {
173            panel_name: panel.panel_name().to_string(),
174            ..Default::default()
175        }
176    }
177
178    pub fn add_child(&mut self, panel: PanelState) {
179        self.children.push(panel);
180    }
181
182    pub fn to_item(
183        &self,
184        dock_area: WeakEntity<DockArea>,
185        window: &mut Window,
186        cx: &mut App,
187    ) -> DockItem {
188        let info = self.info.clone();
189
190        let items: Vec<DockItem> = self
191            .children
192            .iter()
193            .map(|child| child.to_item(dock_area.clone(), window, cx))
194            .collect();
195
196        match info {
197            PanelInfo::Stack { sizes, axis } => {
198                let axis = if axis == 0 {
199                    Axis::Horizontal
200                } else {
201                    Axis::Vertical
202                };
203                let sizes = sizes.iter().map(|s| Some(*s)).collect_vec();
204                DockItem::split_with_sizes(axis, items, sizes, &dock_area, window, cx)
205            }
206            PanelInfo::Tabs { active_index } => {
207                if items.len() == 1 {
208                    return items[0].clone();
209                }
210
211                let items = items
212                    .iter()
213                    .flat_map(|item| match item {
214                        DockItem::Tabs { items, .. } => items.clone(),
215                        _ => {
216                            // ignore invalid panels in tabs
217                            vec![]
218                        }
219                    })
220                    .collect_vec();
221
222                DockItem::tabs(items, Some(active_index), &dock_area, window, cx)
223            }
224            PanelInfo::Panel(_) => {
225                let view = PanelRegistry::build_panel(
226                    &self.panel_name,
227                    dock_area.clone(),
228                    self,
229                    &info,
230                    window,
231                    cx,
232                );
233                DockItem::tabs(vec![view.into()], None, &dock_area, window, cx)
234            }
235            PanelInfo::Tiles { metas } => DockItem::tiles(items, metas, &dock_area, window, cx),
236        }
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use gpui::px;
243
244    use super::*;
245    #[test]
246    fn test_deserialize_item_state() {
247        let json = include_str!("../../tests/fixtures/layout.json");
248        let state: DockAreaState = serde_json::from_str(json).unwrap();
249        assert_eq!(state.version, None);
250        assert_eq!(state.center.panel_name, "StackPanel");
251        assert_eq!(state.center.children.len(), 2);
252        assert_eq!(state.center.children[0].panel_name, "TabPanel");
253        assert_eq!(state.center.children[1].children.len(), 1);
254        assert_eq!(
255            state.center.children[1].children[0].panel_name,
256            "StoryContainer"
257        );
258        assert_eq!(state.center.children[1].panel_name, "TabPanel");
259
260        let left_dock = state.left_dock.unwrap();
261        assert_eq!(left_dock.open, true);
262        assert_eq!(left_dock.size, px(350.0));
263        assert_eq!(left_dock.placement, DockPlacement::Left);
264        assert_eq!(left_dock.panel.panel_name, "TabPanel");
265        assert_eq!(left_dock.panel.children.len(), 1);
266        assert_eq!(left_dock.panel.children[0].panel_name, "StoryContainer");
267
268        let bottom_dock = state.bottom_dock.unwrap();
269        assert_eq!(bottom_dock.open, true);
270        assert_eq!(bottom_dock.size, px(200.0));
271        assert_eq!(bottom_dock.panel.panel_name, "TabPanel");
272        assert_eq!(bottom_dock.panel.children.len(), 2);
273        assert_eq!(bottom_dock.panel.children[0].panel_name, "StoryContainer");
274
275        let right_dock = state.right_dock.unwrap();
276        assert_eq!(right_dock.open, true);
277        assert_eq!(right_dock.size, px(320.0));
278        assert_eq!(right_dock.panel.panel_name, "TabPanel");
279        assert_eq!(right_dock.panel.children.len(), 1);
280        assert_eq!(right_dock.panel.children[0].panel_name, "StoryContainer");
281    }
282}