Skip to main content

gpui_base/dock/
state.rs

1use gpui::{Axis, Bounds, Pixels, point, px, size};
2use serde::{Deserialize, Serialize};
3
4/// Used to serialize and deserialize the DockArea.
5///
6/// This mirrors a persisted, on-disk schema shipped to end users. Its fields
7/// stay `pub` rather than following the seam's builder/reader convention —
8/// see "Public Data Types Across the Seam" in `docs/ARCHITECTURE.md`.
9#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
10pub struct DockAreaState {
11    /// The version is used to mark this persisted state is compatible with the current version
12    /// For example, some times we many totally changed the structure of the Panel,
13    /// then we can compare the version to decide whether we can use the state or ignore.
14    #[serde(default)]
15    pub version: Option<usize>,
16    pub center: PanelState,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub left_dock: Option<DockState>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub right_dock: Option<DockState>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub bottom_dock: Option<DockState>,
23}
24
25/// Used to serialize and deserialize the Dock.
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
27pub struct DockState {
28    panel: PanelState,
29    placement: DockPlacement,
30    size: Pixels,
31    open: bool,
32}
33
34impl DockState {
35    pub fn new(panel: PanelState, placement: DockPlacement, size: Pixels, open: bool) -> Self {
36        Self {
37            panel,
38            placement,
39            size,
40            open,
41        }
42    }
43
44    pub fn panel(&self) -> &PanelState {
45        &self.panel
46    }
47
48    pub fn placement(&self) -> DockPlacement {
49        self.placement
50    }
51
52    pub fn size(&self) -> Pixels {
53        self.size
54    }
55
56    pub fn open(&self) -> bool {
57        self.open
58    }
59}
60
61/// Used to serialize and deserialize the DockerItem.
62///
63/// This mirrors a persisted, on-disk schema shipped to end users. Its fields
64/// stay `pub` rather than following the seam's builder/reader convention —
65/// see "Public Data Types Across the Seam" in `docs/ARCHITECTURE.md`.
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
67pub struct PanelState {
68    pub panel_name: String,
69    pub children: Vec<PanelState>,
70    pub info: PanelInfo,
71}
72
73#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
74pub struct TileMeta {
75    pub bounds: Bounds<Pixels>,
76    pub z_index: usize,
77}
78
79impl Default for TileMeta {
80    fn default() -> Self {
81        Self {
82            bounds: Bounds {
83                origin: point(px(10.), px(10.)),
84                size: size(px(200.), px(200.)),
85            },
86            z_index: 0,
87        }
88    }
89}
90
91impl From<Bounds<Pixels>> for TileMeta {
92    fn from(bounds: Bounds<Pixels>) -> Self {
93        Self { bounds, z_index: 0 }
94    }
95}
96
97#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
98pub enum PanelInfo {
99    #[serde(rename = "stack")]
100    Stack {
101        sizes: Vec<Pixels>,
102        axis: usize, // 0 for horizontal, 1 for vertical
103    },
104    #[serde(rename = "tabs")]
105    Tabs { active_index: usize },
106    #[serde(rename = "panel")]
107    Panel(serde_json::Value),
108    #[serde(rename = "tiles")]
109    Tiles { metas: Vec<TileMeta> },
110}
111
112impl PanelInfo {
113    pub fn stack(sizes: Vec<Pixels>, axis: Axis) -> Self {
114        Self::Stack {
115            sizes,
116            axis: if axis == Axis::Horizontal { 0 } else { 1 },
117        }
118    }
119
120    pub fn tabs(active_index: usize) -> Self {
121        Self::Tabs { active_index }
122    }
123
124    pub fn panel(info: serde_json::Value) -> Self {
125        Self::Panel(info)
126    }
127
128    pub fn tiles(metas: Vec<TileMeta>) -> Self {
129        Self::Tiles { metas }
130    }
131
132    pub fn axis(&self) -> Option<Axis> {
133        match self {
134            Self::Stack { axis, .. } => Some(if *axis == 0 {
135                Axis::Horizontal
136            } else {
137                Axis::Vertical
138            }),
139            _ => None,
140        }
141    }
142
143    pub fn sizes(&self) -> Option<&Vec<Pixels>> {
144        match self {
145            Self::Stack { sizes, .. } => Some(sizes),
146            _ => None,
147        }
148    }
149
150    pub fn active_index(&self) -> Option<usize> {
151        match self {
152            Self::Tabs { active_index } => Some(*active_index),
153            _ => None,
154        }
155    }
156}
157
158impl Default for PanelState {
159    fn default() -> Self {
160        Self {
161            panel_name: "".to_string(),
162            children: Vec::new(),
163            info: PanelInfo::Panel(serde_json::Value::Null),
164        }
165    }
166}
167
168impl PanelState {
169    /// Create a new leaf state for a panel with the given name.
170    ///
171    /// The base layer has no `Panel` trait yet (`gpui_component::dock::Panel`
172    /// is layered above), so this takes the name directly rather than
173    /// deriving it from a panel value.
174    pub fn new(panel_name: impl Into<String>) -> Self {
175        Self {
176            panel_name: panel_name.into(),
177            ..Default::default()
178        }
179    }
180
181    pub fn add_child(&mut self, panel: PanelState) {
182        self.children.push(panel);
183    }
184}
185
186/// Placement of a [`Dock`](super::Dock) relative to the center area.
187///
188/// This mirrors a persisted, on-disk schema shipped to end users: the
189/// `#[serde(rename = ...)]` tags below are frozen and must not change.
190#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
191pub enum DockPlacement {
192    #[serde(rename = "center")]
193    Center,
194    #[serde(rename = "left")]
195    Left,
196    #[serde(rename = "bottom")]
197    Bottom,
198    #[serde(rename = "right")]
199    Right,
200}
201
202impl DockPlacement {
203    pub fn axis(&self) -> Axis {
204        match self {
205            Self::Left | Self::Right | Self::Center => Axis::Horizontal,
206            Self::Bottom => Axis::Vertical,
207        }
208    }
209
210    pub fn is_left(&self) -> bool {
211        matches!(self, Self::Left)
212    }
213
214    pub fn is_bottom(&self) -> bool {
215        matches!(self, Self::Bottom)
216    }
217
218    pub fn is_right(&self) -> bool {
219        matches!(self, Self::Right)
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use gpui::px;
227
228    /// The whole of a real user's file, not just its outline: every dock and
229    /// the nesting under each one. Ported from the old
230    /// `test_deserialize_item_state`, which checked all three docks and their
231    /// children — a fixture test that only reads the center and one dock
232    /// would keep passing if a dock's shape stopped deserializing at all.
233    #[test]
234    fn the_shipped_fixture_still_deserializes() {
235        let json = include_str!("fixtures/layout.json");
236        let state: DockAreaState = serde_json::from_str(json).unwrap();
237
238        assert_eq!(state.version, None);
239        assert_eq!(state.center.panel_name, "StackPanel");
240        assert_eq!(state.center.children.len(), 2);
241        assert_eq!(state.center.children[0].panel_name, "TabPanel");
242        assert_eq!(state.center.children[1].panel_name, "TabPanel");
243        assert_eq!(state.center.children[1].children.len(), 1);
244        assert_eq!(
245            state.center.children[1].children[0].panel_name,
246            "StoryContainer"
247        );
248
249        let left = state.left_dock.unwrap();
250        assert_eq!(left.open(), true);
251        assert_eq!(left.size(), px(350.0));
252        assert_eq!(left.placement(), DockPlacement::Left);
253        assert_eq!(left.panel().panel_name, "TabPanel");
254        assert_eq!(left.panel().children.len(), 1);
255        assert_eq!(left.panel().children[0].panel_name, "StoryContainer");
256
257        let bottom = state.bottom_dock.unwrap();
258        assert_eq!(bottom.open(), true);
259        assert_eq!(bottom.size(), px(200.0));
260        assert_eq!(bottom.placement(), DockPlacement::Bottom);
261        assert_eq!(bottom.panel().panel_name, "TabPanel");
262        assert_eq!(bottom.panel().children.len(), 2);
263        assert_eq!(bottom.panel().children[0].panel_name, "StoryContainer");
264
265        let right = state.right_dock.unwrap();
266        assert_eq!(right.open(), true);
267        assert_eq!(right.size(), px(320.0));
268        assert_eq!(right.placement(), DockPlacement::Right);
269        assert_eq!(right.panel().panel_name, "TabPanel");
270        assert_eq!(right.panel().children.len(), 1);
271        assert_eq!(right.panel().children[0].panel_name, "StoryContainer");
272    }
273
274    #[test]
275    fn the_serde_tags_are_frozen() {
276        let stack = serde_json::to_value(PanelInfo::stack(vec![px(1.)], Axis::Vertical)).unwrap();
277        assert_eq!(
278            stack,
279            serde_json::json!({"stack": {"sizes": [1.0], "axis": 1}})
280        );
281
282        let tabs = serde_json::to_value(PanelInfo::tabs(2)).unwrap();
283        assert_eq!(tabs, serde_json::json!({"tabs": {"active_index": 2}}));
284
285        let placement = serde_json::to_value(DockPlacement::Bottom).unwrap();
286        assert_eq!(placement, serde_json::json!("bottom"));
287    }
288
289    #[test]
290    fn optional_docks_are_omitted_not_nulled() {
291        let state = DockAreaState::default();
292        let json = serde_json::to_value(&state).unwrap();
293        assert!(json.get("left_dock").is_none());
294        assert!(json.get("right_dock").is_none());
295        assert!(json.get("bottom_dock").is_none());
296    }
297}