nightshade_api/web/
dock.rs1use leptos::html;
4use leptos::prelude::*;
5
6#[derive(Clone, Copy, PartialEq, Eq)]
8pub enum SplitAxis {
9 Row,
11 Column,
13}
14
15#[derive(Clone, Copy, PartialEq, Eq)]
17pub enum DockSide {
18 Start,
20 End,
22}
23
24#[derive(Clone, Copy)]
25struct DockAxis(SplitAxis);
26
27#[component]
30pub fn DockLayout(
31 #[prop(default = SplitAxis::Row)] axis: SplitAxis,
32 #[prop(into, optional)] class: String,
33 children: Children,
34) -> impl IntoView {
35 provide_context(DockAxis(axis));
36 let orientation = match axis {
37 SplitAxis::Row => "row",
38 SplitAxis::Column => "column",
39 };
40 view! {
41 <div class=format!("nightshade-dock-layout {orientation} {class}")>{children()}</div>
42 }
43}
44
45#[component]
47pub fn DockMain(#[prop(into, optional)] class: String, children: Children) -> impl IntoView {
48 view! { <div class=format!("nightshade-dock-main {class}")>{children()}</div> }
49}
50
51#[component]
56pub fn DockPanel(
57 #[prop(into, optional)] title: String,
58 #[prop(default = DockSide::Start)] side: DockSide,
59 size: RwSignal<f64>,
60 #[prop(default = 140.0)] min: f64,
61 #[prop(default = 680.0)] max: f64,
62 #[prop(optional)] collapsible: bool,
63 #[prop(optional)] collapsed: Option<RwSignal<bool>>,
64 children: ChildrenFn,
65) -> impl IntoView {
66 let axis = use_context::<DockAxis>()
67 .map(|context| context.0)
68 .unwrap_or(SplitAxis::Row);
69 let horizontal = axis == SplitAxis::Row;
70 let collapsed = collapsed.unwrap_or_else(|| RwSignal::new(false));
71 let has_title = !title.is_empty();
72
73 let handle_ref = NodeRef::<html::Div>::new();
74 let drag = StoredValue::new(None::<(f64, f64)>);
75 let invert = matches!(side, DockSide::End);
76
77 let pointer_position = move |event: &web_sys::PointerEvent| {
78 if horizontal {
79 event.client_x() as f64
80 } else {
81 event.client_y() as f64
82 }
83 };
84
85 let on_pointerdown = move |event: web_sys::PointerEvent| {
86 event.prevent_default();
87 drag.set_value(Some((pointer_position(&event), size.get_untracked())));
88 if let Some(node) = handle_ref.get() {
89 let _ = node.set_pointer_capture(event.pointer_id());
90 }
91 };
92 let on_pointermove = move |event: web_sys::PointerEvent| {
93 if let Some((start_pointer, start_size)) = drag.get_value() {
94 let mut delta = pointer_position(&event) - start_pointer;
95 if invert {
96 delta = -delta;
97 }
98 size.set((start_size + delta).clamp(min, max));
99 }
100 };
101 let on_pointerup = move |event: web_sys::PointerEvent| {
102 drag.set_value(None);
103 if let Some(node) = handle_ref.get() {
104 let _ = node.release_pointer_capture(event.pointer_id());
105 }
106 };
107
108 let axis_class = if horizontal { "row" } else { "column" };
109 let side_class = match side {
110 DockSide::Start => "start",
111 DockSide::End => "end",
112 };
113
114 let style = move || {
115 if collapsed.get() {
116 "flex:0 0 auto".to_string()
117 } else {
118 format!("flex:0 0 {}px", size.get())
119 }
120 };
121
122 let children = StoredValue::new(children);
123
124 view! {
125 <div
126 class=format!("nightshade-dock-panel {axis_class} {side_class}")
127 class:collapsed=move || collapsed.get()
128 style=style
129 >
130 {(has_title || collapsible)
131 .then(|| {
132 view! {
133 <div class="nightshade-dock-header">
134 <span class="nightshade-dock-title">{title.clone()}</span>
135 {collapsible
136 .then(|| {
137 view! {
138 <button
139 class="nightshade-dock-collapse"
140 aria-label="Toggle panel"
141 aria-expanded=move || (!collapsed.get()).to_string()
142 on:click=move |_| collapsed.update(|value| *value = !*value)
143 >
144 {move || if collapsed.get() { "+" } else { "-" }}
145 </button>
146 }
147 })}
148 </div>
149 }
150 })}
151 <Show when=move || !collapsed.get() fallback=|| ()>
152 <div class="nightshade-dock-body">{children.with_value(|render| render())}</div>
153 </Show>
154 <Show when=move || !collapsed.get() fallback=|| ()>
155 <div
156 node_ref=handle_ref
157 class=format!("nightshade-dock-handle {axis_class} {side_class}")
158 on:pointerdown=on_pointerdown
159 on:pointermove=on_pointermove
160 on:pointerup=on_pointerup
161 ></div>
162 </Show>
163 </div>
164 }
165}