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