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