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