gpui_router/
route.rs

1use crate::{Layout, RouterState};
2use gpui::*;
3use matchit::Router as MatchitRouter;
4use smallvec::SmallVec;
5use std::fmt::{Debug, Display};
6
7type RouteElementFactory = Box<dyn Fn(&mut Window, &mut App) -> AnyElement>;
8
9/// Creates a new [`Route`](crate::Route) element.
10pub fn route() -> impl IntoElement {
11  Route::new()
12}
13
14/// Configures an element to render when a pattern matches the current path.
15/// It must be rendered within a [`Routes`](crate::Routes) element.
16#[derive(IntoElement)]
17pub struct Route {
18  basename: SharedString,
19  path: Option<SharedString>,
20  pub(crate) element: Option<RouteElementFactory>,
21  pub(crate) routes: SmallVec<[Box<Route>; 1]>,
22  pub(crate) layout: Option<Box<dyn Layout>>,
23}
24
25impl Default for Route {
26  fn default() -> Self {
27    Self {
28      basename: SharedString::default(),
29      path: None,
30      element: None,
31      routes: SmallVec::new(),
32      layout: None,
33    }
34  }
35}
36
37impl Debug for Route {
38  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39    f.debug_struct("Route")
40      .field("basename", &self.basename)
41      .field("path", &self.path)
42      .field("layout", &self.layout.is_some())
43      .field("element", &self.element.is_some())
44      .field("routes", &self.routes.len())
45      .finish()
46  }
47}
48
49impl Display for Route {
50  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51    write!(f, "Route")
52  }
53}
54
55impl Route {
56  pub fn new() -> Self {
57    Self::default()
58  }
59
60  pub(crate) fn basename(mut self, basename: impl Into<SharedString>) -> Self {
61    self.basename = basename.into();
62    self
63  }
64
65  /// The path to match against the current location.
66  pub fn path(mut self, path: impl Into<SharedString>) -> Self {
67    self.path = Some(path.into());
68    self
69  }
70
71  /// The element to render when the route matches.
72  /// Accepts a closure that returns an IntoElement, which will be called lazily when the route matches.
73  /// Panics if a layout is already set.
74  ///
75  /// # Examples
76  /// ```
77  /// Route::new().path("home").element(|| HomeView::render())
78  /// Route::new().path("about").element(|| div().child("About"))
79  /// ```
80  pub fn element<F, E>(mut self, element_fn: F) -> Self
81  where
82    F: Fn(&mut Window, &mut App) -> E + 'static,
83    E: IntoElement,
84  {
85    if cfg!(debug_assertions) && self.layout.is_some() {
86      panic!("Route element and layout cannot be set at the same time");
87    }
88
89    self.element = Some(Box::new(move |window, cx| element_fn(window, cx).into_any_element()));
90    self
91  }
92
93  /// The layout to use when the route matches.
94  /// Panics if an element is already set.
95  pub fn layout(mut self, layout: impl Layout + 'static) -> Self {
96    if cfg!(debug_assertions) && self.element.is_some() {
97      panic!("Route element and layout cannot be set at the same time");
98    }
99
100    self.layout = Some(Box::new(layout));
101    self
102  }
103
104  /// Sets the route as an index route.
105  /// Panics if a path is already set.
106  pub fn index(self) -> Self {
107    if cfg!(debug_assertions) && self.path.is_some() {
108      panic!("Route index and path cannot be set at the same time");
109    }
110    self.path("")
111  }
112
113  /// Adds a `Route` as a child to the `Route`.
114  pub fn child(mut self, child: Route) -> Self {
115    self.routes.push(Box::new(child));
116    self
117  }
118
119  /// Adds multiple `Route`s as children to the `Route`.
120  pub fn children(mut self, children: impl IntoIterator<Item = Route>) -> Self {
121    for child in children.into_iter() {
122      self = self.child(child);
123    }
124    self
125  }
126
127  pub(crate) fn build_route_map(&self, basename: &str) -> MatchitRouter<()> {
128    let basename = basename.trim_end_matches('/');
129    let mut router_map = MatchitRouter::new();
130
131    let path = match self.path {
132      Some(ref path) => format!("{}/{}", basename, path),
133      None => basename.to_string(),
134    };
135
136    let path = if path != "/" { path.trim_end_matches('/') } else { &path };
137
138    if self.element.is_some() {
139      router_map.insert(path, ()).unwrap();
140      return router_map;
141    }
142
143    // Recursively build the route map
144    for route in self.routes.iter() {
145      router_map.merge(route.build_route_map(path)).unwrap();
146    }
147
148    router_map
149  }
150
151  pub(crate) fn in_pattern(&self, basename: &str, path: &str) -> bool {
152    self.build_route_map(basename).at(path).is_ok()
153  }
154}
155
156impl RenderOnce for Route {
157  fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
158    if let Some(element_fn) = self.element {
159      return element_fn(window, cx);
160    }
161
162    if let Some(mut layout) = self.layout {
163      let pathname = cx.global::<RouterState>().location.pathname.clone();
164      let basename = self.basename.trim_end_matches('/');
165      let basename = match self.path {
166        Some(ref path) => format!("{}/{}", basename, path),
167        None => basename.to_string(),
168      };
169      let routes = std::mem::take(&mut self.routes);
170      let route = routes.into_iter().find(|route| route.in_pattern(&basename, &pathname));
171      if let Some(route) = route {
172        layout.outlet(route.basename(basename).render(window, cx).into_any_element());
173      }
174      return layout.render_layout(window, cx).into_any_element();
175    }
176    Empty {}.into_any_element()
177  }
178}