1use gpui::{Axis, Bounds, Pixels, point, px, size};
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, 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, },
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 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#[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 #[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}