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: subscribers.clone(),
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    pub fn create_global<R: Routable + 'static>(cfg: RouterConfig<R>) -> Self {
101        let subscribers = Rc::new(RefCell::new(FxHashSet::default()));
102
103        let history = if let Some(initial_path) = cfg.initial_path {
104            MemoryHistory::with_initial_path(initial_path)
105        } else {
106            MemoryHistory::default()
107        };
108
109        Self {
110            inner: State::create_global(RouterContextInner {
111                subscribers: subscribers.clone(),
112
113                internal_route: |route| R::from_str(route).is_ok(),
114
115                site_map: R::SITE_MAP,
116
117                history,
118            }),
119        }
120    }
121
122    pub fn try_get() -> Option<Self> {
123        try_consume_context()
124    }
125
126    pub fn get() -> Self {
127        consume_context()
128    }
129
130    /// Check whether there is a previous page to navigate back to.
131    #[must_use]
132    pub fn can_go_back(&self) -> bool {
133        self.inner.peek().history.can_go_back()
134    }
135
136    /// Check whether there is a future page to navigate forward to.
137    #[must_use]
138    pub fn can_go_forward(&self) -> bool {
139        self.inner.peek().history.can_go_forward()
140    }
141
142    /// Go back to the previous location.
143    ///
144    /// Will fail silently if there is no previous location to go to.
145    pub fn go_back(&self) {
146        self.inner.peek().history.go_back();
147        self.change_route();
148    }
149
150    /// Go back to the next location.
151    ///
152    /// Will fail silently if there is no next location to go to.
153    pub fn go_forward(&self) {
154        self.inner.peek().history.go_forward();
155        self.change_route();
156    }
157
158    /// Push a new location.
159    ///
160    /// The previous location will be available to go back to.
161    pub fn push(&self, target: impl Into<NavigationTarget>) -> Option<ExternalNavigationFailure> {
162        let target = target.into();
163        {
164            let mut write = self.inner.write_unchecked();
165            match target {
166                NavigationTarget::Internal(p) => write.history.push(p),
167                NavigationTarget::External(e) => return write.external(e),
168            }
169        }
170
171        self.change_route();
172        None
173    }
174
175    /// Replace the current location.
176    ///
177    /// The previous location will **not** be available to go back to.
178    pub fn replace(
179        &self,
180        target: impl Into<NavigationTarget>,
181    ) -> Option<ExternalNavigationFailure> {
182        let target = target.into();
183        {
184            let mut write = self.inner.write_unchecked();
185            match target {
186                NavigationTarget::Internal(p) => write.history.replace(p),
187                NavigationTarget::External(e) => return write.external(e),
188            }
189        }
190
191        self.change_route();
192        None
193    }
194
195    /// The route that is currently active.
196    pub fn current<R: Routable>(&self) -> R {
197        let absolute_route = self.full_route_string();
198        // If this is a child route, map the absolute route to the child route before parsing
199        let mapping = consume_child_route_mapping::<R>();
200        let route = match mapping.as_ref() {
201            Some(mapping) => mapping
202                .parse_route_from_root_route(&absolute_route)
203                .ok_or_else(|| "Failed to parse route".to_string()),
204            None => {
205                R::from_str(&absolute_route).map_err(|err| format!("Failed to parse route {err}"))
206            }
207        };
208
209        match route {
210            Ok(route) => route,
211            Err(_err) => "/".parse().unwrap_or_else(|err| panic!("{err}")),
212        }
213    }
214
215    /// 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.
216    pub fn full_route_string(&self) -> String {
217        let inner = self.inner.read();
218        inner.subscribe_to_current_context();
219
220        self.inner.peek().history.current_route()
221    }
222
223    /// Get the site map of the router.
224    pub fn site_map(&self) -> &'static [SiteMapSegment] {
225        self.inner.read().site_map
226    }
227
228    fn change_route(&self) {
229        self.inner.read().update_subscribers();
230    }
231
232    pub(crate) fn internal_route(&self, route: &str) -> bool {
233        (self.inner.read().internal_route)(route)
234    }
235}
236
237/// This context is set to the RouterConfig on_update method
238pub struct GenericRouterContext<R> {
239    inner: RouterContext,
240    _marker: std::marker::PhantomData<R>,
241}
242
243impl<R> GenericRouterContext<R>
244where
245    R: Routable,
246{
247    /// Check whether there is a previous page to navigate back to.
248    #[must_use]
249    pub fn can_go_back(&self) -> bool {
250        self.inner.can_go_back()
251    }
252
253    /// Check whether there is a future page to navigate forward to.
254    #[must_use]
255    pub fn can_go_forward(&self) -> bool {
256        self.inner.can_go_forward()
257    }
258
259    /// Go back to the previous location.
260    ///
261    /// Will fail silently if there is no previous location to go to.
262    pub fn go_back(&self) {
263        self.inner.go_back();
264    }
265
266    /// Go back to the next location.
267    ///
268    /// Will fail silently if there is no next location to go to.
269    pub fn go_forward(&self) {
270        self.inner.go_forward();
271    }
272
273    /// Push a new location.
274    ///
275    /// The previous location will be available to go back to.
276    pub fn push(
277        &self,
278        target: impl Into<NavigationTarget<R>>,
279    ) -> Option<ExternalNavigationFailure> {
280        self.inner.push(target.into())
281    }
282
283    /// Replace the current location.
284    ///
285    /// The previous location will **not** be available to go back to.
286    pub fn replace(
287        &self,
288        target: impl Into<NavigationTarget<R>>,
289    ) -> Option<ExternalNavigationFailure> {
290        self.inner.replace(target.into())
291    }
292
293    /// The route that is currently active.
294    pub fn current(&self) -> R
295    where
296        R: Clone,
297    {
298        self.inner.current()
299    }
300}