Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-app/src/router.rs

//! **`Router` — route → view, with history.** Generalises the deck's
//! `set_active_by_title` + the wasm `#TabName` deep-link into a route stack with
//! back/forward, usable native AND on wasm. A route is an opaque string id
//! (matches a [`NavItem`](crate::NavItem) route / a deck tab title / a path).

use serde::{Deserialize, Serialize};
use serde_json::{Value, json};

/// Active route + back/forward history.
///
/// **Serde-round-trippable** — that is what makes a whole shell's chrome persistable
/// across a restart; before it was, every minimise, ratio and fold died with the
/// process.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Router {
    current: String,
    back: Vec<String>,
    fwd: Vec<String>,
}

impl Router {
    pub fn new(initial: impl Into<String>) -> Self {
        Self { current: initial.into(), back: Vec::new(), fwd: Vec::new() }
    }

    pub fn current(&self) -> &str {
        &self.current
    }

    /// Go to `route`. Pushes the previous route onto the back stack and clears the
    /// forward stack (standard browser semantics). No-op (returns false) if already
    /// on that route.
    pub fn navigate(&mut self, route: impl Into<String>) -> bool {
        let route = route.into();
        if route == self.current {
            return false;
        }
        self.back.push(std::mem::replace(&mut self.current, route));
        self.fwd.clear();
        true
    }

    pub fn can_back(&self) -> bool {
        !self.back.is_empty()
    }
    pub fn can_forward(&self) -> bool {
        !self.fwd.is_empty()
    }

    /// Pop one step back; the current route moves to the forward stack.
    pub fn back(&mut self) -> bool {
        match self.back.pop() {
            Some(prev) => {
                self.fwd.push(std::mem::replace(&mut self.current, prev));
                true
            }
            None => false,
        }
    }

    /// Re-apply one forward step; the current route moves to the back stack.
    pub fn forward(&mut self) -> bool {
        match self.fwd.pop() {
            Some(next) => {
                self.back.push(std::mem::replace(&mut self.current, next));
                true
            }
            None => false,
        }
    }

    /// The depth of the back history (for a "← (n)" affordance).
    pub fn back_depth(&self) -> usize {
        self.back.len()
    }

    /// The current route as a URL hash fragment (`#route`, spaces → `%20`) — the
    /// deep-link the host writes to `window.location.hash` (wasm) or a CLI arg.
    pub fn to_hash(&self) -> String {
        format!("#{}", self.current.replace(' ', "%20"))
    }

    /// Parse a URL hash fragment (`#route`) into a route string, decoding `%20`/`+`
    /// to spaces. Returns `None` for an empty fragment. Pure (native + wasm).
    pub fn route_from_hash(hash: &str) -> Option<String> {
        let r = hash.trim_start_matches('#').replace("%20", " ").replace('+', " ");
        let r = r.trim();
        (!r.is_empty()).then(|| r.to_string())
    }

    /// Apply a hash fragment as a navigation (deep-link restore). Returns true if
    /// it changed the route. Unknown/empty fragments are ignored.
    pub fn sync_from_hash(&mut self, hash: &str) -> bool {
        match Self::route_from_hash(hash) {
            Some(route) => self.navigate(route),
            None => false,
        }
    }

    pub fn state_json(&self) -> Value {
        json!({
            "current": self.current,
            "hash": self.to_hash(),
            "can_back": self.can_back(),
            "can_forward": self.can_forward(),
            "back_depth": self.back.len(),
            "forward_depth": self.fwd.len(),
        })
    }
}