Skip to main content

freya_components/
docking.rs

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/// A tabbed panel containing one or more tabs.
25#[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    /// Create a new panel with the given id and tabs.
34    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    /// Insert `tab_id` at `position`.
47    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    /// Add `tab_id` at the end.
67    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/// A node in the docking tree, either a split or a tabbed panel.
78#[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    /// Whether this node is empty.
93    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    /// Find the panel with the given id.
101    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    /// Mutable version of [`DockNode::panel`].
111    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    /// Find `tab_id` under this node.
121    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    /// Remove `tab_id` from every panel except `except_panel_id`.
135    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    /// Turn the matching panel into a split that holds the old panel and a new one.
157    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    /// Remove empty panels.
187    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        // When only one child remains, replace this split node with that child.
194        if children.len() == 1 {
195            *self = children.remove(0);
196        }
197    }
198}
199
200/// Which side of a panel a drop targets.
201#[derive(Clone, Copy, PartialEq, Eq, Debug)]
202pub enum Side {
203    Top,
204    Bottom,
205    Left,
206    Right,
207}
208
209impl Side {
210    /// The direction a split on this side runs along.
211    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/// Describes where a dragged tab should be dropped.
220#[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    /// Id for a tab.
229    type TabId: Copy + PartialEq + Hash + 'static;
230    /// Id for a panel.
231    type PanelId: Copy + PartialEq + 'static;
232    /// The value carried by a drag-and-drop.
233    type DropValue: Clone + PartialEq + 'static + From<Self::TabId>;
234
235    /// The current tree of panels and splits, or `None` when it's empty.
236    fn root(&self) -> Option<&DockNode<Self::TabId, Self::PanelId>>;
237    /// Apply a dropped [`Self::DropValue`] at `target`. Returns `true` if
238    /// something changed.
239    fn on_drop(&mut self, value: Self::DropValue, target: DropTarget<Self::PanelId>) -> bool;
240    /// Make `tab_id` the active one in `panel_id`. Returns `true` if it was found.
241    fn set_active(&mut self, panel_id: Self::PanelId, tab_id: Self::TabId) -> bool;
242}
243
244/// The payload carried by a drag in a docking area.
245#[derive(Clone, PartialEq, Debug)]
246pub struct DockDrag<Value> {
247    value: Value,
248}
249
250impl<Value> DockDrag<Value> {
251    /// Wrap a value to be dragged onto a docking area.
252    pub fn new(value: Value) -> Self {
253        Self { value }
254    }
255}
256
257/// Passed to the `render_content` callback.
258#[derive(Clone, PartialEq, Debug)]
259pub struct ContentContext<TabId, PanelId> {
260    /// The id of the panel this content belongs to.
261    pub panel_id: PanelId,
262    /// The panel's active tab, or `None` when the panel has no tabs.
263    pub tab_id: Option<TabId>,
264    /// Number of tabs open in the panel.
265    pub tab_count: usize,
266}
267
268/// Passed to the `render_tab_bar` callback.
269pub struct TabBarContext<PanelId> {
270    pub panel_id: PanelId,
271    /// The tab header elements to lay out in the bar.
272    pub tab_children: Vec<Element>,
273    /// Number of tabs open in the panel.
274    pub tab_count: usize,
275}
276
277/// Passed to the `render_tab` callback.
278#[derive(Clone, PartialEq, Debug)]
279pub struct TabContext<TabId> {
280    pub tab_id: TabId,
281    /// True when a drag is going on and the cursor is over this tab.
282    pub is_drop_target: bool,
283}
284
285/// The drop target currently under the cursor during a drag.
286#[derive(Clone, Copy, PartialEq)]
287enum HoverTarget<TabId> {
288    Tab(TabId),
289    Edge(Side),
290    Center,
291}
292
293/// Set `hover` to `target` on enter, or clear it on leave if it still points at `target`.
294fn 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
306/// Make the center take 50% of the width.
307const MIDDLE_FLEX: f32 = 2.0;
308
309/// The render callbacks.
310#[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    /// Preview shown over the drop target while dragging.
373    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
397/// Draw one node of the tree (a split or a panel).
398fn 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
654/// Drop zone for one edge.
655fn 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}