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}