Skip to main content

sequoia_dock/
ops.rs

1use crate::drop::DockDrop;
2use crate::model::{DockModel, DockNode, FloatingPanel, PanelId, SplitAxis};
3
4#[derive(Clone, Debug, PartialEq)]
5#[non_exhaustive]
6pub enum DockOp {
7    Insert {
8        panel: PanelId,
9        target: PanelId,
10        drop: DockDrop,
11        frac: f32,
12        moving: bool,
13    },
14
15    Float {
16        panel: PanelId,
17        x: f32,
18        y: f32,
19        w: f32,
20        h: f32,
21    },
22
23    DockFloating {
24        panel: PanelId,
25        target: PanelId,
26        drop: DockDrop,
27        frac: f32,
28    },
29
30    Close {
31        panel: PanelId,
32    },
33
34    SetFrac {
35        panel: PanelId,
36        frac: f32,
37    },
38
39    Activate {
40        panel: PanelId,
41    },
42
43    MoveTab {
44        panel: PanelId,
45        to: usize,
46    },
47}
48
49impl DockModel {
50    pub fn apply(&mut self, op: DockOp) -> bool {
51        match op {
52            DockOp::Insert {
53                panel,
54                target,
55                drop,
56                frac,
57                moving,
58            } => self.insert(panel, target, drop, frac, moving),
59            DockOp::Float { panel, x, y, w, h } => self.float(panel, x, y, w, h),
60            DockOp::DockFloating {
61                panel,
62                target,
63                drop,
64                frac,
65            } => self.dock_floating(panel, target, drop, frac),
66            DockOp::Close { panel } => self.close(panel),
67            DockOp::SetFrac { panel, frac } => self.set_frac(panel, frac),
68            DockOp::Activate { panel } => self.activate(panel),
69            DockOp::MoveTab { panel, to } => self.move_tab(panel, to),
70        }
71    }
72
73    pub fn close(&mut self, panel: PanelId) -> bool {
74        let mut removed = false;
75        if let Some(root) = self.root.take() {
76            match remove_from_node(root, panel) {
77                (None, true) => removed = true,
78                (Some(r), found) => {
79                    self.root = Some(r);
80                    removed = found;
81                }
82                (None, false) => {}
83            }
84        }
85        let n = self.floating.len();
86        self.floating.retain(|f| f.panel != panel);
87        if self.floating.len() != n {
88            removed = true;
89        }
90        self.sanitize();
91        removed
92    }
93
94    fn take_from_tree(&mut self, panel: PanelId) -> bool {
95        if let Some(root) = self.root.take() {
96            match remove_from_node(root, panel) {
97                (None, found) => found,
98                (Some(r), found) => {
99                    self.root = Some(r);
100                    found
101                }
102            }
103        } else {
104            false
105        }
106    }
107
108    fn insert(
109        &mut self,
110        panel: PanelId,
111        target: PanelId,
112        drop: DockDrop,
113        frac: f32,
114        moving: bool,
115    ) -> bool {
116        if panel == target {
117            return false;
118        }
119        if drop == DockDrop::Float {
120            return false;
121        }
122
123        let target_in_tree = self.root.as_ref().is_some_and(|r| r.contains(target));
124        if !target_in_tree {
125            return false;
126        }
127        if moving {
128            let from_tree = self.take_from_tree(panel);
129            let from_float = {
130                let n = self.floating.len();
131                self.floating.retain(|f| f.panel != panel);
132                self.floating.len() != n
133            };
134            if !from_tree && !from_float {}
135
136            if self.root.is_none() {
137                self.root = Some(DockNode::Leaf(panel));
138                self.sanitize();
139                return true;
140            }
141            if !self.root.as_ref().is_some_and(|r| r.contains(target)) {
142                self.root = Some(DockNode::Leaf(panel));
143                self.sanitize();
144                return true;
145            }
146        } else if self.contains(panel) {
147            return false;
148        }
149        let f = DockNode::sanitized_frac(frac);
150        let mut root = self.root.take();
151        let ok = match &mut root {
152            Some(r) => insert_into_node(r, panel, target, drop, f),
153            None => false,
154        };
155        if !ok {
156            self.root = root;
157            return false;
158        }
159        self.root = root;
160        self.sanitize();
161        true
162    }
163
164    pub fn float(&mut self, panel: PanelId, x: f32, y: f32, w: f32, h: f32) -> bool {
165        if self.is_floating(panel) {
166            if let Some(f) = self.floating.iter_mut().find(|f| f.panel == panel) {
167                if x.is_finite() {
168                    f.x = x;
169                }
170                if y.is_finite() {
171                    f.y = y;
172                }
173                if w.is_finite() && w > 0.0 {
174                    f.w = w;
175                }
176                if h.is_finite() && h > 0.0 {
177                    f.h = h;
178                }
179                return true;
180            }
181            return false;
182        }
183        if !self.root.as_ref().is_some_and(|r| r.contains(panel)) {
184            return false;
185        }
186        self.take_from_tree(panel);
187
188        self.floating
189            .push(FloatingPanel::overlay(panel, x, y, w, h));
190        self.sanitize();
191        true
192    }
193
194    fn dock_floating(
195        &mut self,
196        panel: PanelId,
197        target: PanelId,
198        drop: DockDrop,
199        frac: f32,
200    ) -> bool {
201        if panel == target || drop == DockDrop::Float {
202            return false;
203        }
204        if !self.is_floating(panel) {
205            return false;
206        }
207        if !self.root.as_ref().is_some_and(|r| r.contains(target)) {
208            if self.root.is_none() {
209                self.floating.retain(|f| f.panel != panel);
210                self.root = Some(DockNode::Leaf(panel));
211                self.sanitize();
212                return true;
213            }
214            return false;
215        }
216        self.floating.retain(|f| f.panel != panel);
217        let f = DockNode::sanitized_frac(frac);
218        let mut root = self.root.take();
219        let ok = match &mut root {
220            Some(r) => insert_into_node(r, panel, target, drop, f),
221            None => false,
222        };
223        self.root = root;
224        if !ok {
225            self.floating
226                .push(FloatingPanel::overlay(panel, 40.0, 40.0, 320.0, 240.0));
227            return false;
228        }
229        self.sanitize();
230        true
231    }
232
233    fn set_frac(&mut self, panel: PanelId, frac: f32) -> bool {
234        let f = DockNode::sanitized_frac(frac);
235        fn go(node: &mut DockNode, panel: PanelId, f: f32) -> bool {
236            match node {
237                DockNode::Leaf(_) | DockNode::Tabs { .. } => false,
238                DockNode::Split {
239                    frac,
240                    first,
241                    second,
242                    ..
243                } => {
244                    if first.contains(panel) || second.contains(panel) {
245                        if go(first, panel, f) || go(second, panel, f) {
246                            return true;
247                        }
248                        *frac = f;
249                        true
250                    } else {
251                        false
252                    }
253                }
254            }
255        }
256        let ok = self.root.as_mut().is_some_and(|r| go(r, panel, f));
257        if ok {
258            self.sanitize();
259        }
260        ok
261    }
262
263    fn activate(&mut self, panel: PanelId) -> bool {
264        fn go(node: &mut DockNode, panel: PanelId) -> bool {
265            match node {
266                DockNode::Leaf(_) => false,
267                DockNode::Tabs { tabs, active } => {
268                    if let Some(i) = tabs.iter().position(|p| *p == panel) {
269                        *active = i;
270                        true
271                    } else {
272                        false
273                    }
274                }
275                DockNode::Split { first, second, .. } => go(first, panel) || go(second, panel),
276            }
277        }
278        let ok = self.root.as_mut().is_some_and(|r| go(r, panel));
279        if ok {
280            self.sanitize();
281        }
282        ok
283    }
284
285    fn move_tab(&mut self, panel: PanelId, to: usize) -> bool {
286        fn go(node: &mut DockNode, panel: PanelId, to: usize) -> bool {
287            match node {
288                DockNode::Leaf(_) => false,
289                DockNode::Tabs { tabs, active } => {
290                    if let Some(from) = tabs.iter().position(|p| *p == panel) {
291                        let item = tabs.remove(from);
292                        let t = to.min(tabs.len());
293                        tabs.insert(t, item);
294                        *active = t;
295                        true
296                    } else {
297                        false
298                    }
299                }
300                DockNode::Split { first, second, .. } => {
301                    go(first, panel, to) || go(second, panel, to)
302                }
303            }
304        }
305        let ok = self.root.as_mut().is_some_and(|r| go(r, panel, to));
306        if ok {
307            self.sanitize();
308        }
309        ok
310    }
311
312    pub fn attach_window(&mut self, panel: PanelId, window: Option<u64>) -> bool {
313        if let Some(f) = self.floating.iter_mut().find(|f| f.panel == panel) {
314            f.window = window;
315            true
316        } else {
317            false
318        }
319    }
320
321    pub fn set_frac_at(&mut self, path: &[bool], frac: f32) -> bool {
322        let f = DockNode::sanitized_frac(frac);
323        let mut node = match &mut self.root {
324            Some(r) => r,
325            None => return false,
326        };
327        for step in path {
328            match node {
329                DockNode::Split { first, second, .. } => {
330                    node = if *step { second } else { first };
331                }
332                _ => return false,
333            }
334        }
335        match node {
336            DockNode::Split { frac: slot, .. } => {
337                *slot = f;
338                self.sanitize();
339                true
340            }
341            _ => false,
342        }
343    }
344
345    pub fn dock_first_tab(&mut self, panel: PanelId) -> bool {
346        if !self.is_floating(panel) {
347            return false;
348        }
349        let target = match &self.root {
350            None => {
351                self.floating.retain(|f| f.panel != panel);
352                self.root = Some(DockNode::Leaf(panel));
353                self.sanitize();
354                return true;
355            }
356            Some(root) => match root.panels().first().copied() {
357                Some(t) => t,
358                None => {
359                    self.floating.retain(|f| f.panel != panel);
360                    self.root = Some(DockNode::Leaf(panel));
361                    self.sanitize();
362                    return true;
363                }
364            },
365        };
366        self.dock_floating(panel, target, crate::drop::DockDrop::Tab, 0.5)
367    }
368
369    pub fn frac_at(&self, path: &[bool]) -> Option<f32> {
370        let mut node = self.root.as_ref()?;
371        for step in path {
372            match node {
373                DockNode::Split { first, second, .. } => {
374                    node = if *step { second } else { first };
375                }
376                _ => return None,
377            }
378        }
379        match node {
380            DockNode::Split { frac, .. } => Some(*frac),
381            _ => None,
382        }
383    }
384}
385
386fn insert_into_node(
387    node: &mut DockNode,
388    panel: PanelId,
389    target: PanelId,
390    drop: DockDrop,
391    frac: f32,
392) -> bool {
393    match node {
394        DockNode::Leaf(p) => {
395            if *p == target {
396                let old = *p;
397                match drop {
398                    DockDrop::Tab => {
399                        *node = DockNode::Tabs {
400                            tabs: vec![old, panel],
401                            active: 1,
402                        };
403                    }
404                    DockDrop::Float => return false,
405                    side => {
406                        let (axis, first_new) = split_for(side);
407                        let (a, b) = if first_new {
408                            (DockNode::Leaf(panel), DockNode::Leaf(old))
409                        } else {
410                            (DockNode::Leaf(old), DockNode::Leaf(panel))
411                        };
412                        *node = DockNode::Split {
413                            axis,
414                            frac,
415                            first: Box::new(a),
416                            second: Box::new(b),
417                        };
418                    }
419                }
420                true
421            } else {
422                false
423            }
424        }
425        DockNode::Tabs { tabs, active } => {
426            if tabs.contains(&target) {
427                match drop {
428                    DockDrop::Tab => {
429                        tabs.push(panel);
430                        *active = tabs.len() - 1;
431                    }
432                    DockDrop::Float => return false,
433                    side => {
434                        let old = DockNode::Tabs {
435                            tabs: std::mem::take(tabs),
436                            active: *active,
437                        };
438                        let (axis, first_new) = split_for(side);
439                        let (a, b) = if first_new {
440                            (DockNode::Leaf(panel), old)
441                        } else {
442                            (old, DockNode::Leaf(panel))
443                        };
444                        *node = DockNode::Split {
445                            axis,
446                            frac,
447                            first: Box::new(a),
448                            second: Box::new(b),
449                        };
450                    }
451                }
452                true
453            } else {
454                false
455            }
456        }
457        DockNode::Split { first, second, .. } => {
458            insert_into_node(first, panel, target, drop, frac)
459                || insert_into_node(second, panel, target, drop, frac)
460        }
461    }
462}
463
464fn split_for(drop: DockDrop) -> (SplitAxis, bool) {
465    use sequoia_layout::{Dock, split_axis_for_dock};
466    let dock = match drop {
467        DockDrop::Left => Dock::Left,
468        DockDrop::Right => Dock::Right,
469        DockDrop::Top => Dock::Top,
470        DockDrop::Bottom => Dock::Bottom,
471        DockDrop::Tab | DockDrop::Float => return (SplitAxis::Horizontal, false),
472    };
473    match split_axis_for_dock(dock) {
474        Some((axis, first)) => (SplitAxis::from_axis(axis), first),
475        None => (SplitAxis::Horizontal, false),
476    }
477}
478
479fn remove_from_node(node: DockNode, panel: PanelId) -> (Option<DockNode>, bool) {
480    match node {
481        DockNode::Leaf(p) => {
482            if p == panel {
483                (None, true)
484            } else {
485                (Some(DockNode::Leaf(p)), false)
486            }
487        }
488        DockNode::Tabs {
489            mut tabs,
490            mut active,
491        } => {
492            if let Some(i) = tabs.iter().position(|p| *p == panel) {
493                tabs.remove(i);
494                if tabs.is_empty() {
495                    (None, true)
496                } else {
497                    if active >= tabs.len() {
498                        active = tabs.len() - 1;
499                    }
500
501                    if tabs.len() == 1 {
502                        (Some(DockNode::Leaf(tabs[0])), true)
503                    } else {
504                        (Some(DockNode::Tabs { tabs, active }), true)
505                    }
506                }
507            } else {
508                (Some(DockNode::Tabs { tabs, active }), false)
509            }
510        }
511        DockNode::Split {
512            axis,
513            frac,
514            first,
515            second,
516        } => {
517            let (a, fa) = remove_from_node(*first, panel);
518            if fa {
519                match a {
520                    None => (Some(*second), true),
521                    Some(a) => (
522                        Some(DockNode::Split {
523                            axis,
524                            frac,
525                            first: Box::new(a),
526                            second,
527                        }),
528                        true,
529                    ),
530                }
531            } else {
532                let Some(a_restored) = a else {
533                    debug_assert!(false, "remove_from_node: naruszony kontrakt (first)");
534
535                    return remove_from_node(*second, panel);
536                };
537                let (b, fb) = remove_from_node(*second, panel);
538                match (b, fb) {
539                    (None, true) => (Some(a_restored), true),
540                    (Some(b), true) => (
541                        Some(DockNode::Split {
542                            axis,
543                            frac,
544                            first: Box::new(a_restored),
545                            second: Box::new(b),
546                        }),
547                        true,
548                    ),
549                    (b_opt, false) => {
550                        let Some(b_restored) = b_opt else {
551                            debug_assert!(false, "remove_from_node: naruszony kontrakt (second)");
552                            return (Some(a_restored), false);
553                        };
554                        (
555                            Some(DockNode::Split {
556                                axis,
557                                frac,
558                                first: Box::new(a_restored),
559                                second: Box::new(b_restored),
560                            }),
561                            false,
562                        )
563                    }
564                }
565            }
566        }
567    }
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573
574    fn p(id: u64) -> PanelId {
575        PanelId::new(id)
576    }
577
578    #[test]
579    fn insert_splits_leaf() {
580        let mut m = DockModel::single(p(1));
581        assert!(m.apply(DockOp::Insert {
582            panel: p(2),
583            target: p(1),
584            drop: DockDrop::Right,
585            frac: 0.5,
586            moving: false
587        }));
588        assert!(matches!(m.root, Some(DockNode::Split { .. })));
589        assert_eq!(m.panels().len(), 2);
590    }
591
592    #[test]
593    fn insert_tab_appends() {
594        let mut m = DockModel::single(p(1));
595        assert!(m.apply(DockOp::Insert {
596            panel: p(2),
597            target: p(1),
598            drop: DockDrop::Tab,
599            frac: 0.5,
600            moving: false
601        }));
602        assert!(matches!(m.root, Some(DockNode::Tabs { .. })));
603        assert_eq!(m.active_tab_of(p(2)), Some(1));
604    }
605
606    #[test]
607    fn same_panel_is_noop() {
608        let mut m = DockModel::single(p(1));
609        assert!(!m.apply(DockOp::Insert {
610            panel: p(1),
611            target: p(1),
612            drop: DockDrop::Right,
613            frac: 0.5,
614            moving: true
615        }));
616    }
617
618    #[test]
619    fn move_panel_between_slots() {
620        let mut m = DockModel::single(p(1));
621        m.apply(DockOp::Insert {
622            panel: p(2),
623            target: p(1),
624            drop: DockDrop::Right,
625            frac: 0.5,
626            moving: false,
627        });
628        m.apply(DockOp::Insert {
629            panel: p(3),
630            target: p(2),
631            drop: DockDrop::Tab,
632            frac: 0.5,
633            moving: false,
634        });
635
636        assert!(m.apply(DockOp::Insert {
637            panel: p(1),
638            target: p(2),
639            drop: DockDrop::Tab,
640            frac: 0.5,
641            moving: true
642        }));
643        assert_eq!(m.panels().len(), 3);
644    }
645
646    #[test]
647    fn close_collapses_split() {
648        let mut m = DockModel::single(p(1));
649        m.apply(DockOp::Insert {
650            panel: p(2),
651            target: p(1),
652            drop: DockDrop::Right,
653            frac: 0.5,
654            moving: false,
655        });
656        assert!(m.apply(DockOp::Close { panel: p(2) }));
657        assert_eq!(m.root, Some(DockNode::Leaf(p(1))));
658    }
659
660    #[test]
661    fn float_and_dock_back() {
662        let mut m = DockModel::single(p(1));
663        m.apply(DockOp::Insert {
664            panel: p(2),
665            target: p(1),
666            drop: DockDrop::Right,
667            frac: 0.5,
668            moving: false,
669        });
670        assert!(m.apply(DockOp::Float {
671            panel: p(2),
672            x: 10.0,
673            y: 10.0,
674            w: 300.0,
675            h: 200.0
676        }));
677        assert!(m.is_floating(p(2)));
678        assert!(m.apply(DockOp::DockFloating {
679            panel: p(2),
680            target: p(1),
681            drop: DockDrop::Tab,
682            frac: 0.5
683        }));
684        assert!(!m.is_floating(p(2)));
685    }
686
687    #[test]
688    fn set_frac_and_activate() {
689        let mut m = DockModel::single(p(1));
690        m.apply(DockOp::Insert {
691            panel: p(2),
692            target: p(1),
693            drop: DockDrop::Right,
694            frac: 0.3,
695            moving: false,
696        });
697        assert!(m.apply(DockOp::SetFrac {
698            panel: p(1),
699            frac: 0.7
700        }));
701        assert!(!m.apply(DockOp::SetFrac {
702            panel: p(99),
703            frac: 0.5
704        }));
705        m.apply(DockOp::Insert {
706            panel: p(3),
707            target: p(2),
708            drop: DockDrop::Tab,
709            frac: 0.5,
710            moving: false,
711        });
712        assert!(m.apply(DockOp::Activate { panel: p(2) }));
713        assert_eq!(m.active_tab_of(p(2)), Some(0));
714        assert!(m.apply(DockOp::MoveTab { panel: p(3), to: 0 }));
715        assert_eq!(m.active_tab_of(p(3)), Some(0));
716    }
717
718    #[test]
719    fn close_last_leaves_empty_root() {
720        let mut m = DockModel::single(p(1));
721        assert!(m.apply(DockOp::Close { panel: p(1) }));
722        assert!(m.root.is_none());
723        assert!(!m.apply(DockOp::Close { panel: p(1) }));
724    }
725
726    #[test]
727    fn frac_at_path_roundtrip() {
728        let mut m = DockModel::single(p(1));
729        m.apply(DockOp::Insert {
730            panel: p(2),
731            target: p(1),
732            drop: DockDrop::Right,
733            frac: 0.3,
734            moving: false,
735        });
736        assert_eq!(m.frac_at(&[]), Some(0.3));
737        assert!(m.set_frac_at(&[], 0.7));
738        assert_eq!(m.frac_at(&[]), Some(0.7));
739        assert!(!m.set_frac_at(&[false], 0.5));
740        assert!(!m.set_frac_at(&[true, false], 0.5));
741    }
742}