dioxus_router/contexts/
router.rs1use std::{
2 collections::HashSet,
3 error::Error,
4 fmt::Display,
5 sync::{Arc, Mutex},
6};
7
8use dioxus_core::{provide_context, Element, ReactiveContext, ScopeId};
9use dioxus_history::history;
10use dioxus_signals::{CopyValue, ReadableExt, Signal, WritableExt};
11
12use crate::{
13 components::child_router::consume_child_route_mapping, navigation::NavigationTarget,
14 routable::Routable, router_cfg::RouterConfig, SiteMapSegment,
15};
16
17#[derive(Debug, Clone)]
19pub struct ParseRouteError {
20 message: String,
21}
22
23impl Error for ParseRouteError {}
24impl Display for ParseRouteError {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 self.message.fmt(f)
27 }
28}
29
30#[derive(Clone, Copy)]
32struct RootRouterContext(Signal<Option<RouterContext>>);
33
34pub fn root_router() -> Option<RouterContext> {
38 let rt = dioxus_core::Runtime::current();
39
40 if let Some(ctx) = rt.consume_context::<RootRouterContext>(ScopeId::ROOT) {
41 ctx.0.cloned()
42 } else {
43 rt.provide_context(
44 ScopeId::ROOT,
45 RootRouterContext(Signal::new_in_scope(None, ScopeId::ROOT)),
46 );
47 None
48 }
49}
50
51pub(crate) fn provide_router_context(ctx: RouterContext) {
52 if root_router().is_none() {
53 dioxus_core::Runtime::current().provide_context(
54 ScopeId::ROOT,
55 RootRouterContext(Signal::new_in_scope(Some(ctx), ScopeId::ROOT)),
56 );
57 }
58 provide_context(ctx);
59}
60
61#[derive(Debug, Clone)]
63pub struct ExternalNavigationFailure(pub String);
64
65pub(crate) type RoutingCallback<R> =
67 Arc<dyn Fn(GenericRouterContext<R>) -> Option<NavigationTarget<R>>>;
68pub(crate) type AnyRoutingCallback = Arc<dyn Fn(RouterContext) -> Option<NavigationTarget>>;
69
70struct RouterContextInner {
71 unresolved_error: Option<ExternalNavigationFailure>,
72
73 subscribers: Arc<Mutex<HashSet<ReactiveContext>>>,
74 routing_callback: Option<AnyRoutingCallback>,
75
76 failure_external_navigation: fn() -> Element,
77
78 internal_route: fn(&str) -> bool,
79
80 site_map: &'static [SiteMapSegment],
81}
82
83impl RouterContextInner {
84 fn update_subscribers(&self) {
85 for &id in self.subscribers.lock().unwrap().iter() {
86 id.mark_dirty();
87 }
88 }
89
90 fn subscribe_to_current_context(&self) {
91 if let Some(rc) = ReactiveContext::current() {
92 rc.subscribe(self.subscribers.clone());
93 }
94 }
95
96 fn external(&mut self, external: String) -> Option<ExternalNavigationFailure> {
97 match history().external(external.clone()) {
98 true => None,
99 false => {
100 let failure = ExternalNavigationFailure(external);
101 self.unresolved_error = Some(failure.clone());
102
103 self.update_subscribers();
104
105 Some(failure)
106 }
107 }
108 }
109}
110
111#[derive(Clone, Copy)]
113pub struct RouterContext {
114 inner: CopyValue<RouterContextInner>,
115}
116
117impl RouterContext {
118 pub(crate) fn new<R: Routable + 'static>(cfg: RouterConfig<R>) -> Self {
119 let subscribers = Arc::new(Mutex::new(HashSet::new()));
120 let mapping = consume_child_route_mapping();
121
122 let myself = RouterContextInner {
123 unresolved_error: None,
124 subscribers: subscribers.clone(),
125 routing_callback: cfg.on_update.map(|update| {
126 Arc::new(move |ctx| {
127 let ctx = GenericRouterContext {
128 inner: ctx,
129 _marker: std::marker::PhantomData,
130 };
131 update(ctx).map(|t| match t {
132 NavigationTarget::Internal(r) => match mapping.as_ref() {
133 Some(mapping) => {
134 NavigationTarget::Internal(mapping.format_route_as_root_route(r))
135 }
136 None => NavigationTarget::Internal(r.to_string()),
137 },
138 NavigationTarget::External(s) => NavigationTarget::External(s),
139 })
140 }) as Arc<dyn Fn(RouterContext) -> Option<NavigationTarget>>
141 }),
142
143 failure_external_navigation: cfg.failure_external_navigation,
144
145 internal_route: |route| R::from_str(route).is_ok(),
146
147 site_map: R::SITE_MAP,
148 };
149
150 let history = history();
151
152 history.updater(Arc::new(move || {
154 for &rc in subscribers.lock().unwrap().iter() {
155 rc.mark_dirty();
156 }
157 }));
158
159 let myself = Self {
160 inner: CopyValue::new_in_scope(myself, ScopeId::ROOT),
161 };
162
163 let current_route: R = myself.current();
165
166 if current_route.to_string() != history.current_route() {
167 myself.replace(current_route);
168 }
169
170 myself
171 }
172
173 pub(crate) fn include_prevent_default(&self) -> bool {
176 history().include_prevent_default()
177 }
178
179 #[must_use]
181 pub fn can_go_back(&self) -> bool {
182 history().can_go_back()
183 }
184
185 #[must_use]
187 pub fn can_go_forward(&self) -> bool {
188 history().can_go_forward()
189 }
190
191 pub fn go_back(&self) {
195 history().go_back();
196 self.change_route();
197 }
198
199 pub fn go_forward(&self) {
203 history().go_forward();
204 self.change_route();
205 }
206
207 pub(crate) fn push_any(&self, target: NavigationTarget) -> Option<ExternalNavigationFailure> {
208 {
209 let mut write = self.inner.write_unchecked();
210 match target {
211 NavigationTarget::Internal(p) => history().push(p),
212 NavigationTarget::External(e) => return write.external(e),
213 }
214 }
215
216 self.change_route()
217 }
218
219 pub fn push(&self, target: impl Into<NavigationTarget>) -> Option<ExternalNavigationFailure> {
223 let target = target.into();
224 {
225 let mut write = self.inner.write_unchecked();
226 match target {
227 NavigationTarget::Internal(p) => {
228 let history = history();
229 history.push(p)
230 }
231 NavigationTarget::External(e) => return write.external(e),
232 }
233 }
234
235 self.change_route()
236 }
237
238 pub fn replace(
242 &self,
243 target: impl Into<NavigationTarget>,
244 ) -> Option<ExternalNavigationFailure> {
245 let target = target.into();
246 {
247 let mut state = self.inner.write_unchecked();
248 match target {
249 NavigationTarget::Internal(p) => {
250 let history = history();
251 history.replace(p)
252 }
253 NavigationTarget::External(e) => return state.external(e),
254 }
255 }
256
257 self.change_route()
258 }
259
260 pub fn current<R: Routable>(&self) -> R {
262 let absolute_route = self.full_route_string();
263 let mapping = consume_child_route_mapping::<R>();
265 let route = match mapping.as_ref() {
266 Some(mapping) => mapping
267 .parse_route_from_root_route(&absolute_route)
268 .ok_or_else(|| "Failed to parse route".to_string()),
269 None => {
270 R::from_str(&absolute_route).map_err(|err| format!("Failed to parse route {err}"))
271 }
272 };
273
274 match route {
275 Ok(route) => route,
276 Err(err) => {
277 dioxus_core::throw_error(ParseRouteError { message: err });
278 "/".parse().unwrap_or_else(|err| panic!("{err}"))
279 }
280 }
281 }
282
283 pub fn full_route_string(&self) -> String {
285 let inner = self.inner.read();
286 inner.subscribe_to_current_context();
287 let history = history();
288 history.current_route()
289 }
290
291 pub fn prefix(&self) -> Option<String> {
293 let history = history();
294 history.current_prefix()
295 }
296
297 pub fn clear_error(&self) {
299 let mut write_inner = self.inner.write_unchecked();
300 write_inner.unresolved_error = None;
301
302 write_inner.update_subscribers();
303 }
304
305 pub fn site_map(&self) -> &'static [SiteMapSegment] {
307 self.inner.read().site_map
308 }
309
310 pub(crate) fn render_error(&self) -> Option<Element> {
311 let inner_write = self.inner.write_unchecked();
312 inner_write.subscribe_to_current_context();
313 inner_write
314 .unresolved_error
315 .as_ref()
316 .map(|_| (inner_write.failure_external_navigation)())
317 }
318
319 fn change_route(&self) -> Option<ExternalNavigationFailure> {
320 let self_read = self.inner.read();
321 if let Some(callback) = &self_read.routing_callback {
322 let myself = *self;
323 let callback = callback.clone();
324 drop(self_read);
325 if let Some(new) = callback(myself) {
326 let mut self_write = self.inner.write_unchecked();
327 match new {
328 NavigationTarget::Internal(p) => {
329 let history = history();
330 history.replace(p)
331 }
332 NavigationTarget::External(e) => return self_write.external(e),
333 }
334 }
335 }
336
337 self.inner.read().update_subscribers();
338
339 None
340 }
341
342 pub(crate) fn internal_route(&self, route: &str) -> bool {
343 (self.inner.read().internal_route)(route)
344 }
345}
346
347pub struct GenericRouterContext<R> {
349 inner: RouterContext,
350 _marker: std::marker::PhantomData<R>,
351}
352
353impl<R> GenericRouterContext<R>
354where
355 R: Routable,
356{
357 #[must_use]
359 pub fn can_go_back(&self) -> bool {
360 self.inner.can_go_back()
361 }
362
363 #[must_use]
365 pub fn can_go_forward(&self) -> bool {
366 self.inner.can_go_forward()
367 }
368
369 pub fn go_back(&self) {
373 self.inner.go_back();
374 }
375
376 pub fn go_forward(&self) {
380 self.inner.go_forward();
381 }
382
383 pub fn push(
387 &self,
388 target: impl Into<NavigationTarget<R>>,
389 ) -> Option<ExternalNavigationFailure> {
390 self.inner.push(target.into())
391 }
392
393 pub fn replace(
397 &self,
398 target: impl Into<NavigationTarget<R>>,
399 ) -> Option<ExternalNavigationFailure> {
400 self.inner.replace(target.into())
401 }
402
403 pub fn current(&self) -> R
405 where
406 R: Clone,
407 {
408 self.inner.current()
409 }
410
411 pub fn prefix(&self) -> Option<String> {
413 self.inner.prefix()
414 }
415
416 pub fn clear_error(&self) {
418 self.inner.clear_error()
419 }
420}