1#[cfg(feature = "cache")]
7use crate::cache::{CacheStats, RouteCache};
8use crate::route::NamedRouteRegistry;
9#[cfg(feature = "transition")]
10use crate::transition::Transition;
11use crate::{IntoRoute, Route, RouteChangeEvent, RouteParams, RouterState};
12use gpui::{App, BorrowAppContext, Global};
13
14pub struct NavigationRequest {
31 pub from: Option<String>,
33
34 pub to: String,
36
37 pub params: RouteParams,
39}
40
41impl NavigationRequest {
42 pub fn new(to: String) -> Self {
44 Self {
45 from: None,
46 to,
47 params: RouteParams::new(),
48 }
49 }
50
51 pub fn with_from(to: String, from: String) -> Self {
53 Self {
54 from: Some(from),
55 to,
56 params: RouteParams::new(),
57 }
58 }
59
60 pub fn with_params(mut self, params: RouteParams) -> Self {
62 self.params = params;
63 self
64 }
65}
66
67impl std::fmt::Debug for NavigationRequest {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 f.debug_struct("NavigationRequest")
70 .field("from", &self.from)
71 .field("to", &self.to)
72 .field("params", &self.params)
73 .finish_non_exhaustive()
74 }
75}
76
77#[derive(Clone)]
83pub struct GlobalRouter {
84 state: RouterState,
85 #[cfg(feature = "cache")]
87 nested_cache: RouteCache,
88 named_routes: NamedRouteRegistry,
90 #[cfg(feature = "transition")]
92 next_transition: Option<Transition>,
93}
94
95impl GlobalRouter {
96 pub fn new() -> Self {
98 Self {
99 state: RouterState::new(),
100 #[cfg(feature = "cache")]
101 nested_cache: RouteCache::new(),
102 named_routes: NamedRouteRegistry::new(),
103 #[cfg(feature = "transition")]
104 next_transition: None,
105 }
106 }
107
108 pub fn add_route(&mut self, route: Route) {
110 if let Some(name) = &route.config.name {
112 self.named_routes
113 .register(name.clone(), route.config.path.clone());
114 }
115
116 self.state.add_route(route);
117 #[cfg(feature = "cache")]
119 self.nested_cache.clear();
120 }
121
122 pub fn push_named(&mut self, name: &str, params: &RouteParams) -> Option<RouteChangeEvent> {
124 let url = self.named_routes.url_for(name, params)?;
125 Some(self.push(url))
126 }
127
128 pub fn url_for(&self, name: &str, params: &RouteParams) -> Option<String> {
130 self.named_routes.url_for(name, params)
131 }
132
133 pub fn push(&mut self, path: String) -> RouteChangeEvent {
135 #[cfg(feature = "cache")]
137 self.nested_cache.clear();
138 self.state.push(path)
139 }
140
141 pub fn replace(&mut self, path: String) -> RouteChangeEvent {
143 #[cfg(feature = "cache")]
145 self.nested_cache.clear();
146 self.state.replace(path)
147 }
148
149 pub fn back(&mut self) -> Option<RouteChangeEvent> {
151 #[cfg(feature = "cache")]
153 self.nested_cache.clear();
154 self.state.back()
155 }
156
157 pub fn forward(&mut self) -> Option<RouteChangeEvent> {
159 #[cfg(feature = "cache")]
161 self.nested_cache.clear();
162 self.state.forward()
163 }
164
165 pub fn current_path(&self) -> &str {
167 self.state.current_path()
168 }
169
170 pub fn current_match(&mut self) -> Option<crate::RouteMatch> {
172 self.state.current_match()
173 }
174
175 pub fn current_match_immutable(&self) -> Option<crate::RouteMatch> {
179 self.state.current_match_immutable()
180 }
181
182 pub fn current_route(&self) -> Option<&std::sync::Arc<crate::route::Route>> {
187 self.state.current_route()
188 }
189
190 pub fn can_go_back(&self) -> bool {
192 self.state.can_go_back()
193 }
194
195 pub fn can_go_forward(&self) -> bool {
197 self.state.can_go_forward()
198 }
199
200 pub fn state_mut(&mut self) -> &mut RouterState {
202 &mut self.state
203 }
204
205 pub fn state(&self) -> &RouterState {
207 &self.state
208 }
209
210 #[cfg(feature = "cache")]
212 pub fn nested_cache_mut(&mut self) -> &mut RouteCache {
213 &mut self.nested_cache
214 }
215
216 #[cfg(feature = "cache")]
218 pub fn cache_stats(&self) -> &CacheStats {
219 self.nested_cache.stats()
220 }
221
222 #[cfg(feature = "transition")]
237 pub fn set_next_transition(&mut self, transition: Transition) {
238 self.next_transition = Some(transition);
239 }
240
241 #[cfg(feature = "transition")]
246 pub fn take_next_transition(&mut self) -> Option<Transition> {
247 self.next_transition.take()
248 }
249
250 #[cfg(feature = "transition")]
252 pub fn has_next_transition(&self) -> bool {
253 self.next_transition.is_some()
254 }
255
256 #[cfg(feature = "transition")]
258 pub fn clear_next_transition(&mut self) {
259 self.next_transition = None;
260 }
261
262 #[cfg(feature = "transition")]
275 pub fn push_with_transition(
276 &mut self,
277 path: String,
278 transition: Transition,
279 ) -> RouteChangeEvent {
280 self.set_next_transition(transition);
281 self.push(path)
282 }
283
284 #[cfg(feature = "transition")]
286 pub fn replace_with_transition(
287 &mut self,
288 path: String,
289 transition: Transition,
290 ) -> RouteChangeEvent {
291 self.set_next_transition(transition);
292 self.replace(path)
293 }
294}
295
296impl Default for GlobalRouter {
297 fn default() -> Self {
298 Self::new()
299 }
300}
301
302impl Global for GlobalRouter {}
303
304pub trait UseRouter {
306 fn router(&self) -> &GlobalRouter;
308
309 fn update_router<F, R>(&mut self, f: F) -> R
311 where
312 F: FnOnce(&mut GlobalRouter, &mut App) -> R;
313}
314
315impl UseRouter for App {
316 fn router(&self) -> &GlobalRouter {
317 self.global::<GlobalRouter>()
318 }
319
320 fn update_router<F, R>(&mut self, f: F) -> R
321 where
322 F: FnOnce(&mut GlobalRouter, &mut App) -> R,
323 {
324 self.update_global(f)
325 }
326}
327
328pub fn init_router<F>(cx: &mut App, configure: F)
345where
346 F: FnOnce(&mut GlobalRouter),
347{
348 let mut router = GlobalRouter::new();
349 configure(&mut router);
350 cx.set_global(router);
351}
352
353pub fn navigate(cx: &mut App, path: impl Into<String>) {
364 cx.update_router(|router, _cx| {
365 router.push(path.into());
366 });
367}
368
369pub fn current_path(cx: &App) -> String {
371 cx.router().current_path().to_string()
372}
373
374pub struct NavigatorHandle<'a, C: BorrowAppContext> {
378 cx: &'a mut C,
379}
380
381impl<C: BorrowAppContext> NavigatorHandle<'_, C> {
382 pub fn push(self, route: impl IntoRoute) -> Self {
397 let descriptor = route.into_route();
398 self.cx.update_global::<GlobalRouter, _>(|router, _| {
399 router.push(descriptor.path);
400 });
401 self
402 }
403
404 pub fn replace(self, route: impl IntoRoute) -> Self {
406 let descriptor = route.into_route();
407 self.cx.update_global::<GlobalRouter, _>(|router, _| {
408 router.replace(descriptor.path);
409 });
410 self
411 }
412
413 pub fn pop(self) -> Self {
415 self.cx.update_global::<GlobalRouter, _>(|router, _| {
416 router.back();
417 });
418 self
419 }
420
421 pub fn forward(self) -> Self {
423 self.cx.update_global::<GlobalRouter, _>(|router, _| {
424 router.forward();
425 });
426 self
427 }
428}
429
430pub struct Navigator;
454
455impl Navigator {
456 pub fn of<C: BorrowAppContext>(cx: &mut C) -> NavigatorHandle<'_, C> {
471 NavigatorHandle { cx }
472 }
473
474 pub fn push(cx: &mut impl BorrowAppContext, route: impl IntoRoute) {
489 let descriptor = route.into_route();
490 cx.update_global::<GlobalRouter, _>(|router, _| {
491 router.push(descriptor.path);
492 });
493 }
494
495 pub fn replace(cx: &mut impl BorrowAppContext, route: impl IntoRoute) {
510 let descriptor = route.into_route();
511 cx.update_global::<GlobalRouter, _>(|router, _| {
512 router.replace(descriptor.path);
513 });
514 }
515
516 pub fn pop(cx: &mut impl BorrowAppContext) {
528 cx.update_global::<GlobalRouter, _>(|router, _| {
529 router.back();
530 });
531 }
532
533 pub fn back(cx: &mut impl BorrowAppContext) {
535 Self::pop(cx);
536 }
537
538 pub fn forward(cx: &mut impl BorrowAppContext) {
540 cx.update_global::<GlobalRouter, _>(|router, _| {
541 router.forward();
542 });
543 }
544
545 pub fn current_path(cx: &App) -> String {
557 cx.global::<GlobalRouter>().current_path().to_string()
558 }
559
560 pub fn can_pop(cx: &App) -> bool {
562 cx.global::<GlobalRouter>().can_go_back()
563 }
564
565 pub fn can_go_back(cx: &App) -> bool {
567 Self::can_pop(cx)
568 }
569
570 pub fn push_named(cx: &mut impl BorrowAppContext, name: &str, params: &RouteParams) {
583 cx.update_global::<GlobalRouter, _>(|router, _| {
584 router.push_named(name, params);
585 });
586 }
587
588 pub fn url_for(cx: &App, name: &str, params: &RouteParams) -> Option<String> {
602 cx.global::<GlobalRouter>().url_for(name, params)
603 }
604
605 pub fn can_go_forward(cx: &App) -> bool {
607 cx.global::<GlobalRouter>().can_go_forward()
608 }
609
610 #[cfg(feature = "transition")]
623 pub fn set_next_transition(cx: &mut impl BorrowAppContext, transition: Transition) {
624 cx.update_global::<GlobalRouter, _>(|router, _| {
625 router.set_next_transition(transition);
626 });
627 }
628
629 #[cfg(feature = "transition")]
638 pub fn push_with_transition(
639 cx: &mut impl BorrowAppContext,
640 route: impl IntoRoute,
641 transition: Transition,
642 ) {
643 let descriptor = route.into_route();
644 cx.update_global::<GlobalRouter, _>(|router, _| {
645 router.push_with_transition(descriptor.path, transition);
646 });
647 }
648
649 #[cfg(feature = "transition")]
658 pub fn replace_with_transition(
659 cx: &mut impl BorrowAppContext,
660 route: impl IntoRoute,
661 transition: Transition,
662 ) {
663 let descriptor = route.into_route();
664 cx.update_global::<GlobalRouter, _>(|router, _| {
665 router.replace_with_transition(descriptor.path, transition);
666 });
667 }
668
669 #[cfg(feature = "transition")]
685 pub fn push_named_with_transition(
686 cx: &mut impl BorrowAppContext,
687 name: &str,
688 params: &RouteParams,
689 transition: Transition,
690 ) {
691 cx.update_global::<GlobalRouter, _>(|router, _| {
692 router.set_next_transition(transition);
693 router.push_named(name, params);
694 });
695 }
696}
697
698#[cfg(test)]
699mod tests {
700 use super::*;
701 use gpui::{IntoElement, TestAppContext};
702
703 #[gpui::test]
704 fn test_nav_push(cx: &mut TestAppContext) {
705 cx.update(|cx| {
707 init_router(cx, |router| {
708 router.add_route(Route::new("/", |_, _cx, _params| {
709 gpui::div().into_any_element()
710 }));
711 router.add_route(Route::new("/users", |_, _cx, _params| {
712 gpui::div().into_any_element()
713 }));
714 router.add_route(Route::new("/users/:id", |_, _cx, _params| {
715 gpui::div().into_any_element()
716 }));
717 });
718 });
719
720 let initial_path = cx.read(Navigator::current_path);
722 assert_eq!(initial_path, "/");
723
724 cx.update(|cx| {
726 Navigator::push(cx, "/users");
727 });
728
729 let current_path = cx.read(Navigator::current_path);
730 assert_eq!(current_path, "/users");
731
732 cx.update(|cx| {
734 Navigator::push(cx, "/users/123");
735 });
736
737 let current_path = cx.read(Navigator::current_path);
738 assert_eq!(current_path, "/users/123");
739 }
740
741 #[gpui::test]
742 fn test_nav_back_forward(cx: &mut TestAppContext) {
743 cx.update(|cx| {
745 init_router(cx, |router| {
746 router.add_route(Route::new("/", |_, _cx, _params| {
747 gpui::div().into_any_element()
748 }));
749 router.add_route(Route::new("/page1", |_, _cx, _params| {
750 gpui::div().into_any_element()
751 }));
752 router.add_route(Route::new("/page2", |_, _cx, _params| {
753 gpui::div().into_any_element()
754 }));
755 });
756 });
757
758 cx.update(|cx| {
760 Navigator::push(cx, "/page1");
761 Navigator::push(cx, "/page2");
762 });
763
764 assert_eq!(cx.read(Navigator::current_path), "/page2");
765 assert!(cx.read(Navigator::can_pop));
766
767 cx.update(|cx| {
769 Navigator::pop(cx);
770 });
771
772 assert_eq!(cx.read(Navigator::current_path), "/page1");
773 assert!(cx.read(Navigator::can_pop));
774 assert!(cx.read(Navigator::can_go_forward));
775
776 cx.update(|cx| {
778 Navigator::forward(cx);
779 });
780
781 assert_eq!(cx.read(Navigator::current_path), "/page2");
782 assert!(!cx.read(Navigator::can_go_forward));
783 }
784
785 #[gpui::test]
786 fn test_nav_replace(cx: &mut TestAppContext) {
787 cx.update(|cx| {
789 init_router(cx, |router| {
790 router.add_route(Route::new("/", |_, _cx, _params| {
791 gpui::div().into_any_element()
792 }));
793 router.add_route(Route::new("/login", |_, _cx, _params| {
794 gpui::div().into_any_element()
795 }));
796 router.add_route(Route::new("/home", |_, _cx, _params| {
797 gpui::div().into_any_element()
798 }));
799 });
800 });
801
802 cx.update(|cx| {
804 Navigator::push(cx, "/login");
805 Navigator::replace(cx, "/home");
806 });
807
808 assert_eq!(cx.read(Navigator::current_path), "/home");
809
810 cx.update(|cx| {
812 Navigator::pop(cx);
813 });
814
815 assert_eq!(cx.read(Navigator::current_path), "/");
816 }
817
818 #[gpui::test]
819 fn test_nav_can_go_back_boundaries(cx: &mut TestAppContext) {
820 cx.update(|cx| {
822 init_router(cx, |router| {
823 router.add_route(Route::new("/", |_, _cx, _params| {
824 gpui::div().into_any_element()
825 }));
826 });
827 });
828
829 assert!(!cx.read(Navigator::can_pop));
831
832 cx.update(|cx| {
834 Navigator::push(cx, "/page1");
835 });
836
837 assert!(cx.read(Navigator::can_pop));
838
839 cx.update(|cx| {
841 Navigator::pop(cx);
842 });
843
844 assert!(!cx.read(Navigator::can_pop));
845 }
846
847 #[gpui::test]
848 fn test_nav_multiple_pushes(cx: &mut TestAppContext) {
849 cx.update(|cx| {
851 init_router(cx, |router| {
852 router.add_route(Route::new("/", |_, _cx, _params| {
853 gpui::div().into_any_element()
854 }));
855 router.add_route(Route::new("/step1", |_, _cx, _params| {
856 gpui::div().into_any_element()
857 }));
858 router.add_route(Route::new("/step2", |_, _cx, _params| {
859 gpui::div().into_any_element()
860 }));
861 router.add_route(Route::new("/step3", |_, _cx, _params| {
862 gpui::div().into_any_element()
863 }));
864 });
865 });
866
867 cx.update(|cx| {
869 Navigator::push(cx, "/step1");
870 Navigator::push(cx, "/step2");
871 Navigator::push(cx, "/step3");
872 });
873
874 assert_eq!(cx.read(Navigator::current_path), "/step3");
875
876 cx.update(|cx| {
878 Navigator::pop(cx);
879 });
880 assert_eq!(cx.read(Navigator::current_path), "/step2");
881
882 cx.update(|cx| {
883 Navigator::pop(cx);
884 });
885 assert_eq!(cx.read(Navigator::current_path), "/step1");
886
887 cx.update(|cx| {
888 Navigator::pop(cx);
889 });
890 assert_eq!(cx.read(Navigator::current_path), "/");
891 }
892
893 #[gpui::test]
894 fn test_nav_with_route_parameters(cx: &mut TestAppContext) {
895 cx.update(|cx| {
897 init_router(cx, |router| {
898 router.add_route(Route::new("/", |_, _cx, _params| {
899 gpui::div().into_any_element()
900 }));
901 router.add_route(Route::new("/users/:id", |_, _cx, _params| {
902 gpui::div().into_any_element()
903 }));
904 router.add_route(Route::new(
905 "/posts/:id/comments/:commentId",
906 |_, _cx, _params| gpui::div().into_any_element(),
907 ));
908 });
909 });
910
911 cx.update(|cx| {
913 Navigator::push(cx, "/users/42");
914 });
915
916 assert_eq!(cx.read(Navigator::current_path), "/users/42");
917
918 cx.update(|cx| {
919 Navigator::push(cx, "/posts/123/comments/456");
920 });
921
922 assert_eq!(cx.read(Navigator::current_path), "/posts/123/comments/456");
923 }
924
925 #[gpui::test]
926 fn test_navigator_api_style(cx: &mut TestAppContext) {
927 cx.update(|cx| {
929 init_router(cx, |router| {
930 router.add_route(Route::new("/", |_, _cx, _params| {
931 gpui::div().into_any_element()
932 }));
933 router.add_route(Route::new("/home", |_, _cx, _params| {
934 gpui::div().into_any_element()
935 }));
936 router.add_route(Route::new("/profile", |_, _cx, _params| {
937 gpui::div().into_any_element()
938 }));
939 });
940 });
941
942 cx.update(|cx| {
944 Navigator::of(cx).push("/home");
945 });
946
947 assert_eq!(cx.read(Navigator::current_path), "/home");
948
949 cx.update(|cx| {
951 Navigator::of(cx).push("/profile").pop();
952 });
953
954 assert_eq!(cx.read(Navigator::current_path), "/home");
955
956 cx.update(|cx| {
958 Navigator::of(cx).replace("/profile");
959 });
960
961 assert_eq!(cx.read(Navigator::current_path), "/profile");
962
963 assert!(cx.read(Navigator::can_pop));
965
966 cx.update(|cx| {
968 Navigator::of(cx).pop();
969 });
970
971 assert_eq!(cx.read(Navigator::current_path), "/");
972
973 assert!(!cx.read(Navigator::can_pop));
975 }
976
977 #[gpui::test]
978 fn test_material_route_with_params(cx: &mut TestAppContext) {
979 use crate::PageRoute;
980
981 cx.update(|cx| {
983 init_router(cx, |router| {
984 router.add_route(Route::new("/", |_, _cx, _params| {
985 gpui::div().into_any_element()
986 }));
987 router.add_route(Route::new("/users/:id", |_, _cx, _params| {
988 gpui::div().into_any_element()
989 }));
990 });
991 });
992
993 cx.update(|cx| {
995 Navigator::push(
996 cx,
997 PageRoute::builder("/users/:id", |_, _cx, _params| {
998 gpui::div().into_any_element()
999 })
1000 .with_param("id", "123"),
1001 );
1002 });
1003
1004 assert_eq!(cx.read(Navigator::current_path), "/users/:id");
1005
1006 cx.update(|cx| {
1008 Navigator::of(cx).push(
1009 PageRoute::builder("/users/:id", |_, _cx, _params| {
1010 gpui::div().into_any_element()
1011 })
1012 .with_param("id", "456"),
1013 );
1014 });
1015
1016 assert_eq!(cx.read(Navigator::current_path), "/users/:id");
1017 }
1018
1019 #[gpui::test]
1020 fn test_string_into_route(cx: &mut TestAppContext) {
1021 cx.update(|cx| {
1023 init_router(cx, |router| {
1024 router.add_route(Route::new("/", |_, _cx, _params| {
1025 gpui::div().into_any_element()
1026 }));
1027 router.add_route(Route::new("/home", |_, _cx, _params| {
1028 gpui::div().into_any_element()
1029 }));
1030 });
1031 });
1032
1033 cx.update(|cx| {
1035 Navigator::push(cx, "/home");
1036 });
1037
1038 assert_eq!(cx.read(Navigator::current_path), "/home");
1039
1040 cx.update(|cx| {
1042 let path = "/home";
1043 Navigator::push(cx, path);
1044 });
1045
1046 assert_eq!(cx.read(Navigator::current_path), "/home");
1047
1048 cx.update(|cx| {
1050 Navigator::push(cx, String::from("/home"));
1051 });
1052
1053 assert_eq!(cx.read(Navigator::current_path), "/home");
1054 }
1055
1056 #[gpui::test]
1057 fn test_both_api_styles(cx: &mut TestAppContext) {
1058 cx.update(|cx| {
1060 init_router(cx, |router| {
1061 router.add_route(Route::new("/", |_, _cx, _params| {
1062 gpui::div().into_any_element()
1063 }));
1064 router.add_route(Route::new("/page1", |_, _cx, _params| {
1065 gpui::div().into_any_element()
1066 }));
1067 router.add_route(Route::new("/page2", |_, _cx, _params| {
1068 gpui::div().into_any_element()
1069 }));
1070 });
1071 });
1072
1073 cx.update(|cx| {
1075 Navigator::push(cx, "/page1");
1076 });
1077 assert_eq!(cx.read(Navigator::current_path), "/page1");
1078
1079 cx.update(|cx| {
1081 Navigator::of(cx).push("/page2");
1082 });
1083 assert_eq!(cx.read(Navigator::current_path), "/page2");
1084
1085 cx.update(|cx| {
1087 Navigator::pop(cx); });
1089 assert_eq!(cx.read(Navigator::current_path), "/page1");
1090
1091 cx.update(|cx| {
1092 Navigator::of(cx).pop(); });
1094 assert_eq!(cx.read(Navigator::current_path), "/");
1095 }
1096}