Skip to main content

freya_router/contexts/
router.rs

1use std::{
2    cell::RefCell,
3    error::Error,
4    fmt::Display,
5    rc::Rc,
6};
7
8use freya_core::{
9    integration::FxHashSet,
10    prelude::*,
11};
12
13use crate::{
14    components::child_router::consume_child_route_mapping,
15    memory::MemoryHistory,
16    navigation::NavigationTarget,
17    prelude::SiteMapSegment,
18    routable::Routable,
19    router_cfg::RouterConfig,
20};
21
22/// An error that is thrown when the router fails to parse a route
23#[derive(Debug, Clone)]
24pub struct ParseRouteError {
25    message: String,
26}
27
28impl Error for ParseRouteError {}
29impl Display for ParseRouteError {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        self.message.fmt(f)
32    }
33}
34
35/// An error that can occur when navigating to an external URL.
36#[derive(Debug, Clone)]
37pub struct ExternalNavigationFailure(pub String);
38
39impl Error for ExternalNavigationFailure {}
40impl Display for ExternalNavigationFailure {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        write!(f, "External navigation failed: {}", self.0)
43    }
44}
45
46struct RouterContextInner {
47    subscribers: Rc<RefCell<FxHashSet<ReactiveContext>>>,
48
49    internal_route: fn(&str) -> bool,
50
51    site_map: &'static [SiteMapSegment],
52
53    history: MemoryHistory,
54}
55
56impl RouterContextInner {
57    fn update_subscribers(&self) {
58        for id in self.subscribers.borrow().iter() {
59            id.notify();
60        }
61    }
62
63    fn subscribe_to_current_context(&self) {
64        if let Some(mut rc) = ReactiveContext::try_current() {
65            rc.subscribe(&self.subscribers);
66        }
67    }
68
69    fn external(&mut self, external: String) -> Result<(), ExternalNavigationFailure> {
70        let failure = ExternalNavigationFailure(external);
71
72        self.update_subscribers();
73
74        Err(failure)
75    }
76}
77
78/// A collection of router data that manages all routing functionality.
79#[derive(Clone, Copy)]
80pub struct RouterContext {
81    inner: State<RouterContextInner>,
82}
83
84impl RouterContext {
85    pub(crate) fn create<R: Routable + 'static>(cfg: RouterConfig<R>) -> Self {
86        let subscribers = Rc::new(RefCell::new(FxHashSet::default()));
87
88        let history = if let Some(initial_path) = cfg.initial_path {
89            MemoryHistory::with_initial_path(initial_path)
90        } else {
91            MemoryHistory::default()
92        };
93
94        Self {
95            inner: State::create(RouterContextInner {
96                subscribers,
97
98                internal_route: |route| R::from_str(route).is_ok(),
99
100                site_map: R::SITE_MAP,
101
102                history,
103            }),
104        }
105    }
106
107    /// Create a global [`RouterContext`] that lives for the entire application lifetime.
108    /// This is useful for sharing router state across multiple windows.
109    ///
110    /// This is **not** a hook, do not use it inside components like you would [`use_route`](crate::hooks::use_route).
111    /// You would usually want to call this in your `main` function, not anywhere else.
112    ///
113    /// # Example
114    ///
115    /// ```rust, ignore
116    /// # use freya::prelude::*;
117    /// # use freya::router::*;
118    ///
119    /// fn main() {
120    ///     let router = RouterContext::create_global::<Route>(RouterConfig::default());
121    ///
122    ///     launch(
123    ///         LaunchConfig::new()
124    ///             .with_window(WindowConfig::new_app(MyApp { router })),
125    ///     );
126    /// }
127    /// ```
128    pub fn create_global<R: Routable + 'static>(cfg: RouterConfig<R>) -> Self {
129        let subscribers = Rc::new(RefCell::new(FxHashSet::default()));
130
131        let history = if let Some(initial_path) = cfg.initial_path {
132            MemoryHistory::with_initial_path(initial_path)
133        } else {
134            MemoryHistory::default()
135        };
136
137        Self {
138            inner: State::create_global(RouterContextInner {
139                subscribers,
140
141                internal_route: |route| R::from_str(route).is_ok(),
142
143                site_map: R::SITE_MAP,
144
145                history,
146            }),
147        }
148    }
149
150    pub fn try_get() -> Option<Self> {
151        try_consume_context()
152    }
153
154    pub fn get() -> Self {
155        consume_context()
156    }
157
158    /// Check whether there is a previous page to navigate back to.
159    #[must_use]
160    pub fn can_go_back(&self) -> bool {
161        self.inner.peek().history.can_go_back()
162    }
163
164    /// Check whether there is a future page to navigate forward to.
165    #[must_use]
166    pub fn can_go_forward(&self) -> bool {
167        self.inner.peek().history.can_go_forward()
168    }
169
170    /// Go back to the previous location.
171    ///
172    /// Will fail silently if there is no previous location to go to.
173    pub fn go_back(&self) {
174        self.inner.peek().history.go_back();
175        self.change_route();
176    }
177
178    /// Go back to the next location.
179    ///
180    /// Will fail silently if there is no next location to go to.
181    pub fn go_forward(&self) {
182        self.inner.peek().history.go_forward();
183        self.change_route();
184    }
185
186    /// Push a new location.
187    ///
188    /// The previous location will be available to go back to.
189    pub fn push(
190        &self,
191        target: impl Into<NavigationTarget>,
192    ) -> Result<(), ExternalNavigationFailure> {
193        let target = target.into();
194        {
195            let mut write = self.inner.write_unchecked();
196            match target {
197                NavigationTarget::Internal(p) => write.history.push(p),
198                NavigationTarget::External(e) => return write.external(e),
199            }
200        }
201
202        self.change_route();
203        Ok(())
204    }
205
206    /// Replace the current location.
207    ///
208    /// The previous location will **not** be available to go back to.
209    pub fn replace(
210        &self,
211        target: impl Into<NavigationTarget>,
212    ) -> Result<(), ExternalNavigationFailure> {
213        let target = target.into();
214        {
215            let mut write = self.inner.write_unchecked();
216            match target {
217                NavigationTarget::Internal(p) => write.history.replace(p),
218                NavigationTarget::External(e) => return write.external(e),
219            }
220        }
221
222        self.change_route();
223        Ok(())
224    }
225
226    /// The route that is currently active.
227    pub fn current<R: Routable>(&self) -> R {
228        let absolute_route = self.full_route_string();
229        // If this is a child route, map the absolute route to the child route before parsing
230        let mapping = consume_child_route_mapping::<R>();
231        let route = match mapping.as_ref() {
232            Some(mapping) => mapping
233                .parse_route_from_root_route(&absolute_route)
234                .ok_or_else(|| "Failed to parse route".to_string()),
235            None => {
236                R::from_str(&absolute_route).map_err(|err| format!("Failed to parse route {err}"))
237            }
238        };
239
240        match route {
241            Ok(route) => route,
242            Err(_err) => "/".parse().unwrap_or_else(|err| panic!("{err}")),
243        }
244    }
245
246    /// The full route that is currently active. If this is called from inside a child router, this will always return the parent's view of the route.
247    pub fn full_route_string(&self) -> String {
248        let inner = self.inner.read();
249        inner.subscribe_to_current_context();
250
251        self.inner.peek().history.current_route()
252    }
253
254    /// Get the site map of the router.
255    pub fn site_map(&self) -> &'static [SiteMapSegment] {
256        self.inner.read().site_map
257    }
258
259    fn change_route(&self) {
260        self.inner.read().update_subscribers();
261    }
262
263    pub(crate) fn internal_route(&self, route: &str) -> bool {
264        (self.inner.read().internal_route)(route)
265    }
266}
267
268/// This context is set to the RouterConfig on_update method
269pub struct GenericRouterContext<R> {
270    inner: RouterContext,
271    _marker: std::marker::PhantomData<R>,
272}
273
274impl<R> GenericRouterContext<R>
275where
276    R: Routable,
277{
278    /// Check whether there is a previous page to navigate back to.
279    #[must_use]
280    pub fn can_go_back(&self) -> bool {
281        self.inner.can_go_back()
282    }
283
284    /// Check whether there is a future page to navigate forward to.
285    #[must_use]
286    pub fn can_go_forward(&self) -> bool {
287        self.inner.can_go_forward()
288    }
289
290    /// Go back to the previous location.
291    ///
292    /// Will fail silently if there is no previous location to go to.
293    pub fn go_back(&self) {
294        self.inner.go_back();
295    }
296
297    /// Go back to the next location.
298    ///
299    /// Will fail silently if there is no next location to go to.
300    pub fn go_forward(&self) {
301        self.inner.go_forward();
302    }
303
304    /// Push a new location.
305    ///
306    /// The previous location will be available to go back to.
307    pub fn push(
308        &self,
309        target: impl Into<NavigationTarget<R>>,
310    ) -> Result<(), ExternalNavigationFailure> {
311        self.inner.push(target.into())
312    }
313
314    /// Replace the current location.
315    ///
316    /// The previous location will **not** be available to go back to.
317    pub fn replace(
318        &self,
319        target: impl Into<NavigationTarget<R>>,
320    ) -> Result<(), ExternalNavigationFailure> {
321        self.inner.replace(target.into())
322    }
323
324    /// The route that is currently active.
325    pub fn current(&self) -> R
326    where
327        R: Clone,
328    {
329        self.inner.current()
330    }
331}