1#![allow(non_snake_case)]
2
3use std::cell::RefCell;
4use std::collections::HashMap;
5use std::rc::Rc;
6
7use repose_core::*;
8use repose_ui::TextStyle;
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
250#[derive(Clone)]
254pub struct DockHandle {
255 pub(crate) key: String,
256 pub(crate) state: Rc<RefCell<DockState>>,
257 pub(crate) callbacks: DockCallbacks,
258 pub(crate) hover_sig: Signal<Option<HoverHint>>,
259 pub(crate) tab_hover: Signal<Option<PanelId>>,
260 pub(crate) drag_active: Signal<bool>,
261}
262
263pub fn remember_dock_handle(
266 key: impl Into<String>,
267 state: Rc<RefCell<DockState>>,
268 callbacks: DockCallbacks,
269) -> DockHandle {
270 let key = key.into();
271
272 let hover_sig = remember_with_key(format!("dock:hover:{key}"), || signal(None::<HoverHint>));
273 let tab_hover = remember_with_key(format!("dock:tab_hover:{key}"), || signal(None::<PanelId>));
274 let drag_active = remember_with_key(format!("dock:drag_active:{key}"), || signal(false));
275
276 DockHandle {
277 key,
278 state,
279 callbacks,
280 hover_sig: (*hover_sig).clone(),
281 tab_hover: (*tab_hover).clone(),
282 drag_active: (*drag_active).clone(),
283 }
284}
285
286pub trait DockModifierExt: Sized {
293 fn dock_tab_source(self, dock: &DockHandle, panel_id: PanelId) -> Modifier;
295
296 fn dock_tab_strip_drop_target(
298 self,
299 dock: &DockHandle,
300 node_id: u64,
301 tabbar_rect: Rc<RefCell<Rect>>,
302 ) -> Modifier;
303
304 fn dock_drop_zone(self, dock: &DockHandle, node_id: u64, zone: DropZone) -> Modifier;
306
307 fn dock_float_target(self, dock: &DockHandle) -> Modifier;
309}
310
311impl DockModifierExt for Modifier {
312 fn dock_tab_source(self, dock: &DockHandle, panel_id: PanelId) -> Modifier {
313 let drag_active_start = dock.drag_active.clone();
314
315 let hover_end = dock.hover_sig.clone();
316 let drag_active_end = dock.drag_active.clone();
317
318 self.cursor(CursorIcon::Grab)
319 .drag_source::<DockTabPayload>(move |_start| {
320 drag_active_start.set(true);
321 Some(DockTabPayload { panel_id })
322 })
323 .on_drag_end(move |_end| {
324 drag_active_end.set(false);
325 hover_end.set(None);
326 })
327 }
328
329 fn dock_tab_strip_drop_target(
330 self,
331 dock: &DockHandle,
332 node_id: u64,
333 tabbar_rect: Rc<RefCell<Rect>>,
334 ) -> Modifier {
335 let state = dock.state.clone();
336 let hover_sig = dock.hover_sig.clone();
337 let drag_active = dock.drag_active.clone();
338
339 self.on_drop_typed::<DockTabPayload>(move |ev, p| {
340 let mut st = state.borrow_mut();
341
342 st.remove_panel_no_normalize(p.panel_id);
344
345 let r = *tabbar_rect.borrow();
346 let t = if r.w > 1.0 {
347 ((ev.position.x - r.x) / r.w).clamp(0.0, 1.0)
348 } else {
349 1.0
350 };
351
352 if let Some(n) = find_node_mut(&mut st.root, node_id) {
353 if matches!(n.kind, DockKind::Empty) {
354 n.kind = DockKind::Tabs {
355 tabs: Vec::new(),
356 active: None,
357 };
358 }
359
360 if let DockKind::Tabs { tabs, active } = &mut n.kind {
361 tabs.retain(|&x| x != p.panel_id);
362 let idx = ((t * (tabs.len() as f32 + 1.0)).floor() as usize).min(tabs.len());
363 tabs.insert(idx, p.panel_id);
364 *active = Some(p.panel_id);
365 }
366 }
367
368 st.normalize();
369 hover_sig.set(None);
370 drag_active.set(false);
371 request_frame();
372 true
373 })
374 }
375
376 fn dock_drop_zone(self, dock: &DockHandle, node_id: u64, zone: DropZone) -> Modifier {
377 let hover_enter = dock.hover_sig.clone();
378 let hover_over = dock.hover_sig.clone();
379 let hover_leave = dock.hover_sig.clone();
380 let hover_drop = dock.hover_sig.clone();
381 let state = dock.state.clone();
382
383 self.z_index(3000.0)
384 .render_z_index(3000.0)
385 .key(hash_zone_key(node_id, zone))
386 .on_drag_enter_typed::<DockTabPayload>(move |_ev, _p| {
387 hover_enter.set(Some(HoverHint { node_id, zone }));
388 })
389 .on_drag_over_typed::<DockTabPayload>(move |_ev, _p| {
390 hover_over.set(Some(HoverHint { node_id, zone }));
391 })
392 .on_drag_leave_typed::<DockTabPayload>(move |_ev, _p| {
393 if hover_leave.get().as_ref() == Some(&HoverHint { node_id, zone }) {
394 hover_leave.set(None);
395 }
396 })
397 .on_drop_typed::<DockTabPayload>(move |_ev, p| {
398 let ok = state.borrow_mut().dock_panel(node_id, zone, p.panel_id);
399 hover_drop.set(None);
400 request_frame();
401 ok
402 })
403 }
404
405 fn dock_float_target(self, dock: &DockHandle) -> Modifier {
406 let state = dock.state.clone();
407 let hover_sig = dock.hover_sig.clone();
408 let cb_pop = dock.callbacks.on_popout.clone();
409
410 self.on_drop_typed::<DockTabPayload>(move |_ev, p| {
411 let Some(pop) = cb_pop.as_ref() else {
412 return false;
413 };
414
415 state.borrow_mut().remove_panel(p.panel_id);
416 pop(p.panel_id);
417 hover_sig.set(None);
418 request_frame();
419 true
420 })
421 }
422}
423
424pub fn DockArea(
425 key: impl Into<String>,
426 modifier: Modifier,
427 state: Rc<RefCell<DockState>>,
428 panels: Vec<DockPanel>,
429 callbacks: DockCallbacks,
430) -> View {
431 let key = key.into();
432 let registry = Rc::new(build_registry(panels));
433
434 let dock = remember_dock_handle(key.clone(), state, callbacks);
435
436 let split_hover = remember_with_key(format!("dock:split_hover:{key}"), || signal(None::<u64>));
437 let split_drag = remember_with_key(format!("dock:split_drag:{key}"), || {
438 RefCell::new(None::<SplitDrag>)
439 });
440
441 let float_target = Box(
444 Modifier::new()
445 .fill_max_size()
446 .z_index(-1000.0)
447 .dock_float_target(&dock),
448 );
449
450 let root_view = {
452 let st = dock.state.borrow().clone();
453 render_node(
454 &st.root,
455 ®istry,
456 &dock,
457 &split_hover,
458 &split_drag,
459 key.as_str(),
460 )
461 };
462
463 ZStack(modifier.fill_max_size()).child((
464 Box(Modifier::new()
465 .absolute()
466 .offset(Some(0.0), Some(0.0), Some(0.0), Some(0.0)))
467 .child(float_target),
468 Box(Modifier::new()
469 .absolute()
470 .offset(Some(0.0), Some(0.0), Some(0.0), Some(0.0)))
471 .child(root_view),
472 ))
473}
474
475fn build_registry(panels: Vec<DockPanel>) -> HashMap<PanelId, DockPanel> {
476 let mut m = HashMap::new();
477 for p in panels {
478 m.insert(p.id, p);
479 }
480 m
481}
482
483fn render_node(
484 node: &DockNode,
485 registry: &Rc<HashMap<PanelId, DockPanel>>,
486 dock: &DockHandle,
487 split_hover: &Signal<Option<u64>>,
488 split_drag: &Rc<RefCell<Option<SplitDrag>>>,
489 key_prefix: &str,
490) -> View {
491 match &node.kind {
492 DockKind::Empty => Box(
493 Modifier::new()
494 .fill_max_size()
495 .padding(6.0)
496 .background(theme().surface_container_lowest)
497 .clip_rounded(theme().shapes.medium)
498 .border(
499 1.0,
500 theme().outline_variant.with_alpha(80),
501 theme().shapes.medium,
502 )
503 .key(node.id),
504 )
505 .child(
506 Box(Modifier::new().fill_max_size().padding(16.0)).child(
507 Text("Drop panel here")
508 .size(theme().typography.label_medium)
509 .color(theme().on_surface_variant),
510 ),
511 ),
512
513 DockKind::Tabs { tabs, active } => render_tabs(
514 node.id,
515 tabs,
516 *active,
517 registry,
518 dock,
519 split_hover,
520 key_prefix,
521 ),
522
523 DockKind::Split { dir, ratio, a, b } => render_split(
524 node.id,
525 *dir,
526 *ratio,
527 a,
528 b,
529 registry,
530 dock,
531 split_hover,
532 split_drag,
533 key_prefix,
534 ),
535 }
536}
537
538fn render_tabs(
539 node_id: u64,
540 tabs: &Vec<PanelId>,
541 active: Option<PanelId>,
542 registry: &Rc<HashMap<PanelId, DockPanel>>,
543 dock: &DockHandle,
544 _split_hover: &Signal<Option<u64>>,
545 key_prefix: &str,
546) -> View {
547 let th = theme();
548
549 const PANEL_PAD: f32 = 5.0;
550 const TAB_BAR_H: f32 = 44.0;
551 const TAB_H: f32 = 32.0;
552 const TAB_RADIUS: f32 = 16.0;
553
554 let active_pid = active.or_else(|| tabs.first().copied());
556
557 let tabbar_rect = remember_with_key(format!("dock:tabbar_rect:{key_prefix}:{node_id}"), || {
558 RefCell::new(Rect::default())
559 });
560
561 let strip_bg = th.surface_container_low;
562 let active_bg = th.secondary_container;
563 let active_fg = th.on_secondary_container;
564 let inactive_fg = th.on_surface_variant;
565 let hover_bg = th.surface_container_high;
566
567 let mut bar_mod = Modifier::new()
568 .fill_max_width()
569 .height(TAB_BAR_H)
570 .background(strip_bg)
571 .padding_values(PaddingValues {
572 left: 8.0,
573 right: 8.0,
574 top: 6.0,
575 bottom: 6.0,
576 })
577 .gap(6.0)
578 .painter({
579 let tabbar_rect = tabbar_rect.clone();
580 move |_scene, r, _alpha| *tabbar_rect.borrow_mut() = r
581 });
582
583 if dock.drag_active.get() {
584 bar_mod = bar_mod.dock_tab_strip_drop_target(dock, node_id, tabbar_rect.clone());
585 }
586
587 let tab_bar = Row(bar_mod).with_children(
588 tabs.iter()
589 .copied()
590 .filter_map(|pid| {
591 let panel = registry.get(&pid)?;
592 let is_active = Some(pid) == active_pid;
593 let is_hovered = dock.tab_hover.get() == Some(pid);
594
595 let state_set = dock.state.clone();
596 let title = panel.title.clone();
597 let drag_pid = pid;
598
599 let cb_close = dock.callbacks.on_close.clone();
600 let cb_pop = dock.callbacks.on_popout.clone();
601
602 let tab_bg = if is_active {
603 active_bg
604 } else if is_hovered {
605 hover_bg
606 } else {
607 Color::TRANSPARENT
608 };
609
610 let tab_fg = if is_active { active_fg } else { inactive_fg };
611
612 let hover_in = {
613 let tab_hover = dock.tab_hover.clone();
614 move |_| tab_hover.set(Some(pid))
615 };
616
617 let hover_out = {
618 let tab_hover = dock.tab_hover.clone();
619 move |_| {
620 if tab_hover.get() == Some(pid) {
621 tab_hover.set(None);
622 }
623 }
624 };
625
626 let pop_view = if let Some(pop) = cb_pop {
627 let state_for_pop = dock.state.clone();
628 dock_tab_icon_button("↗", tab_fg, move |_| {
629 state_for_pop.borrow_mut().remove_panel(pid);
630 pop(pid);
631 request_frame();
632 })
633 } else {
634 Box(Modifier::new())
635 };
636
637 let close_view = if let Some(close) = cb_close {
638 dock_tab_icon_button("×", tab_fg, move |_| {
639 close(pid);
640 request_frame();
641 })
642 } else {
643 Box(Modifier::new())
644 };
645
646 Some(
647 Row(
648 Modifier::new()
649 .key(pid)
650 .height(TAB_H)
651 .min_width(108.0)
652 .max_width(240.0)
653 .clip_rounded(TAB_RADIUS)
654 .background(tab_bg)
655 .padding_values(PaddingValues {
656 left: 12.0,
657 right: 4.0,
658 top: 0.0,
659 bottom: 0.0,
660 })
661 .gap(4.0)
662 .clickable()
663 .on_pointer_enter(hover_in)
664 .on_pointer_leave(hover_out)
665 .on_pointer_down({
666 let state_set = state_set.clone();
667 move |_| {
668 state_set.borrow_mut().set_active(node_id, pid);
669 request_frame();
670 }
671 })
672 .drag_preview_chip(title.clone(), th.primary)
673 .dock_tab_source(dock, drag_pid),
674 )
675 .child((
676 Box(
677 Modifier::new()
678 .height(TAB_H)
679 .weight(1.0)
680 .padding_values(PaddingValues {
681 left: 0.0,
682 right: 4.0,
683 top: 0.0,
684 bottom: 0.0,
685 })
686 .content_alignment(Alignment::Center),
687 )
688 .child(
689 Text(title)
690 .size(th.typography.label_large)
691 .single_line()
692 .overflow_ellipsize()
693 .color(tab_fg),
694 ),
695 pop_view,
696 close_view,
697 )),
698 )
699 })
700 .collect::<Vec<_>>(),
701 );
702
703 let content = if let Some(pid) = active_pid {
705 if let Some(panel) = registry.get(&pid) {
706 (panel.content)()
707 } else {
708 Text("Missing panel").color(th.error)
709 }
710 } else {
711 Text("No tabs").color(th.on_surface_variant)
712 };
713
714 let overlay = dock_drop_overlay(node_id, dock, key_prefix);
716
717 ZStack(Modifier::new().fill_max_size().key(node_id)).child((
718 Column(
719 Modifier::new()
720 .fill_max_size()
721 .padding(PANEL_PAD)
722 .clip_rounded(th.shapes.medium)
723 .background(th.surface_container_lowest)
724 .border(1.0, th.outline_variant.with_alpha(70), th.shapes.medium),
725 )
726 .child((
727 tab_bar,
728 Box(
729 Modifier::new()
730 .fill_max_size()
731 .background(th.surface_container_lowest),
732 )
733 .child(Box(Modifier::new().fill_max_size().padding(8.0)).child(content)),
734 )),
735 Box(Modifier::new()
736 .absolute()
737 .offset(
738 Some(PANEL_PAD),
739 Some(PANEL_PAD + TAB_BAR_H),
740 Some(PANEL_PAD),
741 Some(PANEL_PAD),
742 )
743 .render_z_index(2000.0))
744 .child(overlay),
745 ))
746}
747
748fn dock_tab_icon_button(
749 label: &'static str,
750 fg: Color,
751 on_click: impl Fn(PointerEvent) + 'static,
752) -> View {
753 Box(
754 Modifier::new()
755 .size(26.0, 26.0)
756 .padding(2.0)
757 .clip_rounded(13.0)
758 .background(fg.with_alpha(18))
759 .clickable()
760 .cursor(CursorIcon::Pointer)
761 .on_pointer_down(on_click),
762 )
763 .child(
764 Box(
765 Modifier::new()
766 .fill_max_size()
767 .content_alignment(Alignment::Center),
768 )
769 .child(Text(label).size(14.0).color(fg)),
770 )
771}
772
773fn dock_drop_overlay(node_id: u64, dock: &DockHandle, key_prefix: &str) -> View {
774 let th = theme();
775
776 if !dock.drag_active.get() {
777 return Box(Modifier::new().hit_passthrough());
778 }
779
780 let zone_dp = 72.0;
781 let hover = dock.hover_sig.get();
782
783 let preview = if let Some(h) = hover.as_ref() {
784 if h.node_id == node_id {
785 dock_drop_preview(h.zone)
786 } else {
787 Box(Modifier::new())
788 }
789 } else {
790 Box(Modifier::new())
791 };
792
793 let mk_zone = |zone: DropZone, m: Modifier| -> View { Box(m.dock_drop_zone(dock, node_id, zone)) };
794
795 let left = mk_zone(
798 DropZone::Left,
799 Modifier::new()
800 .absolute()
801 .offset(Some(0.0), Some(0.0), None, Some(0.0))
802 .width(zone_dp),
803 );
804
805 let right = mk_zone(
806 DropZone::Right,
807 Modifier::new()
808 .absolute()
809 .offset(None, Some(0.0), Some(0.0), Some(0.0))
810 .width(zone_dp),
811 );
812
813 let top = mk_zone(
814 DropZone::Top,
815 Modifier::new()
816 .absolute()
817 .offset(Some(zone_dp), Some(0.0), Some(zone_dp), None)
818 .height(zone_dp),
819 );
820
821 let bottom = mk_zone(
822 DropZone::Bottom,
823 Modifier::new()
824 .absolute()
825 .offset(Some(zone_dp), None, Some(zone_dp), Some(0.0))
826 .height(zone_dp),
827 );
828
829 let center = mk_zone(
830 DropZone::Center,
831 Modifier::new().absolute().offset(
832 Some(zone_dp),
833 Some(zone_dp),
834 Some(zone_dp),
835 Some(zone_dp),
836 ),
837 );
838
839 ZStack(
840 Modifier::new()
841 .fill_max_size()
842 .key(hash_str_key(key_prefix, node_id)),
843 )
844 .child((
845 Box(
847 Modifier::new()
848 .fill_max_size()
849 .background(th.scrim.with_alpha(18))
850 .hit_passthrough()
851 .render_z_index(1000.0),
852 ),
853 Box(Modifier::new()
854 .fill_max_size()
855 .hit_passthrough()
856 .render_z_index(2000.0))
857 .child(preview),
858 left,
859 right,
860 top,
861 bottom,
862 center,
863 ))
864}
865
866fn dock_drop_preview(zone: DropZone) -> View {
867 let th = theme();
868
869 let fill = th
870 .primary
871 .with_alpha(38)
872 .composite_over(th.surface_container_lowest);
873 let border = th.primary.with_alpha(210);
874 let radius = th.shapes.large;
875
876 let card = |label: &'static str, modifier: Modifier| -> View {
877 Box(
878 modifier
879 .clip_rounded(radius)
880 .background(fill)
881 .border(2.0, border, radius),
882 )
883 .child(
884 Box(Modifier::new().padding(12.0)).child(
885 Text(label)
886 .size(th.typography.label_medium)
887 .single_line()
888 .color(th.primary),
889 ),
890 )
891 };
892
893 match zone {
894 DropZone::Center => card(
895 "Add as tab",
896 Modifier::new()
897 .absolute()
898 .offset(Some(14.0), Some(14.0), Some(14.0), Some(14.0)),
899 ),
900
901 DropZone::Left => Row(Modifier::new().fill_max_size().padding(14.0).gap(10.0)).child((
902 card("Split left", Modifier::new().weight(0.44).fill_max_height()),
903 Box(Modifier::new().weight(0.56)),
904 )),
905
906 DropZone::Right => Row(Modifier::new().fill_max_size().padding(14.0).gap(10.0)).child((
907 Box(Modifier::new().weight(0.56)),
908 card("Split right", Modifier::new().weight(0.44).fill_max_height()),
909 )),
910
911 DropZone::Top => Column(
912 Modifier::new()
913 .fill_max_size()
914 .padding(14.0)
915 .gap(10.0),
916 )
917 .child((
918 card("Split top", Modifier::new().weight(0.44).fill_max_width()),
919 Box(Modifier::new().weight(0.56)),
920 )),
921
922 DropZone::Bottom => Column(
923 Modifier::new()
924 .fill_max_size()
925 .padding(14.0)
926 .gap(10.0),
927 )
928 .child((
929 Box(Modifier::new().weight(0.56)),
930 card("Split bottom", Modifier::new().weight(0.44).fill_max_width()),
931 )),
932
933 DropZone::Float => Box(Modifier::new()),
934 }
935}
936
937fn render_split(
938 node_id: u64,
939 dir: SplitDir,
940 ratio: f32,
941 a: &DockNode,
942 b: &DockNode,
943 registry: &Rc<HashMap<PanelId, DockPanel>>,
944 dock: &DockHandle,
945 split_hover: &Signal<Option<u64>>,
946 split_drag: &Rc<RefCell<Option<SplitDrag>>>,
947 key_prefix: &str,
948) -> View {
949 let th = theme();
950 let ratio = ratio.clamp(0.05, 0.95);
951
952 let rect_rc = remember_with_key(format!("dock:split_rect:{}:{node_id}", key_prefix), || {
954 RefCell::new(Rect::default())
955 });
956
957 let track = {
959 let rect_rc = rect_rc.clone();
960 Modifier::new().painter(move |_scene, r, _alpha| {
961 *rect_rc.borrow_mut() = r;
962 })
963 };
964
965 let divider_thick = 8.0;
966
967 let start_drag = {
968 let split_drag = split_drag.clone();
969 move |_pe: PointerEvent| {
970 *split_drag.borrow_mut() = Some(SplitDrag { node_id, dir });
971 request_frame();
972 }
973 };
974
975 let move_drag = {
976 let split_drag = split_drag.clone();
977 let rect_rc = rect_rc.clone();
978 let state = dock.state.clone();
979 move |pe: PointerEvent| {
980 let Some(sd) = split_drag.borrow().clone() else {
981 return;
982 };
983 if sd.node_id != node_id {
984 return;
985 }
986 let r = *rect_rc.borrow();
987 if r.w <= 1.0 || r.h <= 1.0 {
988 return;
989 }
990 let mut t = match dir {
991 SplitDir::Horizontal => (pe.position.x - r.x) / r.w,
992 SplitDir::Vertical => (pe.position.y - r.y) / r.h,
993 };
994 for snap in [0.25_f32, 0.5, 0.75] {
995 if (t - snap).abs() < 0.018 {
996 t = snap;
997 break;
998 }
999 }
1000 state.borrow_mut().set_split_ratio(node_id, t);
1001 request_frame();
1002 }
1003 };
1004
1005 let end_drag = {
1006 let split_drag = split_drag.clone();
1007 move |_pe: PointerEvent| {
1008 *split_drag.borrow_mut() = None;
1010 request_frame();
1011 }
1012 };
1013
1014 let hovered = split_hover.get() == Some(node_id);
1017 let dragging = split_drag
1018 .borrow()
1019 .as_ref()
1020 .map(|sd| sd.node_id == node_id)
1021 .unwrap_or(false);
1022
1023 let active = hovered || dragging;
1024
1025 let gutter_color = if active {
1026 th.primary.with_alpha(24)
1027 } else {
1028 Color::TRANSPARENT
1029 };
1030
1031 let grabber_color = if active {
1032 th.primary
1033 } else {
1034 th.outline_variant.with_alpha(0)
1035 };
1036
1037 let splitter_mod = match dir {
1038 SplitDir::Horizontal => Modifier::new().width(divider_thick).fill_max_height(),
1039 SplitDir::Vertical => Modifier::new().height(divider_thick).fill_max_width(),
1040 };
1041
1042 let grabber = match dir {
1043 SplitDir::Horizontal => Box(
1044 Modifier::new()
1045 .fill_max_size()
1046 .content_alignment(Alignment::Center)
1047 .clip_rounded(4.0),
1048 )
1049 .child(Box(
1050 Modifier::new()
1051 .width(4.0)
1052 .offset(None, Some(24.0), None, Some(24.0))
1053 .background(grabber_color),
1054 )),
1055 SplitDir::Vertical => Box(
1056 Modifier::new()
1057 .fill_max_size()
1058 .content_alignment(Alignment::Center)
1059 .clip_rounded(4.0),
1060 )
1061 .child(Box(
1062 Modifier::new()
1063 .height(4.0)
1064 .offset(Some(24.0), None, Some(24.0), None)
1065 .background(grabber_color),
1066 )),
1067 };
1068
1069 let divider = Box(
1070 splitter_mod
1071 .background(gutter_color)
1072 .on_pointer_enter({
1073 let split_hover = split_hover.clone();
1074 move |_| {
1075 split_hover.set(Some(node_id));
1076 request_frame();
1077 }
1078 })
1079 .on_pointer_leave({
1080 let split_hover = split_hover.clone();
1081 move |_| {
1082 if split_hover.get() == Some(node_id) {
1083 split_hover.set(None);
1084 request_frame();
1085 }
1086 }
1087 })
1088 .on_pointer_down(start_drag)
1089 .on_pointer_move(move_drag)
1090 .on_pointer_up(end_drag)
1091 .cursor(match dir {
1092 SplitDir::Horizontal => CursorIcon::EwResize,
1093 SplitDir::Vertical => CursorIcon::NsResize,
1094 })
1095 .z_index(1500.0)
1096 .render_z_index(1500.0),
1097 )
1098 .child(grabber);
1099
1100 let a_view = render_node(
1101 a,
1102 registry,
1103 dock,
1104 split_hover,
1105 split_drag,
1106 key_prefix,
1107 );
1108 let b_view = render_node(
1109 b,
1110 registry,
1111 dock,
1112 split_hover,
1113 split_drag,
1114 key_prefix,
1115 );
1116
1117 match dir {
1118 SplitDir::Horizontal => Row(track.fill_max_size().key(node_id)).child((
1119 Box(Modifier::new().weight(ratio)).child(a_view),
1120 divider,
1121 Box(Modifier::new().weight(1.0 - ratio)).child(b_view),
1122 )),
1123 SplitDir::Vertical => Column(track.fill_max_size().key(node_id)).child((
1124 Box(Modifier::new().weight(ratio)).child(a_view),
1125 divider,
1126 Box(Modifier::new().weight(1.0 - ratio)).child(b_view),
1127 )),
1128 }
1129}
1130
1131fn find_node_mut(node: &mut DockNode, id: u64) -> Option<&mut DockNode> {
1132 if node.id == id {
1133 return Some(node);
1134 }
1135 match &mut node.kind {
1136 DockKind::Split { a, b, .. } => find_node_mut(a, id).or_else(|| find_node_mut(b, id)),
1137 _ => None,
1138 }
1139}
1140
1141fn remove_panel_in_node(node: &mut DockNode, pid: PanelId) -> bool {
1142 match &mut node.kind {
1143 DockKind::Empty => false,
1144
1145 DockKind::Tabs { tabs, active } => {
1146 let before = tabs.len();
1147 tabs.retain(|&x| x != pid);
1148 if tabs.len() != before {
1149 if active == &Some(pid) {
1150 *active = tabs.first().copied();
1151 }
1152 if tabs.is_empty() {
1153 node.kind = DockKind::Empty;
1154 }
1155 true
1156 } else {
1157 false
1158 }
1159 }
1160
1161 DockKind::Split { a, b, .. } => {
1162 let ra = remove_panel_in_node(a, pid);
1163 let rb = remove_panel_in_node(b, pid);
1164 ra || rb
1165 }
1166 }
1167}
1168
1169fn normalize_node(node: &mut DockNode) {
1170 match &mut node.kind {
1171 DockKind::Empty => {}
1172 DockKind::Tabs { tabs, active } => {
1173 if tabs.is_empty() {
1174 node.kind = DockKind::Empty;
1175 } else if active.is_none() || !tabs.contains(&active.unwrap()) {
1176 *active = tabs.first().copied();
1177 }
1178 }
1179 DockKind::Split { a, b, ratio, .. } => {
1180 *ratio = ratio.clamp(0.05, 0.95);
1181 normalize_node(a);
1182 normalize_node(b);
1183
1184 let a_empty = matches!(a.kind, DockKind::Empty);
1185 let b_empty = matches!(b.kind, DockKind::Empty);
1186
1187 if a_empty && !b_empty {
1189 node.kind = std::mem::replace(&mut b.kind, DockKind::Empty);
1190 } else if b_empty && !a_empty {
1191 node.kind = std::mem::replace(&mut a.kind, DockKind::Empty);
1192 } else if a_empty && b_empty {
1193 node.kind = DockKind::Empty;
1194 }
1195 }
1196 }
1197}
1198
1199fn hash_zone_key(node_id: u64, zone: DropZone) -> u64 {
1200 let z = match zone {
1201 DropZone::Center => 1u64,
1202 DropZone::Left => 2,
1203 DropZone::Right => 3,
1204 DropZone::Top => 4,
1205 DropZone::Bottom => 5,
1206 DropZone::Float => 6,
1207 };
1208 node_id ^ (z.wrapping_mul(0x9E3779B97F4A7C15))
1209}
1210
1211fn hash_str_key(prefix: &str, node_id: u64) -> u64 {
1212 let mut h = 1469598103934665603u64;
1213 for b in prefix.as_bytes() {
1214 h ^= *b as u64;
1215 h = h.wrapping_mul(1099511628211u64);
1216 }
1217 h ^ node_id.wrapping_mul(0x9E3779B97F4A7C15)
1218}
1219
1220#[cfg(test)]
1221mod tests {
1222 use super::*;
1223
1224 #[test]
1225 fn move_tab_into_center() {
1226 let mut st = DockState::new_with_tabs(vec![1, 2, 3]);
1227 assert!(st.dock_panel(1, DropZone::Right, 3));
1229 assert!(!st.dock_panel(st.root.id, DropZone::Center, 2));
1231 }
1232
1233 #[test]
1234 fn remove_collapses_empty_split() {
1235 let mut st = DockState::new_with_tabs(vec![10]);
1236 assert!(st.dock_panel(1, DropZone::Right, 20)); assert!(st.remove_panel(10));
1238 st.normalize();
1239 fn count_tabs(n: &DockNode) -> usize {
1242 match &n.kind {
1243 DockKind::Tabs { tabs, .. } => tabs.len(),
1244 DockKind::Split { a, b, .. } => count_tabs(a) + count_tabs(b),
1245 DockKind::Empty => 0,
1246 }
1247 }
1248 assert_eq!(count_tabs(&st.root), 1);
1249 }
1250}