nightshade-api 0.51.0

Procedural high level API for the nightshade game engine
Documentation
//! 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>
    }
}