1use std::hash::Hash;
2
3use freya_core::prelude::*;
4use torin::{
5 content::Content,
6 direction::Direction,
7 position::Position,
8 size::Size,
9};
10
11use crate::{
12 drag_drop::{
13 DragZone,
14 DropZone,
15 use_drag,
16 },
17 resizable_container::{
18 PanelSize,
19 ResizableContainer,
20 ResizablePanel,
21 },
22};
23
24#[derive(Clone, PartialEq, Debug)]
26pub struct DockPanel<TabId, PanelId> {
27 pub panel_id: PanelId,
28 pub tabs: Vec<TabId>,
29 pub active_tab_id: Option<TabId>,
30}
31
32impl<TabId, PanelId> DockPanel<TabId, PanelId> {
33 pub fn new(panel_id: PanelId, tabs: Vec<TabId>) -> Self
35 where
36 TabId: Clone,
37 {
38 let active_tab_id = tabs.first().cloned();
39 Self {
40 panel_id,
41 tabs,
42 active_tab_id,
43 }
44 }
45
46 pub fn insert_tab(&mut self, tab_id: TabId, position: usize)
48 where
49 TabId: Clone + PartialEq,
50 {
51 let target = match self.tabs.iter().position(|item| *item == tab_id) {
52 Some(existing) => {
53 self.tabs.remove(existing);
54 if position > existing {
55 position - 1
56 } else {
57 position
58 }
59 }
60 None => position,
61 };
62 self.active_tab_id = Some(tab_id.clone());
63 self.tabs.insert(target.min(self.tabs.len()), tab_id);
64 }
65
66 pub fn append_tab(&mut self, tab_id: TabId)
68 where
69 TabId: Clone + PartialEq,
70 {
71 self.tabs.retain(|item| *item != tab_id);
72 self.active_tab_id = Some(tab_id.clone());
73 self.tabs.push(tab_id);
74 }
75}
76
77#[derive(Clone, PartialEq, Debug)]
79pub enum DockNode<TabId, PanelId> {
80 Split {
81 direction: Direction,
82 children: Vec<DockNode<TabId, PanelId>>,
83 },
84 Panel(DockPanel<TabId, PanelId>),
85}
86
87impl<TabId, PanelId> DockNode<TabId, PanelId>
88where
89 TabId: Clone + PartialEq,
90 PanelId: Copy + PartialEq,
91{
92 pub fn is_empty(&self) -> bool {
94 match self {
95 DockNode::Panel(panel) => panel.tabs.is_empty(),
96 DockNode::Split { children, .. } => children.is_empty(),
97 }
98 }
99
100 pub fn panel(&self, panel_id: &PanelId) -> Option<&DockPanel<TabId, PanelId>> {
102 match self {
103 DockNode::Panel(panel) => (&panel.panel_id == panel_id).then_some(panel),
104 DockNode::Split { children, .. } => {
105 children.iter().find_map(|child| child.panel(panel_id))
106 }
107 }
108 }
109
110 pub fn panel_mut(&mut self, panel_id: &PanelId) -> Option<&mut DockPanel<TabId, PanelId>> {
112 match self {
113 DockNode::Panel(panel) => (&panel.panel_id == panel_id).then_some(panel),
114 DockNode::Split { children, .. } => children
115 .iter_mut()
116 .find_map(|child| child.panel_mut(panel_id)),
117 }
118 }
119
120 pub fn find_tab(&self, tab_id: &TabId) -> Option<(PanelId, usize)> {
122 match self {
123 DockNode::Panel(panel) => panel
124 .tabs
125 .iter()
126 .position(|item| item == tab_id)
127 .map(|position| (panel.panel_id, position)),
128 DockNode::Split { children, .. } => {
129 children.iter().find_map(|child| child.find_tab(tab_id))
130 }
131 }
132 }
133
134 pub fn remove_tab_except(&mut self, tab_id: &TabId, except_panel_id: Option<&PanelId>) -> bool {
136 match self {
137 DockNode::Panel(panel) => {
138 if except_panel_id == Some(&panel.panel_id) {
139 return false;
140 }
141 let Some(position) = panel.tabs.iter().position(|item| item == tab_id) else {
142 return false;
143 };
144 panel.tabs.remove(position);
145 if panel.active_tab_id.as_ref() == Some(tab_id) {
146 panel.active_tab_id = panel.tabs.first().cloned();
147 }
148 true
149 }
150 DockNode::Split { children, .. } => children
151 .iter_mut()
152 .any(|child| child.remove_tab_except(tab_id, except_panel_id)),
153 }
154 }
155
156 pub fn split_panel(
158 &mut self,
159 panel_id: &PanelId,
160 side: Side,
161 new_panel: &DockPanel<TabId, PanelId>,
162 ) -> bool {
163 match self {
164 DockNode::Panel(panel) if &panel.panel_id == panel_id => {
165 let new_dock_node = DockNode::Split {
166 direction: side.direction(),
167 children: Vec::new(),
168 };
169 let old_dock_node = std::mem::replace(self, new_dock_node);
170 if let DockNode::Split { children, .. } = self {
171 let new_node = DockNode::Panel(new_panel.clone());
172 *children = match side {
173 Side::Left | Side::Top => vec![new_node, old_dock_node],
174 Side::Right | Side::Bottom => vec![old_dock_node, new_node],
175 };
176 }
177 true
178 }
179 DockNode::Split { children, .. } => children
180 .iter_mut()
181 .any(|child| child.split_panel(panel_id, side, new_panel)),
182 _ => false,
183 }
184 }
185
186 pub fn close_empty_panels(&mut self) {
188 let DockNode::Split { children, .. } = self else {
189 return;
190 };
191 children.iter_mut().for_each(DockNode::close_empty_panels);
192 children.retain(|child| !child.is_empty());
193 if children.len() == 1 {
195 *self = children.remove(0);
196 }
197 }
198}
199
200#[derive(Clone, Copy, PartialEq, Eq, Debug)]
202pub enum Side {
203 Top,
204 Bottom,
205 Left,
206 Right,
207}
208
209impl Side {
210 pub fn direction(self) -> Direction {
212 match self {
213 Side::Left | Side::Right => Direction::Horizontal,
214 Side::Top | Side::Bottom => Direction::Vertical,
215 }
216 }
217}
218
219#[derive(Clone, PartialEq, Debug)]
221pub enum DropTarget<PanelId> {
222 Tab { panel_id: PanelId, position: usize },
223 Center(PanelId),
224 Split { panel_id: PanelId, side: Side },
225}
226
227pub trait DockingModel: 'static {
228 type TabId: Copy + PartialEq + Hash + 'static;
230 type PanelId: Copy + PartialEq + 'static;
232 type DropValue: Clone + PartialEq + 'static + From<Self::TabId>;
234
235 fn root(&self) -> Option<&DockNode<Self::TabId, Self::PanelId>>;
237 fn on_drop(&mut self, value: Self::DropValue, target: DropTarget<Self::PanelId>) -> bool;
240 fn set_active(&mut self, panel_id: Self::PanelId, tab_id: Self::TabId) -> bool;
242}
243
244#[derive(Clone, PartialEq, Debug)]
246pub struct DockDrag<Value> {
247 value: Value,
248}
249
250impl<Value> DockDrag<Value> {
251 pub fn new(value: Value) -> Self {
253 Self { value }
254 }
255}
256
257#[derive(Clone, PartialEq, Debug)]
259pub struct ContentContext<TabId, PanelId> {
260 pub panel_id: PanelId,
262 pub tab_id: Option<TabId>,
264 pub tab_count: usize,
266}
267
268pub struct TabBarContext<PanelId> {
270 pub panel_id: PanelId,
271 pub tab_children: Vec<Element>,
273 pub tab_count: usize,
275}
276
277#[derive(Clone, PartialEq, Debug)]
279pub struct TabContext<TabId> {
280 pub tab_id: TabId,
281 pub is_drop_target: bool,
283}
284
285#[derive(Clone, Copy, PartialEq)]
287enum HoverTarget<TabId> {
288 Tab(TabId),
289 Edge(Side),
290 Center,
291}
292
293fn toggle_hover<TabId: Copy + PartialEq + 'static>(
295 mut hover: State<Option<HoverTarget<TabId>>>,
296 target: HoverTarget<TabId>,
297 hovering: bool,
298) {
299 if hovering {
300 hover.set(Some(target));
301 } else if hover() == Some(target) {
302 hover.set(None);
303 }
304}
305
306const MIDDLE_FLEX: f32 = 2.0;
308
309#[derive(Clone, PartialEq)]
311struct Renderers<TabId: 'static, PanelId: 'static> {
312 content: Callback<ContentContext<TabId, PanelId>, Element>,
313 tab: Callback<TabContext<TabId>, Element>,
314 drag: Callback<TabId, Element>,
315 bar: Callback<TabBarContext<PanelId>, Element>,
316}
317
318pub struct DockingArea<M: DockingModel> {
319 controller: Writable<M>,
320 renderers: Renderers<M::TabId, M::PanelId>,
321 preview_element: Option<Element>,
322 key: DiffKey,
323}
324
325impl<M: DockingModel> Clone for DockingArea<M> {
326 fn clone(&self) -> Self {
327 Self {
328 controller: self.controller.clone(),
329 renderers: self.renderers.clone(),
330 preview_element: self.preview_element.clone(),
331 key: self.key.clone(),
332 }
333 }
334}
335
336impl<M: DockingModel> PartialEq for DockingArea<M> {
337 fn eq(&self, other: &Self) -> bool {
338 self.controller == other.controller
339 && self.renderers == other.renderers
340 && self.preview_element == other.preview_element
341 && self.key == other.key
342 }
343}
344
345impl<M: DockingModel> KeyExt for DockingArea<M> {
346 fn write_key(&mut self) -> &mut DiffKey {
347 &mut self.key
348 }
349}
350
351impl<M: DockingModel> DockingArea<M> {
352 pub fn new(
353 controller: impl Into<Writable<M>>,
354 render_content: impl Into<Callback<ContentContext<M::TabId, M::PanelId>, Element>>,
355 render_tab: impl Into<Callback<TabContext<M::TabId>, Element>>,
356 render_drag: impl Into<Callback<M::TabId, Element>>,
357 render_tab_bar: impl Into<Callback<TabBarContext<M::PanelId>, Element>>,
358 ) -> Self {
359 Self {
360 controller: controller.into(),
361 renderers: Renderers {
362 content: render_content.into(),
363 tab: render_tab.into(),
364 drag: render_drag.into(),
365 bar: render_tab_bar.into(),
366 },
367 preview_element: None,
368 key: DiffKey::default(),
369 }
370 }
371
372 pub fn preview_element(mut self, element: impl IntoElement) -> Self {
374 self.preview_element = Some(element.into_element());
375 self
376 }
377}
378
379impl<M: DockingModel> Component for DockingArea<M> {
380 fn render(&self) -> impl IntoElement {
381 let controller = self.controller.clone();
382 let renderers = self.renderers.clone();
383 let preview_element = self.preview_element.clone();
384
385 let node = controller.read().root().cloned();
386
387 rect().expanded().map(node, move |element, root| {
388 element.child(render_node(root, controller, renderers, preview_element))
389 })
390 }
391
392 fn render_key(&self) -> DiffKey {
393 self.key.clone().or(self.default_key())
394 }
395}
396
397fn render_node<M: DockingModel>(
399 node: DockNode<M::TabId, M::PanelId>,
400 controller: Writable<M>,
401 renderers: Renderers<M::TabId, M::PanelId>,
402 preview_element: Option<Element>,
403) -> Element {
404 match node {
405 DockNode::Split {
406 direction,
407 children,
408 } => {
409 let share = 100. / children.len().max(1) as f32;
410 ResizableContainer::new()
411 .direction(direction)
412 .panels_iter(children.into_iter().map(|child| {
413 ResizablePanel::new(PanelSize::percent(share))
414 .min_size(5.)
415 .child(render_node(
416 child,
417 controller.clone(),
418 renderers.clone(),
419 preview_element.clone(),
420 ))
421 }))
422 .into_element()
423 }
424 DockNode::Panel(panel) => DockPanelView {
425 panel,
426 controller,
427 renderers,
428 preview_element,
429 key: DiffKey::default(),
430 }
431 .into_element(),
432 }
433}
434
435struct DockPanelView<M: DockingModel> {
436 panel: DockPanel<M::TabId, M::PanelId>,
437 controller: Writable<M>,
438 renderers: Renderers<M::TabId, M::PanelId>,
439 preview_element: Option<Element>,
440 key: DiffKey,
441}
442
443impl<M: DockingModel> Clone for DockPanelView<M> {
444 fn clone(&self) -> Self {
445 Self {
446 panel: self.panel.clone(),
447 controller: self.controller.clone(),
448 renderers: self.renderers.clone(),
449 preview_element: self.preview_element.clone(),
450 key: self.key.clone(),
451 }
452 }
453}
454
455impl<M: DockingModel> PartialEq for DockPanelView<M> {
456 fn eq(&self, _other: &Self) -> bool {
457 false
458 }
459}
460
461impl<M: DockingModel> KeyExt for DockPanelView<M> {
462 fn write_key(&mut self) -> &mut DiffKey {
463 &mut self.key
464 }
465}
466
467impl<M: DockingModel> ComponentOwned for DockPanelView<M> {
468 fn render(self) -> impl IntoElement {
469 let DockPanelView {
470 panel:
471 DockPanel {
472 panel_id,
473 tabs,
474 active_tab_id,
475 },
476 controller,
477 renderers,
478 preview_element,
479 ..
480 } = self;
481
482 let drag = use_drag::<DockDrag<M::DropValue>>();
483 let is_dragging = drag.read().is_some();
484 let hover = use_state(|| None::<HoverTarget<M::TabId>>);
485
486 let hovered = is_dragging.then(&*hover).flatten();
487 let tab_count = tabs.len();
488 let mut tab_children: Vec<Element> = tabs
489 .iter()
490 .enumerate()
491 .map(|(index, &tab_id)| {
492 let handle = renderers.tab.call(TabContext {
493 tab_id,
494 is_drop_target: hovered == Some(HoverTarget::Tab(tab_id)),
495 });
496 let dragger = DragZone::<DockDrag<M::DropValue>>::new(
497 DockDrag::new(M::DropValue::from(tab_id)),
498 handle,
499 )
500 .drag_element(renderers.drag.call(tab_id))
501 .into_element();
502
503 let activatable = rect()
504 .on_press({
505 let mut controller = controller.clone();
506 move |_| {
507 controller.write().set_active(panel_id, tab_id);
508 }
509 })
510 .child(dragger)
511 .into_element();
512
513 DropZone::<DockDrag<M::DropValue>>::new(activatable, {
514 let mut controller = controller.clone();
515 move |payload: DockDrag<M::DropValue>| {
516 controller.write().on_drop(
517 payload.value,
518 DropTarget::Tab {
519 panel_id,
520 position: index,
521 },
522 );
523 }
524 })
525 .on_drag_over(move |hovering| {
526 toggle_hover(hover, HoverTarget::Tab(tab_id), hovering)
527 })
528 .key(tab_id)
529 .into_element()
530 })
531 .collect();
532
533 tab_children.push(
534 DropZone::<DockDrag<M::DropValue>>::new(rect().expanded().into_element(), {
535 let mut controller = controller.clone();
536 move |payload: DockDrag<M::DropValue>| {
537 controller.write().on_drop(
538 payload.value,
539 DropTarget::Tab {
540 panel_id,
541 position: tab_count,
542 },
543 );
544 }
545 })
546 .into_element(),
547 );
548 let tab_bar = renderers.bar.call(TabBarContext {
549 panel_id,
550 tab_children,
551 tab_count,
552 });
553
554 let content = renderers.content.call(ContentContext {
555 panel_id,
556 tab_id: active_tab_id,
557 tab_count,
558 });
559
560 let overlay = is_dragging.then(move || {
561 let ghost = match (hover(), preview_element) {
562 (Some(HoverTarget::Edge(side)), Some(preview)) => {
563 let (width, height) = match side {
564 Side::Top | Side::Bottom => (Size::percent(100.), Size::percent(50.)),
565 Side::Left | Side::Right => (Size::percent(50.), Size::percent(100.)),
566 };
567 let position = match side {
568 Side::Top | Side::Left => Position::new_absolute(),
569 Side::Bottom => Position::new_absolute().bottom(0.),
570 Side::Right => Position::new_absolute().right(0.),
571 };
572 Some(drag_preview(position, width, height, preview))
573 }
574 (Some(HoverTarget::Center), Some(preview)) => Some(drag_preview(
575 Position::new_absolute(),
576 Size::percent(100.),
577 Size::percent(100.),
578 preview,
579 )),
580 _ => None,
581 };
582
583 let edge = |side: Side, width: Size, height: Size| -> Element {
584 rect()
585 .width(width)
586 .height(height)
587 .child(drop_zone_for_side::<M>(
588 panel_id,
589 side,
590 controller.clone(),
591 hover,
592 ))
593 .into_element()
594 };
595
596 let center_drop =
597 DropZone::<DockDrag<M::DropValue>>::new(rect().expanded().into_element(), {
598 let mut controller = controller.clone();
599 move |payload: DockDrag<M::DropValue>| {
600 controller
601 .write()
602 .on_drop(payload.value, DropTarget::Center(panel_id));
603 }
604 })
605 .on_drag_over(move |hovering| toggle_hover(hover, HoverTarget::Center, hovering))
606 .into_element();
607
608 let middle_row = rect()
609 .width(Size::percent(100.))
610 .height(Size::flex(MIDDLE_FLEX))
611 .horizontal()
612 .content(Content::flex())
613 .child(edge(Side::Left, Size::flex(1.), Size::percent(100.)))
614 .child(
615 rect()
616 .width(Size::flex(MIDDLE_FLEX))
617 .height(Size::percent(100.))
618 .child(center_drop),
619 )
620 .child(edge(Side::Right, Size::flex(1.), Size::percent(100.)));
621
622 rect()
623 .position(Position::new_absolute())
624 .layer(Layer::Overlay)
625 .width(Size::percent(100.))
626 .height(Size::percent(100.))
627 .vertical()
628 .content(Content::flex())
629 .maybe_child(ghost)
630 .child(edge(Side::Top, Size::percent(100.), Size::flex(1.)))
631 .child(middle_row)
632 .child(edge(Side::Bottom, Size::percent(100.), Size::flex(1.)))
633 .into_element()
634 });
635
636 rect()
637 .a11y_role(AccessibilityRole::Pane)
638 .expanded()
639 .child(tab_bar)
640 .child(
641 rect()
642 .expanded()
643 .overflow(Overflow::Clip)
644 .child(content)
645 .maybe_child(overlay),
646 )
647 }
648
649 fn render_key(&self) -> DiffKey {
650 self.key.clone().or(self.default_key())
651 }
652}
653
654fn drop_zone_for_side<M: DockingModel>(
656 panel_id: M::PanelId,
657 side: Side,
658 mut controller: Writable<M>,
659 hover: State<Option<HoverTarget<M::TabId>>>,
660) -> Element {
661 DropZone::<DockDrag<M::DropValue>>::new(
662 rect().expanded().into_element(),
663 move |payload: DockDrag<M::DropValue>| {
664 controller
665 .write()
666 .on_drop(payload.value, DropTarget::Split { panel_id, side });
667 },
668 )
669 .on_drag_over(move |hovering| toggle_hover(hover, HoverTarget::Edge(side), hovering))
670 .into_element()
671}
672
673fn drag_preview(position: Position, width: Size, height: Size, preview: Element) -> Element {
674 rect()
675 .position(position)
676 .interactive(false)
677 .width(width)
678 .height(height)
679 .child(preview)
680 .into_element()
681}