Skip to main content

fret_core/dock/
persistence.rs

1use super::*;
2
3impl DockGraph {
4    pub fn export_layout(&self, windows: &[(AppWindowId, String)]) -> crate::DockLayout {
5        self.export_layout_with_placement(windows, |_| None)
6    }
7
8    pub fn export_layout_with_placement(
9        &self,
10        windows: &[(AppWindowId, String)],
11        mut placement: impl FnMut(AppWindowId) -> Option<crate::DockWindowPlacement>,
12    ) -> crate::DockLayout {
13        use crate::{DockLayoutFloatingWindow, DockLayoutNode, DockLayoutWindow};
14        use std::collections::HashMap;
15
16        fn visit(
17            graph: &DockGraph,
18            node: DockNodeId,
19            next_id: &mut u32,
20            ids: &mut HashMap<DockNodeId, u32>,
21            out: &mut Vec<DockLayoutNode>,
22        ) {
23            if ids.contains_key(&node) {
24                return;
25            }
26
27            let id = *next_id;
28            *next_id = next_id.saturating_add(1);
29            ids.insert(node, id);
30
31            let Some(n) = graph.nodes.get(node) else {
32                return;
33            };
34
35            match n {
36                DockNode::Tabs { tabs, active } => {
37                    out.push(DockLayoutNode::Tabs {
38                        id,
39                        tabs: tabs.clone(),
40                        active: *active,
41                    });
42                }
43                DockNode::Split {
44                    axis,
45                    children,
46                    fractions,
47                } => {
48                    for child in children {
49                        visit(graph, *child, next_id, ids, out);
50                    }
51                    let child_ids: Vec<u32> = children
52                        .iter()
53                        .filter_map(|c| ids.get(c).copied())
54                        .collect();
55                    out.push(DockLayoutNode::Split {
56                        id,
57                        axis: *axis,
58                        children: child_ids,
59                        fractions: fractions.clone(),
60                    });
61                }
62                DockNode::Floating { child } => {
63                    visit(graph, *child, next_id, ids, out);
64                    if let Some(&child_id) = ids.get(child) {
65                        ids.insert(node, child_id);
66                    }
67                }
68            }
69        }
70
71        let mut next_id: u32 = 1;
72        let mut ids: HashMap<DockNodeId, u32> = HashMap::new();
73        let mut nodes: Vec<DockLayoutNode> = Vec::new();
74        let mut out_windows: Vec<DockLayoutWindow> = Vec::new();
75
76        for (window, logical_window_id) in windows {
77            let Some(root) = self.window_root(*window) else {
78                continue;
79            };
80            visit(self, root, &mut next_id, &mut ids, &mut nodes);
81            let Some(root_id) = ids.get(&root).copied() else {
82                continue;
83            };
84
85            let mut floatings: Vec<DockLayoutFloatingWindow> = Vec::new();
86            for floating in self.floating_windows(*window) {
87                visit(self, floating.floating, &mut next_id, &mut ids, &mut nodes);
88                let Some(floating_root) = ids.get(&floating.floating).copied() else {
89                    continue;
90                };
91                floatings.push(DockLayoutFloatingWindow {
92                    root: floating_root,
93                    rect: crate::DockRect::from_rect(floating.rect),
94                });
95            }
96
97            out_windows.push(DockLayoutWindow {
98                logical_window_id: logical_window_id.clone(),
99                root: root_id,
100                placement: placement(*window),
101                floatings,
102            });
103        }
104
105        crate::DockLayout::new(out_windows, nodes)
106    }
107
108    pub fn import_subtree_from_layout(
109        &mut self,
110        layout: &crate::DockLayout,
111        root: u32,
112    ) -> Option<DockNodeId> {
113        use crate::DockLayoutNode;
114        use std::collections::HashMap;
115
116        if layout.layout_version != crate::DOCK_LAYOUT_VERSION {
117            return None;
118        }
119
120        let mut by_id: HashMap<u32, &DockLayoutNode> = HashMap::new();
121        for node in &layout.nodes {
122            let id = match node {
123                DockLayoutNode::Split { id, .. } => *id,
124                DockLayoutNode::Tabs { id, .. } => *id,
125            };
126            by_id.insert(id, node);
127        }
128
129        fn build(
130            graph: &mut DockGraph,
131            by_id: &HashMap<u32, &DockLayoutNode>,
132            id: u32,
133            visiting: &mut HashMap<u32, ()>,
134        ) -> Option<DockNodeId> {
135            if visiting.contains_key(&id) {
136                return None;
137            }
138            visiting.insert(id, ());
139
140            let node = by_id.get(&id)?;
141            let out = match node {
142                DockLayoutNode::Tabs { tabs, active, .. } => {
143                    Some(graph.insert_node(DockNode::Tabs {
144                        tabs: tabs.clone(),
145                        active: *active,
146                    }))
147                }
148                DockLayoutNode::Split {
149                    axis,
150                    children,
151                    fractions,
152                    ..
153                } => {
154                    let mut child_nodes: Vec<DockNodeId> = Vec::new();
155                    for child in children {
156                        child_nodes.push(build(graph, by_id, *child, visiting)?);
157                    }
158                    Some(graph.insert_node(DockNode::Split {
159                        axis: *axis,
160                        children: child_nodes,
161                        fractions: fractions.clone(),
162                    }))
163                }
164            };
165
166            visiting.remove(&id);
167            out
168        }
169
170        let mut visiting: HashMap<u32, ()> = HashMap::new();
171        build(self, &by_id, root, &mut visiting)
172    }
173
174    pub fn import_subtree_from_layout_checked(
175        &mut self,
176        layout: &crate::DockLayout,
177        root: u32,
178    ) -> Result<DockNodeId, crate::DockLayoutValidationError> {
179        use crate::DockLayoutNode;
180        use std::collections::HashMap;
181
182        layout.validate()?;
183
184        let mut by_id: HashMap<u32, &DockLayoutNode> = HashMap::new();
185        for node in &layout.nodes {
186            let id = match node {
187                DockLayoutNode::Split { id, .. } => *id,
188                DockLayoutNode::Tabs { id, .. } => *id,
189            };
190            by_id.insert(id, node);
191        }
192
193        let mut built: HashMap<u32, DockNodeId> = HashMap::new();
194        fn build_checked(
195            graph: &mut DockGraph,
196            by_id: &HashMap<u32, &DockLayoutNode>,
197            built: &mut HashMap<u32, DockNodeId>,
198            id: u32,
199        ) -> DockNodeId {
200            if let Some(&node) = built.get(&id) {
201                return node;
202            }
203
204            let node = by_id
205                .get(&id)
206                .copied()
207                .expect("layout.validate ensures node id exists");
208
209            let out = match node {
210                DockLayoutNode::Tabs { tabs, active, .. } => graph.insert_node(DockNode::Tabs {
211                    tabs: tabs.clone(),
212                    active: *active,
213                }),
214                DockLayoutNode::Split {
215                    axis,
216                    children,
217                    fractions,
218                    ..
219                } => {
220                    let mut child_nodes: Vec<DockNodeId> = Vec::new();
221                    for child in children {
222                        child_nodes.push(build_checked(graph, by_id, built, *child));
223                    }
224                    graph.insert_node(DockNode::Split {
225                        axis: *axis,
226                        children: child_nodes,
227                        fractions: fractions.clone(),
228                    })
229                }
230            };
231
232            built.insert(id, out);
233            out
234        }
235
236        if !by_id.contains_key(&root) {
237            return Err(crate::DockLayoutValidationError {
238                kind: crate::DockLayoutValidationErrorKind::MissingNodeId { id: root },
239            });
240        }
241
242        Ok(build_checked(self, &by_id, &mut built, root))
243    }
244
245    pub fn import_layout_for_windows(
246        &mut self,
247        layout: &crate::DockLayout,
248        windows: &[(AppWindowId, String)],
249    ) -> bool {
250        use std::collections::HashMap;
251
252        if layout.layout_version != crate::DOCK_LAYOUT_VERSION {
253            return false;
254        }
255
256        let mut by_logical: HashMap<&str, AppWindowId> = HashMap::new();
257        for (window, logical_id) in windows {
258            by_logical.insert(logical_id.as_str(), *window);
259        }
260
261        let mut imported_any = false;
262        for w in &layout.windows {
263            let Some(window) = by_logical.get(w.logical_window_id.as_str()).copied() else {
264                continue;
265            };
266
267            let Some(root) = self.import_subtree_from_layout(layout, w.root) else {
268                continue;
269            };
270            self.set_window_root(window, root);
271
272            self.floating_windows_mut(window).clear();
273            for f in &w.floatings {
274                let Some(child) = self.import_subtree_from_layout(layout, f.root) else {
275                    continue;
276                };
277                let floating = self.insert_node(DockNode::Floating { child });
278                self.floating_windows_mut(window).push(DockFloatingWindow {
279                    floating,
280                    rect: f.rect.to_rect(),
281                });
282            }
283
284            self.simplify_window_forest(window);
285            imported_any = true;
286        }
287
288        imported_any
289    }
290
291    pub fn import_layout_for_windows_checked(
292        &mut self,
293        layout: &crate::DockLayout,
294        windows: &[(AppWindowId, String)],
295    ) -> Result<bool, crate::DockLayoutValidationError> {
296        use std::collections::HashMap;
297
298        layout.validate()?;
299
300        let mut by_logical: HashMap<&str, AppWindowId> = HashMap::new();
301        for (window, logical_id) in windows {
302            by_logical.insert(logical_id.as_str(), *window);
303        }
304
305        let mut imported_any = false;
306        for w in &layout.windows {
307            let Some(window) = by_logical.get(w.logical_window_id.as_str()).copied() else {
308                continue;
309            };
310
311            let root = self.import_subtree_from_layout_checked(layout, w.root)?;
312            self.set_window_root(window, root);
313
314            self.floating_windows_mut(window).clear();
315            for f in &w.floatings {
316                let child = self.import_subtree_from_layout_checked(layout, f.root)?;
317                let floating = self.insert_node(DockNode::Floating { child });
318                self.floating_windows_mut(window).push(DockFloatingWindow {
319                    floating,
320                    rect: f.rect.to_rect(),
321                });
322            }
323
324            self.simplify_window_forest(window);
325            imported_any = true;
326        }
327
328        Ok(imported_any)
329    }
330
331    /// Import a dock layout for a set of known windows, degrading any unmapped logical windows
332    /// into in-window floating containers inside `fallback_window`.
333    ///
334    /// This enables loading multi-window layouts on platforms that do not support multiple OS
335    /// windows (wasm/mobile). The extra logical windows become floating dock containers rendered
336    /// by the dock host in `fallback_window`.
337    pub fn import_layout_for_windows_with_fallback_floatings(
338        &mut self,
339        layout: &crate::DockLayout,
340        windows: &[(AppWindowId, String)],
341        fallback_window: AppWindowId,
342    ) -> bool {
343        use std::collections::HashMap;
344
345        if layout.layout_version != crate::DOCK_LAYOUT_VERSION {
346            return false;
347        }
348
349        fn offset_rect(rect: Rect, delta: Point) -> Rect {
350            Rect::new(
351                Point::new(
352                    Px(rect.origin.x.0 + delta.x.0),
353                    Px(rect.origin.y.0 + delta.y.0),
354                ),
355                rect.size,
356            )
357        }
358
359        fn rect_for_unmapped_window(w: &crate::DockLayoutWindow, index: usize) -> Rect {
360            let default_w = 640.0;
361            let default_h = 480.0;
362            let (w_px, h_px) = w
363                .placement
364                .as_ref()
365                .map(|p| (p.width as f32, p.height as f32))
366                .unwrap_or((default_w, default_h));
367
368            let width = w_px.clamp(240.0, 1400.0);
369            let height = h_px.clamp(180.0, 1000.0);
370
371            let stagger = (index as f32).min(12.0) * 24.0;
372            Rect::new(
373                Point::new(Px(32.0 + stagger), Px(32.0 + stagger)),
374                Size::new(Px(width), Px(height)),
375            )
376        }
377
378        let mut by_logical: HashMap<&str, AppWindowId> = HashMap::new();
379        for (window, logical_id) in windows {
380            by_logical.insert(logical_id.as_str(), *window);
381        }
382
383        // Clear and re-import all floating windows for `fallback_window` so the resulting state is
384        // deterministic when this method is used as a "load persisted layout" entry point.
385        self.floating_windows_mut(fallback_window).clear();
386
387        let mut imported_any = false;
388        let mut unmapped_index: usize = 0;
389
390        for w in &layout.windows {
391            if let Some(window) = by_logical.get(w.logical_window_id.as_str()).copied() {
392                let Some(root) = self.import_subtree_from_layout(layout, w.root) else {
393                    continue;
394                };
395                self.set_window_root(window, root);
396
397                self.floating_windows_mut(window).clear();
398                for f in &w.floatings {
399                    let Some(child) = self.import_subtree_from_layout(layout, f.root) else {
400                        continue;
401                    };
402                    let floating = self.insert_node(DockNode::Floating { child });
403                    self.floating_windows_mut(window).push(DockFloatingWindow {
404                        floating,
405                        rect: f.rect.to_rect(),
406                    });
407                }
408
409                self.simplify_window_forest(window);
410                imported_any = true;
411                continue;
412            }
413
414            // Unmapped logical window: import it as a floating container inside `fallback_window`.
415            let Some(child) = self.import_subtree_from_layout(layout, w.root) else {
416                continue;
417            };
418            let window_rect = rect_for_unmapped_window(w, unmapped_index);
419            let floating = self.insert_node(DockNode::Floating { child });
420            self.floating_windows_mut(fallback_window)
421                .push(DockFloatingWindow {
422                    floating,
423                    rect: window_rect,
424                });
425
426            for f in &w.floatings {
427                let Some(child) = self.import_subtree_from_layout(layout, f.root) else {
428                    continue;
429                };
430                let floating = self.insert_node(DockNode::Floating { child });
431                self.floating_windows_mut(fallback_window)
432                    .push(DockFloatingWindow {
433                        floating,
434                        rect: offset_rect(f.rect.to_rect(), window_rect.origin),
435                    });
436            }
437
438            unmapped_index = unmapped_index.saturating_add(1);
439            imported_any = true;
440        }
441
442        self.simplify_window_forest(fallback_window);
443        imported_any
444    }
445
446    pub fn import_layout_for_windows_with_fallback_floatings_checked(
447        &mut self,
448        layout: &crate::DockLayout,
449        windows: &[(AppWindowId, String)],
450        fallback_window: AppWindowId,
451    ) -> Result<bool, crate::DockLayoutValidationError> {
452        use std::collections::HashMap;
453
454        layout.validate()?;
455
456        fn offset_rect(rect: Rect, delta: Point) -> Rect {
457            Rect::new(
458                Point::new(
459                    Px(rect.origin.x.0 + delta.x.0),
460                    Px(rect.origin.y.0 + delta.y.0),
461                ),
462                rect.size,
463            )
464        }
465
466        fn rect_for_unmapped_window(w: &crate::DockLayoutWindow, index: usize) -> Rect {
467            let default_w = 640.0;
468            let default_h = 480.0;
469            let (w_px, h_px) = w
470                .placement
471                .as_ref()
472                .map(|p| (p.width as f32, p.height as f32))
473                .unwrap_or((default_w, default_h));
474
475            let width = w_px.clamp(240.0, 1400.0);
476            let height = h_px.clamp(180.0, 1000.0);
477
478            let stagger = (index as f32).min(12.0) * 24.0;
479            Rect::new(
480                Point::new(Px(32.0 + stagger), Px(32.0 + stagger)),
481                Size::new(Px(width), Px(height)),
482            )
483        }
484
485        let mut by_logical: HashMap<&str, AppWindowId> = HashMap::new();
486        for (window, logical_id) in windows {
487            by_logical.insert(logical_id.as_str(), *window);
488        }
489
490        self.floating_windows_mut(fallback_window).clear();
491
492        let mut imported_any = false;
493        let mut unmapped_index: usize = 0;
494
495        for w in &layout.windows {
496            if let Some(window) = by_logical.get(w.logical_window_id.as_str()).copied() {
497                let root = self.import_subtree_from_layout_checked(layout, w.root)?;
498                self.set_window_root(window, root);
499
500                self.floating_windows_mut(window).clear();
501                for f in &w.floatings {
502                    let child = self.import_subtree_from_layout_checked(layout, f.root)?;
503                    let floating = self.insert_node(DockNode::Floating { child });
504                    self.floating_windows_mut(window).push(DockFloatingWindow {
505                        floating,
506                        rect: f.rect.to_rect(),
507                    });
508                }
509
510                self.simplify_window_forest(window);
511                imported_any = true;
512                continue;
513            }
514
515            let child = self.import_subtree_from_layout_checked(layout, w.root)?;
516            let window_rect = rect_for_unmapped_window(w, unmapped_index);
517            let floating = self.insert_node(DockNode::Floating { child });
518            self.floating_windows_mut(fallback_window)
519                .push(DockFloatingWindow {
520                    floating,
521                    rect: window_rect,
522                });
523
524            for f in &w.floatings {
525                let child = self.import_subtree_from_layout_checked(layout, f.root)?;
526                let floating = self.insert_node(DockNode::Floating { child });
527                self.floating_windows_mut(fallback_window)
528                    .push(DockFloatingWindow {
529                        floating,
530                        rect: offset_rect(f.rect.to_rect(), window_rect.origin),
531                    });
532            }
533
534            unmapped_index = unmapped_index.saturating_add(1);
535            imported_any = true;
536        }
537
538        self.simplify_window_forest(fallback_window);
539        Ok(imported_any)
540    }
541}