Skip to main content

tablo_core/
navigation.rs

1//! Sidebar navigation: [`NavigationItem`] and the [`NavTarget`] it points with.
2
3use topcoat::{context::Cx, icon::IconData, view::Attributes};
4
5/// The `href` and runtime-navigation attributes of a link to another panel page.
6pub(crate) fn runtime_link(cx: &Cx, href: &str) -> Attributes {
7    topcoat::runtime::link_attrs(cx, href.to_string(), topcoat::runtime::prefetch_mode(cx))
8}
9
10/// Where a sidebar entry points.
11#[derive(Clone, Default, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum NavTarget {
14    /// No URL yet; the owning Panel resolves it.
15    #[default]
16    Derived,
17    /// An explicit URL.
18    Url(String),
19}
20
21impl NavTarget {
22    /// The URL this target names.
23    pub fn url(&self) -> Option<&str> {
24        match self {
25            Self::Derived => None,
26            Self::Url(url) => Some(url),
27        }
28    }
29}
30
31impl std::fmt::Debug for NavTarget {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            Self::Derived => f.write_str("Derived"),
35            Self::Url(url) => f.debug_tuple("Url").field(url).finish(),
36        }
37    }
38}
39
40/// A sidebar entry: a resource's, from its [`ResourceDef`](super::ResourceDef), or a `Page`'s.
41#[derive(Clone, Debug, Default)]
42pub struct NavigationItem {
43    pub label: String,
44    /// Where this entry points.
45    pub target: NavTarget,
46    /// Sort key for the sidebar.
47    pub order: i32,
48    /// The icon before the label, if any.
49    pub icon: Option<IconData>,
50    /// The labelled sidebar group the entry renders in; `None` renders it with the ungrouped
51    /// entries, above every group.
52    pub group: Option<String>,
53}
54
55impl NavigationItem {
56    /// A sidebar entry at an explicit `url`.
57    pub fn at(label: impl Into<String>, url: impl Into<String>) -> Self {
58        Self {
59            label: label.into(),
60            target: NavTarget::Url(url.into()),
61            order: 0,
62            icon: None,
63            group: None,
64        }
65    }
66
67    /// This entry with `icon` before its label.
68    #[must_use]
69    pub fn icon(mut self, icon: IconData) -> Self {
70        self.icon = Some(icon);
71        self
72    }
73
74    /// This entry in the sidebar group labelled `group`. Groups render in the order of their
75    /// first entry, after the ungrouped entries.
76    #[must_use]
77    pub fn group(mut self, group: impl Into<String>) -> Self {
78        self.group = Some(group.into());
79        self
80    }
81
82    /// Resolve a [`NavTarget::Derived`] entry to `url`.
83    pub(crate) fn resolved(mut self, url: &str) -> Self {
84        if matches!(self.target, NavTarget::Derived) {
85            self.target = NavTarget::Url(url.to_string());
86        }
87        self
88    }
89
90    /// The URL this entry points at.
91    pub fn url(&self) -> Option<&str> {
92        self.target.url()
93    }
94
95    /// The path this entry points at, without the query or fragment it carries.
96    pub(crate) fn path(&self) -> Option<&str> {
97        let url = self.url()?;
98        let path = url.split_once(['?', '#']).map_or(url, |(path, _)| path);
99        (!path.is_empty()).then_some(path)
100    }
101
102    /// Whether this item is current for the given request path, which carries no query or
103    /// fragment: an entry's own are the state it points with, not part of where it points.
104    pub fn is_current_path(&self, current_path: &str) -> bool {
105        let Some(path) = self.path() else {
106            return false;
107        };
108        if current_path == path {
109            return true;
110        }
111        current_path
112            .strip_prefix(path)
113            .is_some_and(|rest| rest.starts_with('/'))
114    }
115}
116
117#[cfg(test)]
118mod tests;