Skip to main content

nightshade_api/web/
workspace.rs

1//! A tabbed dock whose tabs can be dragged between panes using the pointer-drag system.
2
3use leptos::prelude::*;
4
5use crate::web::pointer_drag::{DragPayload, DragSource, DropZone};
6
7/// A tab in a `TabDock`: a stable `id`, a display `title`, and the id of the
8/// `pane` it currently lives in.
9#[derive(Clone)]
10pub struct DockTab {
11    /// Stable identifier used for selection and drag payloads.
12    pub id: String,
13    /// Text shown on the tab button.
14    pub title: String,
15    /// Id of the pane the tab currently belongs to.
16    pub pane: String,
17}
18
19impl DockTab {
20    /// Creates a tab with the given id, title, and owning pane.
21    pub fn new(id: impl Into<String>, title: impl Into<String>, pane: impl Into<String>) -> Self {
22        Self {
23            id: id.into(),
24            title: title.into(),
25            pane: pane.into(),
26        }
27    }
28}
29
30/// Renders one drop-zone pane per entry in `panes`, each showing its `tabs` as
31/// draggable buttons and the body of the `active` tab via the `render` closure.
32/// Dragging a tab onto another pane moves it there and activates it; empty panes
33/// show a drop hint.
34#[component]
35pub fn TabDock<F>(
36    tabs: RwSignal<Vec<DockTab>>,
37    panes: Vec<String>,
38    active: RwSignal<String>,
39    render: F,
40) -> impl IntoView
41where
42    F: Fn(String) -> AnyView + 'static,
43{
44    let render = StoredValue::new_local(render);
45    view! {
46        <div class="nightshade-tabdock">
47            {panes
48                .into_iter()
49                .map(|pane| {
50                    let pane_key = StoredValue::new(pane.clone());
51                    let tabs_in = move || {
52                        tabs.get()
53                            .into_iter()
54                            .filter(|tab| tab.pane == pane_key.get_value())
55                            .collect::<Vec<_>>()
56                    };
57                    let active_in_pane = move || {
58                        let current = active.get();
59                        let list = tabs_in();
60                        list.iter()
61                            .find(|tab| tab.id == current)
62                            .map(|tab| tab.id.clone())
63                            .or_else(|| list.first().map(|tab| tab.id.clone()))
64                    };
65                    let on_drop = Callback::new(move |payload: DragPayload| {
66                        if payload.kind == "dock-tab" {
67                            let target = pane_key.get_value();
68                            tabs.update(|list| {
69                                if let Some(tab) = list.iter_mut().find(|tab| tab.id == payload.id) {
70                                    tab.pane = target;
71                                }
72                            });
73                            active.set(payload.id);
74                        }
75                    });
76                    view! {
77                        <DropZone
78                            id=format!("pane-{}", pane_key.get_value())
79                            on_drop=on_drop
80                            class="nightshade-tabdock-pane"
81                        >
82                            <div class="nightshade-tabdock-tabs">
83                                {move || {
84                                    tabs_in()
85                                        .into_iter()
86                                        .map(|tab| {
87                                            let id_click = tab.id.clone();
88                                            let id_active = tab.id.clone();
89                                            let is_active = move || active.get() == id_active;
90                                            view! {
91                                                <DragSource
92                                                    kind="dock-tab"
93                                                    id=tab.id.clone()
94                                                    label=tab.title.clone()
95                                                    class="nightshade-tabdock-tab"
96                                                >
97                                                    <button
98                                                        class="nightshade-tabdock-tab-btn"
99                                                        class:active=is_active
100                                                        on:click=move |_| active.set(id_click.clone())
101                                                    >
102                                                        {tab.title}
103                                                    </button>
104                                                </DragSource>
105                                            }
106                                        })
107                                        .collect_view()
108                                }}
109                            </div>
110                            <div class="nightshade-tabdock-body">
111                                {move || match active_in_pane() {
112                                    Some(id) => render.with_value(|render| render(id)),
113                                    None => {
114                                        view! {
115                                            <div class="nightshade-tabdock-empty">"Drop a tab here"</div>
116                                        }
117                                            .into_any()
118                                    }
119                                }}
120                            </div>
121                        </DropZone>
122                    }
123                })
124                .collect_view()}
125        </div>
126    }
127}