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::TextStyle;
10use repose_ui::*;
11
12pub type PanelId = u64;
13
14#[derive(Clone)]
15pub struct DockPanel {
16 pub id: PanelId,
17 pub title: String,
18 pub content: Rc<dyn Fn() -> View>,
19}
20
21#[derive(Clone, Default)]
22pub struct DockCallbacks {
23 pub on_popout: Option<Rc<dyn Fn(PanelId)>>,
26
27 pub on_close: Option<Rc<dyn Fn(PanelId)>>,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum SplitDir {
33 Horizontal, Vertical, }
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum DropZone {
39 Center,
40 Left,
41 Right,
42 Top,
43 Bottom,
44 Float,
45}
46
47#[derive(Clone)]
50pub struct DockState {
51 pub root: DockNode,
52 next_id: u64,
53}
54
55#[derive(Clone)]
56pub struct DockNode {
57 pub id: u64,
58 pub kind: DockKind,
59}
60
61#[derive(Clone)]
62pub enum DockKind {
63 Empty,
64 Tabs {
65 tabs: Vec<PanelId>,
66 active: Option<PanelId>,
67 },
68 Split {
69 dir: SplitDir,
70 ratio: f32, a: Box<DockNode>,
72 b: Box<DockNode>,
73 },
74}
75
76impl DockState {
77 pub fn new_with_tabs(tabs: Vec<PanelId>) -> Self {
78 let mut st = Self {
79 root: DockNode {
80 id: 1,
81 kind: DockKind::Empty,
82 },
83 next_id: 2,
84 };
85 st.root.kind = DockKind::Tabs { tabs, active: None };
86 st.normalize();
87 st
88 }
89
90 pub fn from_root(root: DockNode, max_node_id: u64) -> Self {
93 let mut st = Self {
94 root,
95 next_id: max_node_id + 1,
96 };
97 st.normalize();
98 st
99 }
100
101 fn alloc_id(&mut self) -> u64 {
102 let id = self.next_id;
103 self.next_id += 1;
104 id
105 }
106
107 pub fn normalize(&mut self) {
108 normalize_node(&mut self.root);
109 }
110
111 pub fn remove_panel_no_normalize(&mut self, pid: PanelId) -> bool {
113 remove_panel_in_node(&mut self.root, pid)
114 }
115
116 pub fn remove_panel(&mut self, pid: PanelId) -> bool {
117 let removed = remove_panel_in_node(&mut self.root, pid);
118 if removed {
119 normalize_node(&mut self.root);
120 }
121 removed
122 }
123
124 pub fn set_active(&mut self, tabs_node_id: u64, pid: PanelId) {
125 if let Some(n) = find_node_mut(&mut self.root, tabs_node_id)
126 && let DockKind::Tabs { tabs, active } = &mut n.kind
127 && tabs.contains(&pid)
128 {
129 *active = Some(pid);
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 && let DockKind::Split { ratio: r, .. } = &mut n.kind
137 {
138 *r = ratio;
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 => Box(Modifier::new()
346 .fill_max_size()
347 .background(theme().surface)
348 .key(node.id))
349 .child(Box(Modifier::new().fill_max_size()).child(Text("Empty").color(theme().on_surface))),
350
351 DockKind::Tabs { tabs, active } => render_tabs(
352 node.id,
353 tabs,
354 *active,
355 registry,
356 state,
357 callbacks,
358 hover_sig,
359 drag_active,
360 split_hover,
361 key_prefix,
362 ),
363
364 DockKind::Split { dir, ratio, a, b } => render_split(
365 node.id,
366 *dir,
367 *ratio,
368 a,
369 b,
370 registry,
371 state,
372 callbacks,
373 hover_sig,
374 drag_active,
375 split_hover,
376 split_drag,
377 key_prefix,
378 ),
379 }
380}
381
382fn render_tabs(
383 node_id: u64,
384 tabs: &Vec<PanelId>,
385 active: Option<PanelId>,
386 registry: &Rc<HashMap<PanelId, DockPanel>>,
387 state: &Rc<RefCell<DockState>>,
388 callbacks: &DockCallbacks,
389 hover_sig: &Signal<Option<HoverHint>>,
390 drag_active: &Signal<bool>,
391 _split_hover: &Signal<Option<u64>>,
392 key_prefix: &str,
393) -> View {
394 let th = theme();
395
396 let active_pid = active.or_else(|| tabs.first().copied());
398
399 let tabbar_rect = remember_with_key(format!("dock:tabbar_rect:{key_prefix}:{node_id}"), || {
400 RefCell::new(Rect::default())
401 });
402
403 let mut bar_mod = Modifier::new()
404 .fill_max_width()
405 .height(40.0)
406 .background(th.surface)
407 .border(1.0, th.outline, 0.0)
408 .padding(6.0)
409 .painter({
410 let tabbar_rect = tabbar_rect.clone();
411 move |_scene, r, _alpha| *tabbar_rect.borrow_mut() = r
412 });
413
414 if drag_active.get() {
415 bar_mod = bar_mod.on_drop({
416 let state = state.clone();
417 let tabbar_rect = tabbar_rect.clone();
418 let hover_sig = hover_sig.clone();
419 let drag_active = drag_active.clone();
420
421 move |ev| {
422 let Some(p) = ev.payload.as_ref().downcast_ref::<DockTabPayload>() else {
423 return false;
424 };
425
426 let mut st = state.borrow_mut();
427
428 st.remove_panel_no_normalize(p.panel_id);
430
431 let r = *tabbar_rect.borrow();
432 let t = if r.w > 1.0 {
433 ((ev.position.x - r.x) / r.w).clamp(0.0, 1.0)
434 } else {
435 1.0
436 };
437
438 if let Some(n) = find_node_mut(&mut st.root, node_id) {
439 if matches!(n.kind, DockKind::Empty) {
440 n.kind = DockKind::Tabs {
441 tabs: Vec::new(),
442 active: None,
443 };
444 }
445
446 if let DockKind::Tabs { tabs, active } = &mut n.kind {
447 tabs.retain(|&x| x != p.panel_id);
448 let idx =
449 ((t * (tabs.len() as f32 + 1.0)).floor() as usize).min(tabs.len());
450 tabs.insert(idx, p.panel_id);
451 *active = Some(p.panel_id);
452 }
453 }
454
455 st.normalize();
456
457 hover_sig.set(None);
458 drag_active.set(false);
459 request_frame();
460 true
461 }
462 });
463 }
464
465 let tab_bar = Row(bar_mod).with_children(
466 tabs.iter()
467 .copied()
468 .filter_map(|pid| {
469 let panel = registry.get(&pid)?;
470 let is_active = Some(pid) == active_pid;
471
472 let state_set = state.clone();
473 let title = panel.title.clone();
474
475 let drag_pid = pid;
476
477 let cb_close = callbacks.on_close.clone();
478 let cb_pop = callbacks.on_popout.clone();
479
480 Some(
481 Stack(
482 Modifier::new()
483 .key(pid)
484 .height(32.0)
485 .padding(4.0)
486 .clip_rounded(th.shapes.small)
487 .background(if is_active {
488 th.primary.with_alpha(80)
489 } else {
490 th.surface
491 })
492 .border(1.0, th.outline, th.shapes.small),
493 )
494 .child((
495 Box(Modifier::new()
496 .height(24.0)
497 .offset(None, Some(4.0), None, None)
498 .clickable()
499 .cursor(CursorIcon::Grab)
500 .on_pointer_down({
501 let state_set = state_set.clone();
502 move |_| {
503 state_set.borrow_mut().set_active(node_id, pid);
504 request_frame();
505 }
506 })
507 .on_drag_start({
508 let drag_active = drag_active.clone();
509 move |_start| {
510 drag_active.set(true);
511 Some(Rc::new(DockTabPayload { panel_id: drag_pid })
512 as Rc<dyn Any>)
513 }
514 })
515 .on_drag_end({
516 let hover_sig = hover_sig.clone();
517 let drag_active = drag_active.clone();
518 move |_end| {
519 drag_active.set(false);
520 hover_sig.set(None);
521 }
522 }))
523 .child(
524 Row(Modifier::new().height(24.0))
525 .child((Text(title).color(th.on_surface),)),
526 ),
527 Row(Modifier::new().absolute().height(24.0).offset(
528 None,
529 Some(4.0),
530 Some(2.0),
531 None,
532 ))
533 .child((
534 if let Some(pop) = cb_pop {
535 Box(Modifier::new()
536 .clickable()
537 .on_pointer_down(move |_| pop(pid))
538 .padding(2.0))
539 .child(Text("↗").size(12.0))
540 } else {
541 Box(Modifier::new())
542 },
543 if let Some(close) = cb_close {
544 Box(Modifier::new()
545 .clickable()
546 .on_pointer_down(move |_| close(pid))
547 .padding(2.0))
548 .child(Text("×").size(12.0))
549 } else {
550 Box(Modifier::new())
551 },
552 )),
553 )),
554 )
555 })
556 .collect::<Vec<_>>(),
557 );
558
559 let content = if let Some(pid) = active_pid {
561 if let Some(panel) = registry.get(&pid) {
562 (panel.content)()
563 } else {
564 Text("Missing panel").color(th.error)
565 }
566 } else {
567 Text("No tabs").color(th.on_surface)
568 };
569
570 let overlay = dock_drop_overlay(node_id, state, hover_sig, drag_active, key_prefix);
572
573 let tab_h = 40.0;
574
575 Stack(Modifier::new().fill_max_size().key(node_id)).child((
576 Column(Modifier::new().fill_max_size()).child((
577 tab_bar,
578 Box(Modifier::new().fill_max_size().background(th.background))
579 .child(Box(Modifier::new().fill_max_size().padding(8.0)).child(content)),
580 )),
581 Box(Modifier::new()
582 .absolute()
583 .offset(Some(0.0), Some(tab_h), Some(0.0), Some(0.0)))
584 .child(overlay),
585 ))
586}
587
588fn dock_drop_overlay(
589 node_id: u64,
590 state: &Rc<RefCell<DockState>>,
591 hover_sig: &Signal<Option<HoverHint>>,
592 drag_active: &Signal<bool>,
593 key_prefix: &str,
594) -> View {
595 let th = theme();
596 if !drag_active.get() {
597 return Box(Modifier::new());
598 }
599
600 let zone_dp = 48.0;
601
602 let hover = hover_sig.get();
603
604 let mk_zone = |zone: DropZone, m: Modifier| -> View {
605 let state2 = state.clone();
606 let hover2 = hover_sig.clone();
607
608 let label = match zone {
609 DropZone::Center => " ",
611 DropZone::Left => " ",
612 DropZone::Right => " ",
613 DropZone::Top => " ",
614 DropZone::Bottom => " ",
615 DropZone::Float => " ",
616 };
617
618 let highlight = if hover.as_ref() == Some(&HoverHint { node_id, zone }) {
619 Stack(
620 Modifier::new()
621 .fill_max_size()
622 .background(th.primary.with_alpha(51))
623 .border(2.0, th.primary, 0.0),
624 )
625 .child(Text(label).size(12.0).color(th.on_primary))
626 } else {
627 Stack(
628 Modifier::new()
629 .fill_max_size()
630 .border(1.0, th.outline_variant, 0.0),
631 )
632 .child(Text(label).size(12.0).color(th.on_surface_variant))
633 };
634
635 Stack(
636 m.z_index(2000.0)
637 .key(hash_zone_key(node_id, zone))
638 .on_drag_enter({
639 let hover2 = hover2.clone();
640 move |_ev| {
641 hover2.set(Some(HoverHint { node_id, zone }));
642 }
643 })
644 .on_drag_over({
645 let hover2 = hover2.clone();
646 move |_ev| {
647 hover2.set(Some(HoverHint { node_id, zone }));
648 }
649 })
650 .on_drag_leave({
651 let hover2 = hover2.clone();
652 move |_ev| {
653 if hover2.get().as_ref() == Some(&HoverHint { node_id, zone }) {
655 hover2.set(None);
656 }
657 }
658 })
659 .on_drop(move |ev| {
660 let Some(p) = ev.payload.as_ref().downcast_ref::<DockTabPayload>() else {
661 return false;
662 };
663
664 let ok = state2.borrow_mut().dock_panel(node_id, zone, p.panel_id);
665 hover2.set(None);
666 request_frame();
667 ok
668 }),
669 )
670 .child(highlight)
671 };
672
673 let left = mk_zone(
676 DropZone::Left,
677 Modifier::new()
678 .absolute()
679 .offset(Some(0.0), Some(0.0), None, Some(0.0))
680 .width(zone_dp),
681 );
682
683 let right = mk_zone(
684 DropZone::Right,
685 Modifier::new()
686 .absolute()
687 .offset(None, Some(0.0), Some(0.0), Some(0.0))
688 .width(zone_dp),
689 );
690
691 let top = mk_zone(
692 DropZone::Top,
693 Modifier::new()
694 .absolute()
695 .offset(Some(zone_dp), Some(0.0), Some(zone_dp), None)
696 .height(zone_dp),
697 );
698
699 let bottom = mk_zone(
700 DropZone::Bottom,
701 Modifier::new()
702 .absolute()
703 .offset(Some(zone_dp), None, Some(zone_dp), Some(0.0))
704 .height(zone_dp),
705 );
706
707 let center = mk_zone(
708 DropZone::Center,
709 Modifier::new().absolute().offset(
710 Some(zone_dp),
711 Some(zone_dp),
712 Some(zone_dp),
713 Some(zone_dp),
714 ),
715 );
716
717 Stack(
718 Modifier::new()
719 .fill_max_size()
720 .key(hash_str_key(key_prefix, node_id)),
721 )
722 .child((left, right, top, bottom, center))
723}
724
725fn render_split(
726 node_id: u64,
727 dir: SplitDir,
728 ratio: f32,
729 a: &DockNode,
730 b: &DockNode,
731 registry: &Rc<HashMap<PanelId, DockPanel>>,
732 state: &Rc<RefCell<DockState>>,
733 callbacks: &DockCallbacks,
734 hover_sig: &Signal<Option<HoverHint>>,
735 drag_active: &Signal<bool>,
736 split_hover: &Signal<Option<u64>>,
737 split_drag: &Rc<RefCell<Option<SplitDrag>>>,
738 key_prefix: &str,
739) -> View {
740 let th = theme();
741 let ratio = ratio.clamp(0.05, 0.95);
742
743 let rect_rc = remember_with_key(format!("dock:split_rect:{}:{node_id}", key_prefix), || {
745 RefCell::new(Rect::default())
746 });
747
748 let track = {
750 let rect_rc = rect_rc.clone();
751 Modifier::new().painter(move |_scene, r, _alpha| {
752 *rect_rc.borrow_mut() = r;
753 })
754 };
755
756 let divider_thick = 8.0;
757
758 let start_drag = {
759 let split_drag = split_drag.clone();
760 move |_pe: PointerEvent| {
761 *split_drag.borrow_mut() = Some(SplitDrag { node_id, dir });
762 }
763 };
764
765 let move_drag = {
766 let split_drag = split_drag.clone();
767 let rect_rc = rect_rc.clone();
768 let state = state.clone();
769 move |pe: PointerEvent| {
770 let Some(sd) = split_drag.borrow().clone() else {
771 return;
772 };
773 if sd.node_id != node_id {
774 return;
775 }
776 let r = *rect_rc.borrow();
777 if r.w <= 1.0 || r.h <= 1.0 {
778 return;
779 }
780 let t = match dir {
781 SplitDir::Horizontal => (pe.position.x - r.x) / r.w,
782 SplitDir::Vertical => (pe.position.y - r.y) / r.h,
783 };
784 state.borrow_mut().set_split_ratio(node_id, t);
785 request_frame();
786 }
787 };
788
789 let end_drag = {
790 let split_drag = split_drag.clone();
791 move |_pe: PointerEvent| {
792 *split_drag.borrow_mut() = None;
794 }
795 };
796
797 let hovered = split_hover.get() == Some(node_id);
799 let line_color = if hovered { th.focus } else { th.outline };
800 let hit_color = if hovered {
801 line_color.with_alpha(40)
802 } else {
803 line_color.with_alpha(20)
804 };
805
806 let splitter_mod = match dir {
807 SplitDir::Horizontal => Modifier::new().width(divider_thick).fill_max_height(),
808 SplitDir::Vertical => Modifier::new().height(divider_thick).fill_max_width(),
809 };
810
811 let divider = Stack(
812 splitter_mod
813 .background(hit_color)
814 .on_pointer_enter({
815 let split_hover = split_hover.clone();
816 move |_| split_hover.set(Some(node_id))
817 })
818 .on_pointer_leave({
819 let split_hover = split_hover.clone();
820 move |_| {
821 if split_hover.get() == Some(node_id) {
822 split_hover.set(None);
823 }
824 }
825 })
826 .on_pointer_down(start_drag)
827 .on_pointer_move(move_drag)
828 .on_pointer_up(end_drag)
829 .cursor(match dir {
830 SplitDir::Horizontal => CursorIcon::EwResize,
831 SplitDir::Vertical => CursorIcon::NsResize,
832 })
833 .z_index(1500.0),
834 )
835 .child((
836 match dir {
838 SplitDir::Horizontal => Box(Modifier::new()
839 .absolute()
840 .offset(
841 Some((divider_thick - 1.0) * 0.5),
842 Some(0.0),
843 None,
844 Some(0.0),
845 )
846 .width(1.0)
847 .fill_max_height()
848 .background(line_color)),
849 SplitDir::Vertical => Box(Modifier::new()
850 .absolute()
851 .offset(
852 Some(0.0),
853 Some((divider_thick - 1.0) * 0.5),
854 Some(0.0),
855 None,
856 )
857 .height(1.0)
858 .fill_max_width()
859 .background(line_color)),
860 },
861 ));
862
863 let a_view = render_node(
864 a,
865 registry,
866 state,
867 callbacks,
868 hover_sig,
869 drag_active,
870 split_hover,
871 split_drag,
872 key_prefix,
873 );
874 let b_view = render_node(
875 b,
876 registry,
877 state,
878 callbacks,
879 hover_sig,
880 drag_active,
881 split_hover,
882 split_drag,
883 key_prefix,
884 );
885
886 match dir {
887 SplitDir::Horizontal => Row(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 SplitDir::Vertical => Column(track.fill_max_size().key(node_id)).child((
893 Box(Modifier::new().weight(ratio)).child(a_view),
894 divider,
895 Box(Modifier::new().weight(1.0 - ratio)).child(b_view),
896 )),
897 }
898}
899
900fn find_node_mut(node: &mut DockNode, id: u64) -> Option<&mut DockNode> {
901 if node.id == id {
902 return Some(node);
903 }
904 match &mut node.kind {
905 DockKind::Split { a, b, .. } => find_node_mut(a, id).or_else(|| find_node_mut(b, id)),
906 _ => None,
907 }
908}
909
910fn remove_panel_in_node(node: &mut DockNode, pid: PanelId) -> bool {
911 match &mut node.kind {
912 DockKind::Empty => false,
913
914 DockKind::Tabs { tabs, active } => {
915 let before = tabs.len();
916 tabs.retain(|&x| x != pid);
917 if tabs.len() != before {
918 if active == &Some(pid) {
919 *active = tabs.first().copied();
920 }
921 if tabs.is_empty() {
922 node.kind = DockKind::Empty;
923 }
924 true
925 } else {
926 false
927 }
928 }
929
930 DockKind::Split { a, b, .. } => {
931 let ra = remove_panel_in_node(a, pid);
932 let rb = remove_panel_in_node(b, pid);
933 ra || rb
934 }
935 }
936}
937
938fn normalize_node(node: &mut DockNode) {
939 match &mut node.kind {
940 DockKind::Empty => {}
941 DockKind::Tabs { tabs, active } => {
942 if tabs.is_empty() {
943 node.kind = DockKind::Empty;
944 } else if active.is_none() || !tabs.contains(&active.unwrap()) {
945 *active = tabs.first().copied();
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(st.root.id, DropZone::Center, 2));
1000 }
1001
1002 #[test]
1003 fn remove_collapses_empty_split() {
1004 let mut st = DockState::new_with_tabs(vec![10]);
1005 assert!(st.dock_panel(1, DropZone::Right, 20)); assert!(st.remove_panel(10));
1007 st.normalize();
1008 fn count_tabs(n: &DockNode) -> usize {
1011 match &n.kind {
1012 DockKind::Tabs { tabs, .. } => tabs.len(),
1013 DockKind::Split { a, b, .. } => count_tabs(a) + count_tabs(b),
1014 DockKind::Empty => 0,
1015 }
1016 }
1017 assert_eq!(count_tabs(&st.root), 1);
1018 }
1019}