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}