1use gpui::{Axis, Pixels};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
10pub struct DockAreaState {
11 #[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#[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#[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, },
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 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#[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 #[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}