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