Skip to main content

nightshade_api/web/base/
layout.rs

1use leptos::html;
2use leptos::prelude::*;
3
4/// Full-viewport root container that fills the window and hosts the rest of the
5/// app's layout.
6#[component]
7pub fn AppShell(children: Children) -> impl IntoView {
8    view! { <div class="nightshade-app-shell">{children()}</div> }
9}
10
11/// The axis a [`ResizeHandle`] drags along: `Horizontal` adjusts width,
12/// `Vertical` adjusts height.
13#[derive(Clone, Copy, PartialEq, Eq)]
14pub enum ResizeAxis {
15    Horizontal,
16    Vertical,
17}
18
19/// A draggable divider that writes the pointer's movement along `axis` into the
20/// `value` signal, clamped to `min`/`max` (defaults `120.0`/`2000.0`). Set
21/// `invert` to reverse the drag direction (for panels anchored to the right or
22/// bottom).
23#[component]
24pub fn ResizeHandle(
25    value: RwSignal<f64>,
26    axis: ResizeAxis,
27    #[prop(default = 120.0)] min: f64,
28    #[prop(default = 2000.0)] max: f64,
29    #[prop(optional)] invert: bool,
30) -> impl IntoView {
31    let handle_ref = NodeRef::<html::Div>::new();
32    let drag = StoredValue::new(None::<(f64, f64)>);
33
34    let pointer_position = move |event: &web_sys::PointerEvent| match axis {
35        ResizeAxis::Horizontal => event.client_x() as f64,
36        ResizeAxis::Vertical => event.client_y() as f64,
37    };
38
39    let on_pointerdown = move |event: web_sys::PointerEvent| {
40        event.prevent_default();
41        drag.set_value(Some((pointer_position(&event), value.get_untracked())));
42        if let Some(node) = handle_ref.get() {
43            let _ = node.set_pointer_capture(event.pointer_id());
44        }
45    };
46
47    let on_pointermove = move |event: web_sys::PointerEvent| {
48        if let Some((start_pointer, start_value)) = drag.get_value() {
49            let mut delta = pointer_position(&event) - start_pointer;
50            if invert {
51                delta = -delta;
52            }
53            value.set((start_value + delta).clamp(min, max));
54        }
55    };
56
57    let on_pointerup = move |event: web_sys::PointerEvent| {
58        drag.set_value(None);
59        if let Some(node) = handle_ref.get() {
60            let _ = node.release_pointer_capture(event.pointer_id());
61        }
62    };
63
64    let class = match axis {
65        ResizeAxis::Horizontal => "nightshade-resize-handle horizontal",
66        ResizeAxis::Vertical => "nightshade-resize-handle vertical",
67    };
68
69    view! {
70        <div
71            node_ref=handle_ref
72            class=class
73            on:pointerdown=on_pointerdown
74            on:pointermove=on_pointermove
75            on:pointerup=on_pointerup
76        ></div>
77    }
78}
79
80/// A horizontal flex container. Merge extra classes via `class`.
81#[component]
82pub fn Row(#[prop(into, optional)] class: String, children: Children) -> impl IntoView {
83    view! { <div class=format!("nightshade-row {class}")>{children()}</div> }
84}
85
86/// A vertical flex container. Merge extra classes via `class`.
87#[component]
88pub fn Column(#[prop(into, optional)] class: String, children: Children) -> impl IntoView {
89    view! { <div class=format!("nightshade-column {class}")>{children()}</div> }
90}
91
92/// A CSS grid container. Merge extra classes via `class` to set the template.
93#[component]
94pub fn Grid(#[prop(into, optional)] class: String, children: Children) -> impl IntoView {
95    view! { <div class=format!("nightshade-grid {class}")>{children()}</div> }
96}