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 {
128 *active = Some(pid);
129 }
130 }
131
132 pub fn set_split_ratio(&mut self, split_node_id: u64, ratio: f32) {
133 let ratio = ratio.clamp(0.05, 0.95);
134 if let Some(n) = find_node_mut(&mut self.root, split_node_id)
135 && let DockKind::Split { ratio: r, .. } = &mut n.kind
136 {
137 *r = ratio;
138 }
139 }
140
141 pub fn dock_panel(&mut self, target_node_id: u64, zone: DropZone, pid: PanelId) -> bool {
142 self.remove_panel_no_normalize(pid);
143
144 let result = match zone {
145 DropZone::Center => self.insert_as_tab(target_node_id, pid),
146 DropZone::Left | DropZone::Right | DropZone::Top | DropZone::Bottom => {
147 self.insert_as_split(target_node_id, zone, pid)
148 }
149 DropZone::Float => false,
150 };
151
152 self.normalize();
153 result
154 }
155
156 fn insert_as_tab(&mut self, target_node_id: u64, pid: PanelId) -> bool {
157 let Some(n) = find_node_mut(&mut self.root, target_node_id) else {
158 return false;
159 };
160
161 match &mut n.kind {
162 DockKind::Tabs { tabs, active } => {
163 if !tabs.contains(&pid) {
164 tabs.push(pid);
165 }
166 *active = Some(pid);
167 self.normalize();
168 true
169 }
170 DockKind::Empty => {
171 n.kind = DockKind::Tabs {
172 tabs: vec![pid],
173 active: Some(pid),
174 };
175 self.normalize();
176 true
177 }
178 DockKind::Split { .. } => false,
179 }
180 }
181
182 fn insert_as_split(&mut self, target_node_id: u64, zone: DropZone, pid: PanelId) -> bool {
183 let new_tabs_id = self.alloc_id();
185 let new_split_id = self.alloc_id();
186
187 let Some(n) = find_node_mut(&mut self.root, target_node_id) else {
188 return false;
189 };
190
191 let old_kind = std::mem::replace(&mut n.kind, DockKind::Empty);
192
193 let dir = match zone {
194 DropZone::Left | DropZone::Right => SplitDir::Horizontal,
195 DropZone::Top | DropZone::Bottom => SplitDir::Vertical,
196 _ => SplitDir::Horizontal,
197 };
198
199 let new_tabs = DockNode {
200 id: new_tabs_id,
201 kind: DockKind::Tabs {
202 tabs: vec![pid],
203 active: Some(pid),
204 },
205 };
206
207 let old_node = DockNode {
209 id: target_node_id,
210 kind: old_kind,
211 };
212
213 let (a, b) = match zone {
214 DropZone::Left | DropZone::Top => (Box::new(new_tabs), Box::new(old_node)),
215 DropZone::Right | DropZone::Bottom => (Box::new(old_node), Box::new(new_tabs)),
216 _ => (Box::new(old_node), Box::new(new_tabs)),
217 };
218
219 n.id = new_split_id;
221 n.kind = DockKind::Split {
222 dir,
223 ratio: 0.5,
224 a,
225 b,
226 };
227
228 self.normalize();
229 true
230 }
231}
232
233#[derive(Clone, Debug)]
234pub struct DockTabPayload {
235 pub panel_id: PanelId,
236}
237
238#[derive(Clone, Debug, PartialEq, Eq)]
239struct HoverHint {
240 node_id: u64,
241 zone: DropZone,
242}
243
244#[derive(Clone)]
245struct SplitDrag {
246 node_id: u64,
247 dir: SplitDir,
248}
249
250pub fn DockArea(
251 key: impl Into<String>,
252 modifier: Modifier,
253 state: Rc<RefCell<DockState>>,
254 panels: Vec<DockPanel>,
255 callbacks: DockCallbacks,
256) -> View {
257 let key = key.into();
258 let registry = Rc::new(build_registry(panels));
259
260 let hover_sig = remember_with_key(format!("dock:hover:{key}"), || signal(None::<HoverHint>));
262 let drag_active = remember_with_key(format!("dock:drag_active:{key}"), || signal(false));
263 let split_hover = remember_with_key(format!("dock:split_hover:{key}"), || signal(None::<u64>));
264 let split_drag = remember_with_key(format!("dock:split_drag:{key}"), || {
265 RefCell::new(None::<SplitDrag>)
266 });
267
268 let float_target = {
271 let state = state.clone();
272 let hover_sig = hover_sig.clone();
273 let cb_pop = callbacks.on_popout.clone();
274
275 Box(Modifier::new()
276 .fill_max_size()
277 .z_index(-1000.0)
278 .on_drop(move |ev| {
279 let Some(p) = ev.payload.as_ref().downcast_ref::<DockTabPayload>() else {
281 return false;
282 };
283 let Some(pop) = cb_pop.as_ref() else {
284 return false;
285 };
286
287 state.borrow_mut().remove_panel(p.panel_id);
289 pop(p.panel_id);
290
291 hover_sig.set(None);
292 true
293 }))
294 };
295
296 let root_view = {
298 let st = state.borrow().clone();
299 render_node(
300 &st.root,
301 ®istry,
302 &state,
303 &callbacks,
304 &hover_sig,
305 &drag_active,
306 &split_hover,
307 &split_drag,
308 key.as_str(),
309 )
310 };
311
312 Stack(modifier.fill_max_size()).child((
313 Box(Modifier::new()
314 .absolute()
315 .offset(Some(0.0), Some(0.0), Some(0.0), Some(0.0)))
316 .child(float_target),
317 Box(Modifier::new()
318 .absolute()
319 .offset(Some(0.0), Some(0.0), Some(0.0), Some(0.0)))
320 .child(root_view),
321 ))
322}
323
324fn build_registry(panels: Vec<DockPanel>) -> HashMap<PanelId, DockPanel> {
325 let mut m = HashMap::new();
326 for p in panels {
327 m.insert(p.id, p);
328 }
329 m
330}
331
332fn render_node(
333 node: &DockNode,
334 registry: &Rc<HashMap<PanelId, DockPanel>>,
335 state: &Rc<RefCell<DockState>>,
336 callbacks: &DockCallbacks,
337 hover_sig: &Signal<Option<HoverHint>>,
338 drag_active: &Signal<bool>,
339 split_hover: &Signal<Option<u64>>,
340 split_drag: &Rc<RefCell<Option<SplitDrag>>>,
341 key_prefix: &str,
342) -> View {
343 match &node.kind {
344 DockKind::Empty => Box(Modifier::new()
345 .fill_max_size()
346 .background(theme().surface)
347 .key(node.id))
348 .child(Box(Modifier::new().fill_max_size()).child(Text("Empty").color(theme().on_surface))),
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, _alpha| *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(th.shapes.small)
486 .background(if is_active {
487 th.primary.with_alpha(80)
488 } else {
489 th.surface
490 })
491 .border(1.0, th.outline, th.shapes.small),
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 Box(Modifier::new()
535 .clickable()
536 .on_pointer_down(move |_| pop(pid))
537 .padding(2.0))
538 .child(Text("↗").size(12.0))
539 } else {
540 Box(Modifier::new())
541 },
542 if let Some(close) = cb_close {
543 Box(Modifier::new()
544 .clickable()
545 .on_pointer_down(move |_| close(pid))
546 .padding(2.0))
547 .child(Text("×").size(12.0))
548 } else {
549 Box(Modifier::new())
550 },
551 )),
552 )),
553 )
554 })
555 .collect::<Vec<_>>(),
556 );
557
558 let content = if let Some(pid) = active_pid {
560 if let Some(panel) = registry.get(&pid) {
561 (panel.content)()
562 } else {
563 Text("Missing panel").color(th.error)
564 }
565 } else {
566 Text("No tabs").color(th.on_surface)
567 };
568
569 let overlay = dock_drop_overlay(node_id, state, hover_sig, drag_active, key_prefix);
571
572 let tab_h = 40.0;
573
574 Stack(Modifier::new().fill_max_size().key(node_id)).child((
575 Column(Modifier::new().fill_max_size()).child((
576 tab_bar,
577 Box(Modifier::new().fill_max_size().background(th.background))
578 .child(Box(Modifier::new().fill_max_size().padding(8.0)).child(content)),
579 )),
580 Box(Modifier::new()
581 .absolute()
582 .offset(Some(0.0), Some(tab_h), Some(0.0), Some(0.0)))
583 .child(overlay),
584 ))
585}
586
587fn dock_drop_overlay(
588 node_id: u64,
589 state: &Rc<RefCell<DockState>>,
590 hover_sig: &Signal<Option<HoverHint>>,
591 drag_active: &Signal<bool>,
592 key_prefix: &str,
593) -> View {
594 let th = theme();
595 if !drag_active.get() {
596 return Box(Modifier::new());
597 }
598
599 let zone_dp = 48.0;
600
601 let hover = hover_sig.get();
602
603 let mk_zone = |zone: DropZone, m: Modifier| -> View {
604 let state2 = state.clone();
605 let hover2 = hover_sig.clone();
606
607 let label = match zone {
608 DropZone::Center => " ",
610 DropZone::Left => " ",
611 DropZone::Right => " ",
612 DropZone::Top => " ",
613 DropZone::Bottom => " ",
614 DropZone::Float => " ",
615 };
616
617 let highlight = if hover.as_ref() == Some(&HoverHint { node_id, zone }) {
618 Stack(
619 Modifier::new()
620 .fill_max_size()
621 .background(th.primary.with_alpha(51))
622 .border(2.0, th.primary, 0.0),
623 )
624 .child(Text(label).size(12.0).color(th.on_primary))
625 } else {
626 Stack(
627 Modifier::new()
628 .fill_max_size()
629 .border(1.0, th.outline_variant, 0.0),
630 )
631 .child(Text(label).size(12.0).color(th.on_surface_variant))
632 };
633
634 Stack(
635 m.z_index(2000.0)
636 .key(hash_zone_key(node_id, zone))
637 .on_drag_enter({
638 let hover2 = hover2.clone();
639 move |_ev| {
640 hover2.set(Some(HoverHint { node_id, zone }));
641 }
642 })
643 .on_drag_over({
644 let hover2 = hover2.clone();
645 move |_ev| {
646 hover2.set(Some(HoverHint { node_id, zone }));
647 }
648 })
649 .on_drag_leave({
650 let hover2 = hover2.clone();
651 move |_ev| {
652 if hover2.get().as_ref() == Some(&HoverHint { node_id, zone }) {
654 hover2.set(None);
655 }
656 }
657 })
658 .on_drop(move |ev| {
659 let Some(p) = ev.payload.as_ref().downcast_ref::<DockTabPayload>() else {
660 return false;
661 };
662
663 let ok = state2.borrow_mut().dock_panel(node_id, zone, p.panel_id);
664 hover2.set(None);
665 request_frame();
666 ok
667 }),
668 )
669 .child(highlight)
670 };
671
672 let left = mk_zone(
675 DropZone::Left,
676 Modifier::new()
677 .absolute()
678 .offset(Some(0.0), Some(0.0), None, Some(0.0))
679 .width(zone_dp),
680 );
681
682 let right = mk_zone(
683 DropZone::Right,
684 Modifier::new()
685 .absolute()
686 .offset(None, Some(0.0), Some(0.0), Some(0.0))
687 .width(zone_dp),
688 );
689
690 let top = mk_zone(
691 DropZone::Top,
692 Modifier::new()
693 .absolute()
694 .offset(Some(zone_dp), Some(0.0), Some(zone_dp), None)
695 .height(zone_dp),
696 );
697
698 let bottom = mk_zone(
699 DropZone::Bottom,
700 Modifier::new()
701 .absolute()
702 .offset(Some(zone_dp), None, Some(zone_dp), Some(0.0))
703 .height(zone_dp),
704 );
705
706 let center = mk_zone(
707 DropZone::Center,
708 Modifier::new().absolute().offset(
709 Some(zone_dp),
710 Some(zone_dp),
711 Some(zone_dp),
712 Some(zone_dp),
713 ),
714 );
715
716 Stack(
717 Modifier::new()
718 .fill_max_size()
719 .key(hash_str_key(key_prefix, node_id)),
720 )
721 .child((left, right, top, bottom, center))
722}
723
724fn render_split(
725 node_id: u64,
726 dir: SplitDir,
727 ratio: f32,
728 a: &DockNode,
729 b: &DockNode,
730 registry: &Rc<HashMap<PanelId, DockPanel>>,
731 state: &Rc<RefCell<DockState>>,
732 callbacks: &DockCallbacks,
733 hover_sig: &Signal<Option<HoverHint>>,
734 drag_active: &Signal<bool>,
735 split_hover: &Signal<Option<u64>>,
736 split_drag: &Rc<RefCell<Option<SplitDrag>>>,
737 key_prefix: &str,
738) -> View {
739 let th = theme();
740 let ratio = ratio.clamp(0.05, 0.95);
741
742 let rect_rc = remember_with_key(format!("dock:split_rect:{}:{node_id}", key_prefix), || {
744 RefCell::new(Rect::default())
745 });
746
747 let track = {
749 let rect_rc = rect_rc.clone();
750 Modifier::new().painter(move |_scene, r, _alpha| {
751 *rect_rc.borrow_mut() = r;
752 })
753 };
754
755 let divider_thick = 8.0;
756
757 let start_drag = {
758 let split_drag = split_drag.clone();
759 move |_pe: PointerEvent| {
760 *split_drag.borrow_mut() = Some(SplitDrag { node_id, dir });
761 }
762 };
763
764 let move_drag = {
765 let split_drag = split_drag.clone();
766 let rect_rc = rect_rc.clone();
767 let state = state.clone();
768 move |pe: PointerEvent| {
769 let Some(sd) = split_drag.borrow().clone() else {
770 return;
771 };
772 if sd.node_id != node_id {
773 return;
774 }
775 let r = *rect_rc.borrow();
776 if r.w <= 1.0 || r.h <= 1.0 {
777 return;
778 }
779 let t = match dir {
780 SplitDir::Horizontal => (pe.position.x - r.x) / r.w,
781 SplitDir::Vertical => (pe.position.y - r.y) / r.h,
782 };
783 state.borrow_mut().set_split_ratio(node_id, t);
784 request_frame();
785 }
786 };
787
788 let end_drag = {
789 let split_drag = split_drag.clone();
790 move |_pe: PointerEvent| {
791 *split_drag.borrow_mut() = None;
793 }
794 };
795
796 let hovered = split_hover.get() == Some(node_id);
798 let line_color = if hovered { th.focus } else { th.outline };
799 let hit_color = if hovered {
800 line_color.with_alpha(40)
801 } else {
802 line_color.with_alpha(20)
803 };
804
805 let splitter_mod = match dir {
806 SplitDir::Horizontal => Modifier::new().width(divider_thick).fill_max_height(),
807 SplitDir::Vertical => Modifier::new().height(divider_thick).fill_max_width(),
808 };
809
810 let divider = Stack(
811 splitter_mod
812 .background(hit_color)
813 .on_pointer_enter({
814 let split_hover = split_hover.clone();
815 move |_| split_hover.set(Some(node_id))
816 })
817 .on_pointer_leave({
818 let split_hover = split_hover.clone();
819 move |_| {
820 if split_hover.get() == Some(node_id) {
821 split_hover.set(None);
822 }
823 }
824 })
825 .on_pointer_down(start_drag)
826 .on_pointer_move(move_drag)
827 .on_pointer_up(end_drag)
828 .cursor(match dir {
829 SplitDir::Horizontal => CursorIcon::EwResize,
830 SplitDir::Vertical => CursorIcon::NsResize,
831 })
832 .z_index(1500.0),
833 )
834 .child((
835 match dir {
837 SplitDir::Horizontal => Box(Modifier::new()
838 .absolute()
839 .offset(
840 Some((divider_thick - 1.0) * 0.5),
841 Some(0.0),
842 None,
843 Some(0.0),
844 )
845 .width(1.0)
846 .fill_max_height()
847 .background(line_color)),
848 SplitDir::Vertical => Box(Modifier::new()
849 .absolute()
850 .offset(
851 Some(0.0),
852 Some((divider_thick - 1.0) * 0.5),
853 Some(0.0),
854 None,
855 )
856 .height(1.0)
857 .fill_max_width()
858 .background(line_color)),
859 },
860 ));
861
862 let a_view = render_node(
863 a,
864 registry,
865 state,
866 callbacks,
867 hover_sig,
868 drag_active,
869 split_hover,
870 split_drag,
871 key_prefix,
872 );
873 let b_view = render_node(
874 b,
875 registry,
876 state,
877 callbacks,
878 hover_sig,
879 drag_active,
880 split_hover,
881 split_drag,
882 key_prefix,
883 );
884
885 match dir {
886 SplitDir::Horizontal => Row(track.fill_max_size().key(node_id)).child((
887 Box(Modifier::new().weight(ratio)).child(a_view),
888 divider,
889 Box(Modifier::new().weight(1.0 - ratio)).child(b_view),
890 )),
891 SplitDir::Vertical => Column(track.fill_max_size().key(node_id)).child((
892 Box(Modifier::new().weight(ratio)).child(a_view),
893 divider,
894 Box(Modifier::new().weight(1.0 - ratio)).child(b_view),
895 )),
896 }
897}
898
899fn find_node_mut(node: &mut DockNode, id: u64) -> Option<&mut DockNode> {
900 if node.id == id {
901 return Some(node);
902 }
903 match &mut node.kind {
904 DockKind::Split { a, b, .. } => find_node_mut(a, id).or_else(|| find_node_mut(b, id)),
905 _ => None,
906 }
907}
908
909fn remove_panel_in_node(node: &mut DockNode, pid: PanelId) -> bool {
910 match &mut node.kind {
911 DockKind::Empty => false,
912
913 DockKind::Tabs { tabs, active } => {
914 let before = tabs.len();
915 tabs.retain(|&x| x != pid);
916 if tabs.len() != before {
917 if active == &Some(pid) {
918 *active = tabs.first().copied();
919 }
920 if tabs.is_empty() {
921 node.kind = DockKind::Empty;
922 }
923 true
924 } else {
925 false
926 }
927 }
928
929 DockKind::Split { a, b, .. } => {
930 let ra = remove_panel_in_node(a, pid);
931 let rb = remove_panel_in_node(b, pid);
932 ra || rb
933 }
934 }
935}
936
937fn normalize_node(node: &mut DockNode) {
938 match &mut node.kind {
939 DockKind::Empty => {}
940 DockKind::Tabs { tabs, active } => {
941 if tabs.is_empty() {
942 node.kind = DockKind::Empty;
943 } else if active.is_none() || !tabs.contains(&active.unwrap()) {
944 *active = tabs.first().copied();
945 }
946 }
947 DockKind::Split { a, b, ratio, .. } => {
948 *ratio = ratio.clamp(0.05, 0.95);
949 normalize_node(a);
950 normalize_node(b);
951
952 let a_empty = matches!(a.kind, DockKind::Empty);
953 let b_empty = matches!(b.kind, DockKind::Empty);
954
955 if a_empty && !b_empty {
957 node.kind = std::mem::replace(&mut b.kind, DockKind::Empty);
958 } else if b_empty && !a_empty {
959 node.kind = std::mem::replace(&mut a.kind, DockKind::Empty);
960 } else if a_empty && b_empty {
961 node.kind = DockKind::Empty;
962 }
963 }
964 }
965}
966
967fn hash_zone_key(node_id: u64, zone: DropZone) -> u64 {
968 let z = match zone {
969 DropZone::Center => 1u64,
970 DropZone::Left => 2,
971 DropZone::Right => 3,
972 DropZone::Top => 4,
973 DropZone::Bottom => 5,
974 DropZone::Float => 6,
975 };
976 node_id ^ (z.wrapping_mul(0x9E3779B97F4A7C15))
977}
978
979fn hash_str_key(prefix: &str, node_id: u64) -> u64 {
980 let mut h = 1469598103934665603u64;
981 for b in prefix.as_bytes() {
982 h ^= *b as u64;
983 h = h.wrapping_mul(1099511628211u64);
984 }
985 h ^ node_id.wrapping_mul(0x9E3779B97F4A7C15)
986}
987
988#[cfg(test)]
989mod tests {
990 use super::*;
991
992 #[test]
993 fn move_tab_into_center() {
994 let mut st = DockState::new_with_tabs(vec![1, 2, 3]);
995 assert!(st.dock_panel(1, DropZone::Right, 3));
997 assert!(!st.dock_panel(st.root.id, DropZone::Center, 2));
999 }
1000
1001 #[test]
1002 fn remove_collapses_empty_split() {
1003 let mut st = DockState::new_with_tabs(vec![10]);
1004 assert!(st.dock_panel(1, DropZone::Right, 20)); assert!(st.remove_panel(10));
1006 st.normalize();
1007 fn count_tabs(n: &DockNode) -> usize {
1010 match &n.kind {
1011 DockKind::Tabs { tabs, .. } => tabs.len(),
1012 DockKind::Split { a, b, .. } => count_tabs(a) + count_tabs(b),
1013 DockKind::Empty => 0,
1014 }
1015 }
1016 assert_eq!(count_tabs(&st.root), 1);
1017 }
1018}