1#![allow(non_snake_case)]
2
3use std::any::Any;
4use std::cell::RefCell;
5use std::collections::HashMap;
6use std::rc::Rc;
7
8use repose_core::*;
9use repose_ui::*;
10
11pub type PanelId = u64;
12
13#[derive(Clone)]
14pub struct DockPanel {
15 pub id: PanelId,
16 pub title: String,
17 pub content: Rc<dyn Fn() -> View>,
18}
19
20#[derive(Clone, Default)]
21pub struct DockCallbacks {
22 pub on_popout: Option<Rc<dyn Fn(PanelId)>>,
25
26 pub on_close: Option<Rc<dyn Fn(PanelId)>>,
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum SplitDir {
32 Horizontal, Vertical, }
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum DropZone {
38 Center,
39 Left,
40 Right,
41 Top,
42 Bottom,
43 Float,
44}
45
46#[derive(Clone)]
49pub struct DockState {
50 pub root: DockNode,
51 next_id: u64,
52}
53
54#[derive(Clone)]
55pub struct DockNode {
56 pub id: u64,
57 pub kind: DockKind,
58}
59
60#[derive(Clone)]
61pub enum DockKind {
62 Empty,
63 Tabs {
64 tabs: Vec<PanelId>,
65 active: Option<PanelId>,
66 },
67 Split {
68 dir: SplitDir,
69 ratio: f32, a: Box<DockNode>,
71 b: Box<DockNode>,
72 },
73}
74
75impl DockState {
76 pub fn new_with_tabs(tabs: Vec<PanelId>) -> Self {
77 let mut st = Self {
78 root: DockNode {
79 id: 1,
80 kind: DockKind::Empty,
81 },
82 next_id: 2,
83 };
84 st.root.kind = DockKind::Tabs { tabs, active: None };
85 st.normalize();
86 st
87 }
88
89 pub fn from_root(root: DockNode, max_node_id: u64) -> Self {
92 let mut st = Self {
93 root,
94 next_id: max_node_id + 1,
95 };
96 st.normalize();
97 st
98 }
99
100 fn alloc_id(&mut self) -> u64 {
101 let id = self.next_id;
102 self.next_id += 1;
103 id
104 }
105
106 pub fn normalize(&mut self) {
107 normalize_node(&mut self.root);
108 }
109
110 pub fn remove_panel_no_normalize(&mut self, pid: PanelId) -> bool {
112 remove_panel_in_node(&mut self.root, pid)
113 }
114
115 pub fn remove_panel(&mut self, pid: PanelId) -> bool {
116 let removed = remove_panel_in_node(&mut self.root, pid);
117 if removed {
118 normalize_node(&mut self.root);
119 }
120 removed
121 }
122
123 pub fn set_active(&mut self, tabs_node_id: u64, pid: PanelId) {
124 if let Some(n) = find_node_mut(&mut self.root, tabs_node_id)
125 && let DockKind::Tabs { tabs, active } = &mut n.kind
126 && tabs.contains(&pid) {
127 *active = Some(pid);
128 }
129 }
130
131 pub fn set_split_ratio(&mut self, split_node_id: u64, ratio: f32) {
132 let ratio = ratio.clamp(0.05, 0.95);
133 if let Some(n) = find_node_mut(&mut self.root, split_node_id)
134 && let DockKind::Split { ratio: r, .. } = &mut n.kind {
135 *r = ratio;
136 }
137 }
138
139 pub fn dock_panel(&mut self, target_node_id: u64, zone: DropZone, pid: PanelId) -> bool {
140 self.remove_panel_no_normalize(pid);
141
142 let result = match zone {
143 DropZone::Center => self.insert_as_tab(target_node_id, pid),
144 DropZone::Left | DropZone::Right | DropZone::Top | DropZone::Bottom => {
145 self.insert_as_split(target_node_id, zone, pid)
146 }
147 DropZone::Float => false,
148 };
149
150 self.normalize();
151 result
152 }
153
154 fn insert_as_tab(&mut self, target_node_id: u64, pid: PanelId) -> bool {
155 let Some(n) = find_node_mut(&mut self.root, target_node_id) else {
156 return false;
157 };
158
159 match &mut n.kind {
160 DockKind::Tabs { tabs, active } => {
161 if !tabs.contains(&pid) {
162 tabs.push(pid);
163 }
164 *active = Some(pid);
165 self.normalize();
166 true
167 }
168 DockKind::Empty => {
169 n.kind = DockKind::Tabs {
170 tabs: vec![pid],
171 active: Some(pid),
172 };
173 self.normalize();
174 true
175 }
176 DockKind::Split { .. } => false,
177 }
178 }
179
180 fn insert_as_split(&mut self, target_node_id: u64, zone: DropZone, pid: PanelId) -> bool {
181 let new_tabs_id = self.alloc_id();
183 let new_split_id = self.alloc_id();
184
185 let Some(n) = find_node_mut(&mut self.root, target_node_id) else {
186 return false;
187 };
188
189 let old_kind = std::mem::replace(&mut n.kind, DockKind::Empty);
190
191 let dir = match zone {
192 DropZone::Left | DropZone::Right => SplitDir::Horizontal,
193 DropZone::Top | DropZone::Bottom => SplitDir::Vertical,
194 _ => SplitDir::Horizontal,
195 };
196
197 let new_tabs = DockNode {
198 id: new_tabs_id,
199 kind: DockKind::Tabs {
200 tabs: vec![pid],
201 active: Some(pid),
202 },
203 };
204
205 let old_node = DockNode {
207 id: target_node_id,
208 kind: old_kind,
209 };
210
211 let (a, b) = match zone {
212 DropZone::Left | DropZone::Top => (Box::new(new_tabs), Box::new(old_node)),
213 DropZone::Right | DropZone::Bottom => (Box::new(old_node), Box::new(new_tabs)),
214 _ => (Box::new(old_node), Box::new(new_tabs)),
215 };
216
217 n.id = new_split_id;
219 n.kind = DockKind::Split {
220 dir,
221 ratio: 0.5,
222 a,
223 b,
224 };
225
226 self.normalize();
227 true
228 }
229}
230
231#[derive(Clone, Debug)]
232pub struct DockTabPayload {
233 pub panel_id: PanelId,
234}
235
236#[derive(Clone, Debug, PartialEq, Eq)]
237struct HoverHint {
238 node_id: u64,
239 zone: DropZone,
240}
241
242#[derive(Clone)]
243struct SplitDrag {
244 node_id: u64,
245 dir: SplitDir,
246}
247
248pub fn DockArea(
249 key: impl Into<String>,
250 modifier: Modifier,
251 state: Rc<RefCell<DockState>>,
252 panels: Vec<DockPanel>,
253 callbacks: DockCallbacks,
254) -> View {
255 let key = key.into();
256 let registry = Rc::new(build_registry(panels));
257
258 let hover_sig = remember_with_key(format!("dock:hover:{key}"), || signal(None::<HoverHint>));
260 let drag_active = remember_with_key(format!("dock:drag_active:{key}"), || signal(false));
261 let split_hover = remember_with_key(format!("dock:split_hover:{key}"), || signal(None::<u64>));
262 let split_drag = remember_with_key(format!("dock:split_drag:{key}"), || {
263 RefCell::new(None::<SplitDrag>)
264 });
265
266 let float_target = {
269 let state = state.clone();
270 let hover_sig = hover_sig.clone();
271 let cb_pop = callbacks.on_popout.clone();
272
273 Box(Modifier::new()
274 .fill_max_size()
275 .z_index(-1000.0)
276 .on_drop(move |ev| {
277 let Some(p) = ev.payload.as_ref().downcast_ref::<DockTabPayload>() else {
279 return false;
280 };
281 let Some(pop) = cb_pop.as_ref() else {
282 return false;
283 };
284
285 state.borrow_mut().remove_panel(p.panel_id);
287 pop(p.panel_id);
288
289 hover_sig.set(None);
290 true
291 }))
292 };
293
294 let root_view = {
296 let st = state.borrow().clone();
297 render_node(
298 &st.root,
299 ®istry,
300 &state,
301 &callbacks,
302 &hover_sig,
303 &drag_active,
304 &split_hover,
305 &split_drag,
306 key.as_str(),
307 )
308 };
309
310 Stack(modifier.fill_max_size()).child((
311 Box(Modifier::new()
312 .absolute()
313 .offset(Some(0.0), Some(0.0), Some(0.0), Some(0.0)))
314 .child(float_target),
315 Box(Modifier::new()
316 .absolute()
317 .offset(Some(0.0), Some(0.0), Some(0.0), Some(0.0)))
318 .child(root_view),
319 ))
320}
321
322fn build_registry(panels: Vec<DockPanel>) -> HashMap<PanelId, DockPanel> {
323 let mut m = HashMap::new();
324 for p in panels {
325 m.insert(p.id, p);
326 }
327 m
328}
329
330fn render_node(
331 node: &DockNode,
332 registry: &Rc<HashMap<PanelId, DockPanel>>,
333 state: &Rc<RefCell<DockState>>,
334 callbacks: &DockCallbacks,
335 hover_sig: &Signal<Option<HoverHint>>,
336 drag_active: &Signal<bool>,
337 split_hover: &Signal<Option<u64>>,
338 split_drag: &Rc<RefCell<Option<SplitDrag>>>,
339 key_prefix: &str,
340) -> View {
341 match &node.kind {
342 DockKind::Empty => Surface(
343 Modifier::new()
344 .fill_max_size()
345 .background(theme().surface)
346 .key(node.id),
347 Box(Modifier::new().fill_max_size()).child(Text("Empty").color(theme().on_surface)),
348 ),
349
350 DockKind::Tabs { tabs, active } => render_tabs(
351 node.id,
352 tabs,
353 *active,
354 registry,
355 state,
356 callbacks,
357 hover_sig,
358 drag_active,
359 split_hover,
360 key_prefix,
361 ),
362
363 DockKind::Split { dir, ratio, a, b } => render_split(
364 node.id,
365 *dir,
366 *ratio,
367 a,
368 b,
369 registry,
370 state,
371 callbacks,
372 hover_sig,
373 drag_active,
374 split_hover,
375 split_drag,
376 key_prefix,
377 ),
378 }
379}
380
381fn render_tabs(
382 node_id: u64,
383 tabs: &Vec<PanelId>,
384 active: Option<PanelId>,
385 registry: &Rc<HashMap<PanelId, DockPanel>>,
386 state: &Rc<RefCell<DockState>>,
387 callbacks: &DockCallbacks,
388 hover_sig: &Signal<Option<HoverHint>>,
389 drag_active: &Signal<bool>,
390 _split_hover: &Signal<Option<u64>>,
391 key_prefix: &str,
392) -> View {
393 let th = theme();
394
395 let active_pid = active.or_else(|| tabs.first().copied());
397
398 let tabbar_rect = remember_with_key(format!("dock:tabbar_rect:{key_prefix}:{node_id}"), || {
399 RefCell::new(Rect::default())
400 });
401
402 let mut bar_mod = Modifier::new()
403 .fill_max_width()
404 .height(40.0)
405 .background(th.surface)
406 .border(1.0, th.outline, 0.0)
407 .padding(6.0)
408 .painter({
409 let tabbar_rect = tabbar_rect.clone();
410 move |_scene, r| *tabbar_rect.borrow_mut() = r
411 });
412
413 if drag_active.get() {
414 bar_mod = bar_mod.on_drop({
415 let state = state.clone();
416 let tabbar_rect = tabbar_rect.clone();
417 let hover_sig = hover_sig.clone();
418 let drag_active = drag_active.clone();
419
420 move |ev| {
421 let Some(p) = ev.payload.as_ref().downcast_ref::<DockTabPayload>() else {
422 return false;
423 };
424
425 let mut st = state.borrow_mut();
426
427 st.remove_panel_no_normalize(p.panel_id);
429
430 let r = *tabbar_rect.borrow();
431 let t = if r.w > 1.0 {
432 ((ev.position.x - r.x) / r.w).clamp(0.0, 1.0)
433 } else {
434 1.0
435 };
436
437 if let Some(n) = find_node_mut(&mut st.root, node_id) {
438 if matches!(n.kind, DockKind::Empty) {
439 n.kind = DockKind::Tabs {
440 tabs: Vec::new(),
441 active: None,
442 };
443 }
444
445 if let DockKind::Tabs { tabs, active } = &mut n.kind {
446 tabs.retain(|&x| x != p.panel_id);
447 let idx =
448 ((t * (tabs.len() as f32 + 1.0)).floor() as usize).min(tabs.len());
449 tabs.insert(idx, p.panel_id);
450 *active = Some(p.panel_id);
451 }
452 }
453
454 st.normalize();
455
456 hover_sig.set(None);
457 drag_active.set(false);
458 request_frame();
459 true
460 }
461 });
462 }
463
464 let tab_bar = Row(bar_mod).with_children(
465 tabs.iter()
466 .copied()
467 .filter_map(|pid| {
468 let panel = registry.get(&pid)?;
469 let is_active = Some(pid) == active_pid;
470
471 let state_set = state.clone();
472 let title = panel.title.clone();
473
474 let drag_pid = pid;
475
476 let cb_close = callbacks.on_close.clone();
477 let cb_pop = callbacks.on_popout.clone();
478
479 Some(
480 Stack(
481 Modifier::new()
482 .key(pid)
483 .height(32.0)
484 .padding(4.0)
485 .clip_rounded(8.0)
486 .background(if is_active {
487 th.primary.with_alpha(80)
488 } else {
489 th.surface
490 })
491 .border(1.0, th.outline, 8.0),
492 )
493 .child((
494 Box(Modifier::new()
495 .height(24.0)
496 .offset(None, Some(4.0), None, None)
497 .clickable()
498 .cursor(CursorIcon::Grab)
499 .on_pointer_down({
500 let state_set = state_set.clone();
501 move |_| {
502 state_set.borrow_mut().set_active(node_id, pid);
503 request_frame();
504 }
505 })
506 .on_drag_start({
507 let drag_active = drag_active.clone();
508 move |_start| {
509 drag_active.set(true);
510 Some(Rc::new(DockTabPayload { panel_id: drag_pid })
511 as Rc<dyn Any>)
512 }
513 })
514 .on_drag_end({
515 let hover_sig = hover_sig.clone();
516 let drag_active = drag_active.clone();
517 move |_end| {
518 drag_active.set(false);
519 hover_sig.set(None);
520 }
521 }))
522 .child(
523 Row(Modifier::new().height(24.0))
524 .child((Text(title).color(th.on_surface),)),
525 ),
526 Row(Modifier::new().absolute().height(24.0).offset(
527 None,
528 Some(4.0),
529 Some(2.0),
530 None,
531 ))
532 .child((
533 if let Some(pop) = cb_pop {
534 Button(Text("↗").size(12.0), move || pop(pid))
535 .modifier(Modifier::new().padding(2.0))
536 } else {
537 Box(Modifier::new())
538 },
539 if let Some(close) = cb_close {
540 Button(Text("×").size(12.0), move || close(pid))
541 .modifier(Modifier::new().padding(2.0))
542 } else {
543 Box(Modifier::new())
544 },
545 )),
546 )),
547 )
548 })
549 .collect::<Vec<_>>(),
550 );
551
552 let content = if let Some(pid) = active_pid {
554 if let Some(panel) = registry.get(&pid) {
555 (panel.content)()
556 } else {
557 Text("Missing panel").color(th.error)
558 }
559 } else {
560 Text("No tabs").color(th.on_surface)
561 };
562
563 let overlay = dock_drop_overlay(node_id, state, hover_sig, drag_active, key_prefix);
565
566 let tab_h = 40.0;
567
568 Stack(Modifier::new().fill_max_size().key(node_id)).child((
569 Column(Modifier::new().fill_max_size()).child((
570 tab_bar,
571 Surface(
572 Modifier::new().fill_max_size().background(th.background),
573 Box(Modifier::new().fill_max_size().padding(8.0)).child(content),
574 ),
575 )),
576 Box(Modifier::new()
577 .absolute()
578 .offset(Some(0.0), Some(tab_h), Some(0.0), Some(0.0)))
579 .child(overlay),
580 ))
581}
582
583fn dock_drop_overlay(
584 node_id: u64,
585 state: &Rc<RefCell<DockState>>,
586 hover_sig: &Signal<Option<HoverHint>>,
587 drag_active: &Signal<bool>,
588 key_prefix: &str,
589) -> View {
590 let th = theme();
591 if !drag_active.get() {
592 return Box(Modifier::new());
593 }
594
595 let zone_dp = 48.0;
596
597 let hover = hover_sig.get();
598
599 let mk_zone = |zone: DropZone, m: Modifier| -> View {
600 let state2 = state.clone();
601 let hover2 = hover_sig.clone();
602
603 let label = match zone {
604 DropZone::Center => " ",
606 DropZone::Left => " ",
607 DropZone::Right => " ",
608 DropZone::Top => " ",
609 DropZone::Bottom => " ",
610 DropZone::Float => " ",
611 };
612
613 let highlight = if hover.as_ref() == Some(&HoverHint { node_id, zone }) {
614 Stack(
615 Modifier::new()
616 .fill_max_size()
617 .background(th.primary.with_alpha(51))
618 .border(2.0, th.primary, 0.0),
619 )
620 .child(Text(label).size(12.0).color(th.on_primary))
621 } else {
622 Stack(
623 Modifier::new()
624 .fill_max_size()
625 .border(1.0, th.outline_variant, 0.0),
626 )
627 .child(Text(label).size(12.0).color(th.on_surface_variant))
628 };
629
630 Stack(
631 m.z_index(2000.0)
632 .key(hash_zone_key(node_id, zone))
633 .on_drag_enter({
634 let hover2 = hover2.clone();
635 move |_ev| {
636 hover2.set(Some(HoverHint { node_id, zone }));
637 }
638 })
639 .on_drag_over({
640 let hover2 = hover2.clone();
641 move |_ev| {
642 hover2.set(Some(HoverHint { node_id, zone }));
643 }
644 })
645 .on_drag_leave({
646 let hover2 = hover2.clone();
647 move |_ev| {
648 if hover2.get().as_ref() == Some(&HoverHint { node_id, zone }) {
650 hover2.set(None);
651 }
652 }
653 })
654 .on_drop(move |ev| {
655 let Some(p) = ev.payload.as_ref().downcast_ref::<DockTabPayload>() else {
656 return false;
657 };
658
659 let ok = state2.borrow_mut().dock_panel(node_id, zone, p.panel_id);
660 hover2.set(None);
661 request_frame();
662 ok
663 }),
664 )
665 .child(highlight)
666 };
667
668 let left = mk_zone(
671 DropZone::Left,
672 Modifier::new()
673 .absolute()
674 .offset(Some(0.0), Some(0.0), None, Some(0.0))
675 .width(zone_dp),
676 );
677
678 let right = mk_zone(
679 DropZone::Right,
680 Modifier::new()
681 .absolute()
682 .offset(None, Some(0.0), Some(0.0), Some(0.0))
683 .width(zone_dp),
684 );
685
686 let top = mk_zone(
687 DropZone::Top,
688 Modifier::new()
689 .absolute()
690 .offset(Some(zone_dp), Some(0.0), Some(zone_dp), None)
691 .height(zone_dp),
692 );
693
694 let bottom = mk_zone(
695 DropZone::Bottom,
696 Modifier::new()
697 .absolute()
698 .offset(Some(zone_dp), None, Some(zone_dp), Some(0.0))
699 .height(zone_dp),
700 );
701
702 let center = mk_zone(
703 DropZone::Center,
704 Modifier::new().absolute().offset(
705 Some(zone_dp),
706 Some(zone_dp),
707 Some(zone_dp),
708 Some(zone_dp),
709 ),
710 );
711
712 Stack(
713 Modifier::new()
714 .fill_max_size()
715 .key(hash_str_key(key_prefix, node_id)),
716 )
717 .child((left, right, top, bottom, center))
718}
719
720fn render_split(
721 node_id: u64,
722 dir: SplitDir,
723 ratio: f32,
724 a: &DockNode,
725 b: &DockNode,
726 registry: &Rc<HashMap<PanelId, DockPanel>>,
727 state: &Rc<RefCell<DockState>>,
728 callbacks: &DockCallbacks,
729 hover_sig: &Signal<Option<HoverHint>>,
730 drag_active: &Signal<bool>,
731 split_hover: &Signal<Option<u64>>,
732 split_drag: &Rc<RefCell<Option<SplitDrag>>>,
733 key_prefix: &str,
734) -> View {
735 let th = theme();
736 let ratio = ratio.clamp(0.05, 0.95);
737
738 let rect_rc = remember_with_key(format!("dock:split_rect:{}:{node_id}", key_prefix), || {
740 RefCell::new(Rect::default())
741 });
742
743 let track = {
745 let rect_rc = rect_rc.clone();
746 Modifier::new().painter(move |_scene, r| {
747 *rect_rc.borrow_mut() = r;
748 })
749 };
750
751 let divider_thick = 8.0;
752
753 let start_drag = {
754 let split_drag = split_drag.clone();
755 move |_pe: PointerEvent| {
756 *split_drag.borrow_mut() = Some(SplitDrag { node_id, dir });
757 }
758 };
759
760 let move_drag = {
761 let split_drag = split_drag.clone();
762 let rect_rc = rect_rc.clone();
763 let state = state.clone();
764 move |pe: PointerEvent| {
765 let Some(sd) = split_drag.borrow().clone() else {
766 return;
767 };
768 if sd.node_id != node_id {
769 return;
770 }
771 let r = *rect_rc.borrow();
772 if r.w <= 1.0 || r.h <= 1.0 {
773 return;
774 }
775 let t = match dir {
776 SplitDir::Horizontal => (pe.position.x - r.x) / r.w,
777 SplitDir::Vertical => (pe.position.y - r.y) / r.h,
778 };
779 state.borrow_mut().set_split_ratio(node_id, t);
780 request_frame();
781 }
782 };
783
784 let end_drag = {
785 let split_drag = split_drag.clone();
786 move |_pe: PointerEvent| {
787 *split_drag.borrow_mut() = None;
789 }
790 };
791
792 let hovered = split_hover.get() == Some(node_id);
794 let line_color = if hovered { th.focus } else { th.outline };
795 let hit_color = if hovered {
796 line_color.with_alpha(40)
797 } else {
798 line_color.with_alpha(20)
799 };
800
801 let splitter_mod = match dir {
802 SplitDir::Horizontal => Modifier::new().width(divider_thick).fill_max_height(),
803 SplitDir::Vertical => Modifier::new().height(divider_thick).fill_max_width(),
804 };
805
806 let divider = Stack(
807 splitter_mod
808 .background(hit_color)
809 .on_pointer_enter({
810 let split_hover = split_hover.clone();
811 move |_| split_hover.set(Some(node_id))
812 })
813 .on_pointer_leave({
814 let split_hover = split_hover.clone();
815 move |_| {
816 if split_hover.get() == Some(node_id) {
817 split_hover.set(None);
818 }
819 }
820 })
821 .on_pointer_down(start_drag)
822 .on_pointer_move(move_drag)
823 .on_pointer_up(end_drag)
824 .cursor(match dir {
825 SplitDir::Horizontal => CursorIcon::EwResize,
826 SplitDir::Vertical => CursorIcon::NsResize,
827 })
828 .z_index(1500.0),
829 )
830 .child((
831 match dir {
833 SplitDir::Horizontal => Box(Modifier::new()
834 .absolute()
835 .offset(
836 Some((divider_thick - 1.0) * 0.5),
837 Some(0.0),
838 None,
839 Some(0.0),
840 )
841 .width(1.0)
842 .fill_max_height()
843 .background(line_color)),
844 SplitDir::Vertical => Box(Modifier::new()
845 .absolute()
846 .offset(
847 Some(0.0),
848 Some((divider_thick - 1.0) * 0.5),
849 Some(0.0),
850 None,
851 )
852 .height(1.0)
853 .fill_max_width()
854 .background(line_color)),
855 },
856 ));
857
858 let a_view = render_node(
859 a,
860 registry,
861 state,
862 callbacks,
863 hover_sig,
864 drag_active,
865 split_hover,
866 split_drag,
867 key_prefix,
868 );
869 let b_view = render_node(
870 b,
871 registry,
872 state,
873 callbacks,
874 hover_sig,
875 drag_active,
876 split_hover,
877 split_drag,
878 key_prefix,
879 );
880
881 match dir {
882 SplitDir::Horizontal => Row(track.fill_max_size().key(node_id)).child((
883 Box(Modifier::new().weight(ratio)).child(a_view),
884 divider,
885 Box(Modifier::new().weight(1.0 - ratio)).child(b_view),
886 )),
887 SplitDir::Vertical => Column(track.fill_max_size().key(node_id)).child((
888 Box(Modifier::new().weight(ratio)).child(a_view),
889 divider,
890 Box(Modifier::new().weight(1.0 - ratio)).child(b_view),
891 )),
892 }
893}
894
895fn find_node_mut(node: &mut DockNode, id: u64) -> Option<&mut DockNode> {
896 if node.id == id {
897 return Some(node);
898 }
899 match &mut node.kind {
900 DockKind::Split { a, b, .. } => find_node_mut(a, id).or_else(|| find_node_mut(b, id)),
901 _ => None,
902 }
903}
904
905fn remove_panel_in_node(node: &mut DockNode, pid: PanelId) -> bool {
906 match &mut node.kind {
907 DockKind::Empty => false,
908
909 DockKind::Tabs { tabs, active } => {
910 let before = tabs.len();
911 tabs.retain(|&x| x != pid);
912 if tabs.len() != before {
913 if active == &Some(pid) {
914 *active = tabs.first().copied();
915 }
916 if tabs.is_empty() {
917 node.kind = DockKind::Empty;
918 }
919 true
920 } else {
921 false
922 }
923 }
924
925 DockKind::Split { a, b, .. } => {
926 let ra = remove_panel_in_node(a, pid);
927 let rb = remove_panel_in_node(b, pid);
928 ra || rb
929 }
930 }
931}
932
933fn normalize_node(node: &mut DockNode) {
934 match &mut node.kind {
935 DockKind::Empty => {}
936 DockKind::Tabs { tabs, active } => {
937 if tabs.is_empty() {
938 node.kind = DockKind::Empty;
939 } else if active.is_none() || !tabs.contains(&active.unwrap()) {
940 *active = tabs.first().copied();
941 }
942 }
943 DockKind::Split { a, b, ratio, .. } => {
944 *ratio = ratio.clamp(0.05, 0.95);
945 normalize_node(a);
946 normalize_node(b);
947
948 let a_empty = matches!(a.kind, DockKind::Empty);
949 let b_empty = matches!(b.kind, DockKind::Empty);
950
951 if a_empty && !b_empty {
953 node.kind = std::mem::replace(&mut b.kind, DockKind::Empty);
954 } else if b_empty && !a_empty {
955 node.kind = std::mem::replace(&mut a.kind, DockKind::Empty);
956 } else if a_empty && b_empty {
957 node.kind = DockKind::Empty;
958 }
959 }
960 }
961}
962
963fn hash_zone_key(node_id: u64, zone: DropZone) -> u64 {
964 let z = match zone {
965 DropZone::Center => 1u64,
966 DropZone::Left => 2,
967 DropZone::Right => 3,
968 DropZone::Top => 4,
969 DropZone::Bottom => 5,
970 DropZone::Float => 6,
971 };
972 node_id ^ (z.wrapping_mul(0x9E3779B97F4A7C15))
973}
974
975fn hash_str_key(prefix: &str, node_id: u64) -> u64 {
976 let mut h = 1469598103934665603u64;
977 for b in prefix.as_bytes() {
978 h ^= *b as u64;
979 h = h.wrapping_mul(1099511628211u64);
980 }
981 h ^ node_id.wrapping_mul(0x9E3779B97F4A7C15)
982}
983
984#[cfg(test)]
985mod tests {
986 use super::*;
987
988 #[test]
989 fn move_tab_into_center() {
990 let mut st = DockState::new_with_tabs(vec![1, 2, 3]);
991 assert!(st.dock_panel(1, DropZone::Right, 3));
993 assert!(!st.dock_panel(st.root.id, DropZone::Center, 2));
995 }
996
997 #[test]
998 fn remove_collapses_empty_split() {
999 let mut st = DockState::new_with_tabs(vec![10]);
1000 assert!(st.dock_panel(1, DropZone::Right, 20)); assert!(st.remove_panel(10));
1002 st.normalize();
1003 fn count_tabs(n: &DockNode) -> usize {
1006 match &n.kind {
1007 DockKind::Tabs { tabs, .. } => tabs.len(),
1008 DockKind::Split { a, b, .. } => count_tabs(a) + count_tabs(b),
1009 DockKind::Empty => 0,
1010 }
1011 }
1012 assert_eq!(count_tabs(&st.root), 1);
1013 }
1014}