Skip to main content

gpui_base/dock/
state.rs

1use gpui::{Axis, Pixels};
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, Clone, PartialEq, Serialize, Deserialize)]
74pub enum PanelInfo {
75    #[serde(rename = "stack")]
76    Stack {
77        sizes: Vec<Pixels>,
78        axis: usize, // 0 for horizontal, 1 for vertical
79    },
80    #[serde(rename = "tabs")]
81    Tabs { active_index: usize },
82    #[serde(rename = "panel")]
83    Panel(serde_json::Value),
84}
85
86impl PanelInfo {
87    pub fn stack(sizes: Vec<Pixels>, axis: Axis) -> Self {
88        Self::Stack {
89            sizes,
90            axis: if axis == Axis::Horizontal { 0 } else { 1 },
91        }
92    }
93
94    pub fn tabs(active_index: usize) -> Self {
95        Self::Tabs { active_index }
96    }
97
98    pub fn panel(info: serde_json::Value) -> Self {
99        Self::Panel(info)
100    }
101
102    pub fn axis(&self) -> Option<Axis> {
103        match self {
104            Self::Stack { axis, .. } => Some(if *axis == 0 {
105                Axis::Horizontal
106            } else {
107                Axis::Vertical
108            }),
109            _ => None,
110        }
111    }
112
113    pub fn sizes(&self) -> Option<&Vec<Pixels>> {
114        match self {
115            Self::Stack { sizes, .. } => Some(sizes),
116            _ => None,
117        }
118    }
119
120    pub fn active_index(&self) -> Option<usize> {
121        match self {
122            Self::Tabs { active_index } => Some(*active_index),
123            _ => None,
124        }
125    }
126}
127
128impl Default for PanelState {
129    fn default() -> Self {
130        Self {
131            panel_name: "".to_string(),
132            children: Vec::new(),
133            info: PanelInfo::Panel(serde_json::Value::Null),
134        }
135    }
136}
137
138impl PanelState {
139    /// Create a new leaf state for a panel with the given name.
140    ///
141    /// The base layer has no `Panel` trait yet (`gpui_component::dock::Panel`
142    /// is layered above), so this takes the name directly rather than
143    /// deriving it from a panel value.
144    pub fn new(panel_name: impl Into<String>) -> Self {
145        Self {
146            panel_name: panel_name.into(),
147            ..Default::default()
148        }
149    }
150
151    pub fn add_child(&mut self, panel: PanelState) {
152        self.children.push(panel);
153    }
154}
155
156/// Placement of a [`Dock`](super::Dock) relative to the center area.
157///
158/// This mirrors a persisted, on-disk schema shipped to end users: the
159/// `#[serde(rename = ...)]` tags below are frozen and must not change.
160#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
161pub enum DockPlacement {
162    #[serde(rename = "center")]
163    Center,
164    #[serde(rename = "left")]
165    Left,
166    #[serde(rename = "bottom")]
167    Bottom,
168    #[serde(rename = "right")]
169    Right,
170}
171
172impl DockPlacement {
173    pub fn axis(&self) -> Axis {
174        match self {
175            Self::Left | Self::Right | Self::Center => Axis::Horizontal,
176            Self::Bottom => Axis::Vertical,
177        }
178    }
179
180    pub fn is_left(&self) -> bool {
181        matches!(self, Self::Left)
182    }
183
184    pub fn is_bottom(&self) -> bool {
185        matches!(self, Self::Bottom)
186    }
187
188    pub fn is_right(&self) -> bool {
189        matches!(self, Self::Right)
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use gpui::px;
197
198    /// The whole of a real user's file, not just its outline: every dock and
199    /// the nesting under each one. Ported from the old
200    /// `test_deserialize_item_state`, which checked all three docks and their
201    /// children — a fixture test that only reads the center and one dock
202    /// would keep passing if a dock's shape stopped deserializing at all.
203    #[test]
204    fn the_shipped_fixture_still_deserializes() {
205        let json = include_str!("fixtures/layout.json");
206        let state: DockAreaState = serde_json::from_str(json).unwrap();
207
208        assert_eq!(state.version, None);
209        assert_eq!(state.center.panel_name, "StackPanel");
210        assert_eq!(state.center.children.len(), 2);
211        assert_eq!(state.center.children[0].panel_name, "TabPanel");
212        assert_eq!(state.center.children[1].panel_name, "TabPanel");
213        assert_eq!(state.center.children[1].children.len(), 1);
214        assert_eq!(
215            state.center.children[1].children[0].panel_name,
216            "StoryContainer"
217        );
218
219        let left = state.left_dock.unwrap();
220        assert_eq!(left.open(), true);
221        assert_eq!(left.size(), px(350.0));
222        assert_eq!(left.placement(), DockPlacement::Left);
223        assert_eq!(left.panel().panel_name, "TabPanel");
224        assert_eq!(left.panel().children.len(), 1);
225        assert_eq!(left.panel().children[0].panel_name, "StoryContainer");
226
227        let bottom = state.bottom_dock.unwrap();
228        assert_eq!(bottom.open(), true);
229        assert_eq!(bottom.size(), px(200.0));
230        assert_eq!(bottom.placement(), DockPlacement::Bottom);
231        assert_eq!(bottom.panel().panel_name, "TabPanel");
232        assert_eq!(bottom.panel().children.len(), 2);
233        assert_eq!(bottom.panel().children[0].panel_name, "StoryContainer");
234
235        let right = state.right_dock.unwrap();
236        assert_eq!(right.open(), true);
237        assert_eq!(right.size(), px(320.0));
238        assert_eq!(right.placement(), DockPlacement::Right);
239        assert_eq!(right.panel().panel_name, "TabPanel");
240        assert_eq!(right.panel().children.len(), 1);
241        assert_eq!(right.panel().children[0].panel_name, "StoryContainer");
242    }
243
244    #[test]
245    fn the_serde_tags_are_frozen() {
246        let stack = serde_json::to_value(PanelInfo::stack(vec![px(1.)], Axis::Vertical)).unwrap();
247        assert_eq!(
248            stack,
249            serde_json::json!({"stack": {"sizes": [1.0], "axis": 1}})
250        );
251
252        let tabs = serde_json::to_value(PanelInfo::tabs(2)).unwrap();
253        assert_eq!(tabs, serde_json::json!({"tabs": {"active_index": 2}}));
254
255        let placement = serde_json::to_value(DockPlacement::Bottom).unwrap();
256        assert_eq!(placement, serde_json::json!("bottom"));
257    }
258
259    #[test]
260    fn optional_docks_are_omitted_not_nulled() {
261        let state = DockAreaState::default();
262        let json = serde_json::to_value(&state).unwrap();
263        assert!(json.get("left_dock").is_none());
264        assert!(json.get("right_dock").is_none());
265        assert!(json.get("bottom_dock").is_none());
266    }
267}