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