1use topcoat::{context::Cx, icon::IconData, view::Attributes};
4
5pub(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
10pub(crate) const MENU_LINK_CLASS: &str = "flex w-full items-center gap-2 rounded-md px-2 py-1.5 \
12 text-left text-sm whitespace-nowrap outline-none hover:bg-foreground/5 \
13 focus-visible:bg-foreground/5 active:bg-foreground/10 [&_svg]:size-4 [&_svg]:shrink-0";
14
15#[derive(Clone, Default, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum NavTarget {
19 #[default]
21 Derived,
22 Url(String),
24}
25
26impl NavTarget {
27 pub fn url(&self) -> Option<&str> {
29 match self {
30 Self::Derived => None,
31 Self::Url(url) => Some(url),
32 }
33 }
34}
35
36impl std::fmt::Debug for NavTarget {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 match self {
39 Self::Derived => f.write_str("Derived"),
40 Self::Url(url) => f.debug_tuple("Url").field(url).finish(),
41 }
42 }
43}
44
45#[derive(Clone, Debug, Default)]
47pub struct NavigationItem {
48 pub label: crate::Text,
50 pub target: NavTarget,
52 pub order: i32,
54 pub icon: Option<IconData>,
56 pub group: Option<crate::Text>,
59 pub badge: Option<String>,
61}
62
63impl NavigationItem {
64 pub fn at(label: impl Into<crate::Text>, url: impl Into<String>) -> Self {
70 Self {
71 label: label.into(),
72 target: NavTarget::Url(url.into()),
73 order: 0,
74 icon: None,
75 group: None,
76 badge: None,
77 }
78 }
79
80 #[must_use]
82 pub fn icon(mut self, icon: IconData) -> Self {
83 self.icon = Some(icon);
84 self
85 }
86
87 #[must_use]
90 pub fn group(mut self, group: impl Into<crate::Text>) -> Self {
91 self.group = Some(group.into());
92 self
93 }
94
95 #[must_use]
99 pub fn badge(mut self, text: impl Into<String>) -> Self {
100 let text = text.into().trim().to_string();
101 if !text.is_empty() {
102 self.badge = Some(text);
103 }
104 self
105 }
106
107 pub(crate) fn resolved(mut self, url: &str) -> Self {
109 if matches!(self.target, NavTarget::Derived) {
110 self.target = NavTarget::Url(url.to_string());
111 }
112 self
113 }
114
115 pub fn url(&self) -> Option<&str> {
117 self.target.url()
118 }
119
120 pub(crate) fn path(&self) -> Option<&str> {
122 let url = self.url()?;
123 let path = url.split_once(['?', '#']).map_or(url, |(path, _)| path);
124 (!path.is_empty()).then_some(path)
125 }
126
127 pub fn is_current_path(&self, current_path: &str) -> bool {
130 let Some(path) = self.path() else {
131 return false;
132 };
133 if current_path == path {
134 return true;
135 }
136 current_path
137 .strip_prefix(path)
138 .is_some_and(|rest| rest.starts_with('/'))
139 }
140
141 fn query(&self) -> &str {
143 let url = self.url().unwrap_or_default();
144 let before_fragment = url.split_once('#').map_or(url, |(before, _)| before);
145 before_fragment
146 .split_once('?')
147 .map_or("", |(_, query)| query)
148 }
149
150 fn query_pairs(&self) -> impl Iterator<Item = &str> {
152 self.query().split('&').filter(|pair| !pair.is_empty())
153 }
154
155 pub(crate) fn query_held(&self, request_query: &str) -> bool {
157 self.query_pairs()
158 .all(|pair| request_query.split('&').any(|held| held == pair))
159 }
160}
161
162pub(crate) fn current_index(
166 nav_items: &[NavigationItem],
167 path: &str,
168 query: &str,
169) -> Option<usize> {
170 nav_items
171 .iter()
172 .enumerate()
173 .filter(|(_, item)| item.is_current_path(path))
174 .min_by_key(|(_, item)| {
175 (
176 std::cmp::Reverse(item.path().map_or(0, str::len)),
177 !item.query_held(query),
178 std::cmp::Reverse(item.query_pairs().count()),
179 )
180 })
181 .map(|(index, _)| index)
182}
183
184#[cfg(test)]
185mod tests;