1use crate::context::GlobalRouter;
8use crate::nested::resolve_child_route;
9#[cfg(feature = "transition")]
10use crate::transition::{SlideDirection, Transition};
11use crate::{debug_log, error_log, trace_log, warn_log};
12use gpui::{div, AnyElement, App, Div, IntoElement, ParentElement, SharedString, Styled, Window};
13
14#[cfg(feature = "transition")]
15use gpui::{relative, Animation, AnimationExt};
16
17#[cfg(feature = "transition")]
18use gpui::prelude::FluentBuilder;
19
20#[cfg(feature = "transition")]
21use std::time::Duration;
22
23#[derive(Clone)]
51pub struct RouterOutlet {
52 name: Option<String>,
55}
56
57impl RouterOutlet {
58 pub fn new() -> Self {
60 Self { name: None }
61 }
62
63 pub fn named(name: impl Into<String>) -> Self {
82 Self {
83 name: Some(name.into()),
84 }
85 }
86}
87
88impl Default for RouterOutlet {
89 fn default() -> Self {
90 Self::new()
91 }
92}
93
94use gpui::{Context, Render};
95
96#[derive(Clone)]
98struct OutletState {
99 current_path: String,
100 animation_counter: u32,
101 current_params: crate::RouteParams,
103 current_builder: Option<crate::route::RouteBuilder>,
104 #[cfg(feature = "transition")]
105 current_transition: crate::transition::Transition,
106 previous_route: Option<PreviousRoute>,
108}
109
110#[derive(Clone)]
111struct PreviousRoute {
112 path: String,
113 params: crate::RouteParams,
114 builder: Option<crate::route::RouteBuilder>,
115}
116
117impl Default for OutletState {
118 fn default() -> Self {
119 Self {
120 current_path: String::new(),
121 animation_counter: 0,
122 current_params: crate::RouteParams::new(),
123 current_builder: None,
124 #[cfg(feature = "transition")]
125 current_transition: crate::transition::Transition::None,
126 previous_route: None,
127 }
128 }
129}
130
131impl Render for RouterOutlet {
132 fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
133 trace_log!("🔄 RouterOutlet::render() called");
134
135 let state_key = SharedString::from(format!("outlet_{:?}", self.name));
137 let state = window.use_keyed_state(state_key.clone(), cx, |_, _| OutletState::default());
138
139 let (prev_path, animation_counter) = {
140 let guard = state.read(cx);
141 (guard.current_path.clone(), guard.animation_counter)
142 };
143
144 #[cfg(feature = "transition")]
146 let (router_path, route_params, route_transition, builder_opt) = cx
147 .try_global::<crate::context::GlobalRouter>()
148 .map(|router| {
149 let path = router.current_path().to_string();
150
151 let params = router
152 .current_match_immutable()
153 .map(|m| {
154 let mut rp = crate::RouteParams::new();
155 for (k, v) in m.params {
156 rp.insert(k, v);
157 }
158 rp
159 })
160 .unwrap_or_else(crate::RouteParams::new);
161
162 let transition = router
163 .current_route()
164 .map(|route| route.transition.default.clone())
165 .unwrap_or(Transition::None);
166
167 let builder = router
168 .current_route()
169 .and_then(|route| route.builder.clone());
170
171 (path, params, transition, builder)
172 })
173 .unwrap_or_else(|| {
174 (
175 "/".to_string(),
176 crate::RouteParams::new(),
177 Transition::None,
178 None,
179 )
180 });
181
182 #[cfg(not(feature = "transition"))]
183 let (router_path, route_params, builder_opt) = cx
184 .try_global::<crate::context::GlobalRouter>()
185 .map(|router| {
186 let path = router.current_path().to_string();
187
188 let params = router
189 .current_match_immutable()
190 .map(|m| {
191 let mut rp = crate::RouteParams::new();
192 for (k, v) in m.params {
193 rp.insert(k, v);
194 }
195 rp
196 })
197 .unwrap_or_else(crate::RouteParams::new);
198
199 let builder = router
200 .current_route()
201 .and_then(|route| route.builder.clone());
202
203 (path, params, builder)
204 })
205 .unwrap_or_else(|| ("/".to_string(), crate::RouteParams::new(), None));
206
207 let path_changed = router_path != prev_path;
209
210 #[cfg_attr(not(feature = "transition"), allow(unused_variables))]
212 let animation_counter = if path_changed {
213 #[cfg_attr(not(feature = "transition"), allow(unused_variables))]
214 let is_initial = prev_path.is_empty();
215
216 #[cfg(feature = "transition")]
217 let new_counter = if is_initial {
218 debug_log!("Initial route: '{}', no animation", router_path);
219 animation_counter
220 } else {
221 let counter = animation_counter.wrapping_add(1);
222 debug_log!(
223 "Route changed: '{}' -> '{}', transition={:?}, animation_counter={}",
224 prev_path,
225 router_path,
226 route_transition,
227 counter
228 );
229 counter
230 };
231
232 #[cfg(not(feature = "transition"))]
233 let new_counter = {
234 debug_log!(
235 "Route changed: '{}' -> '{}' (no transition)",
236 prev_path,
237 router_path
238 );
239 animation_counter
240 };
241
242 state.update(cx, |s, _| {
244 if !is_initial {
248 s.previous_route = Some(PreviousRoute {
249 path: s.current_path.clone(),
250 params: s.current_params.clone(),
251 builder: s.current_builder.clone(),
252 });
253 } else {
254 s.previous_route = None;
256 }
257 s.current_path = router_path.clone();
259 s.current_params = route_params.clone();
260 s.current_builder = builder_opt.clone();
261 #[cfg(feature = "transition")]
262 {
263 s.current_transition = route_transition.clone();
264 }
265 s.animation_counter = new_counter;
266 });
267
268 new_counter
269 } else {
270 trace_log!("Route unchanged: '{}'", router_path);
271 animation_counter
272 };
273
274 #[cfg(feature = "transition")]
275 {
276 let duration_ms = match &route_transition {
279 Transition::Fade { duration_ms, .. } => *duration_ms,
280 Transition::Slide { duration_ms, .. } => *duration_ms,
281 Transition::None => 0,
282 };
283
284 debug_log!(
285 "Rendering route '{}' with animation_counter={}, duration={}ms",
286 router_path,
287 animation_counter,
288 duration_ms
289 );
290
291 let previous_route = state
295 .read(cx)
296 .previous_route
297 .as_ref()
298 .filter(|prev| prev.path != router_path)
299 .cloned();
300
301 debug_log!(
302 "Previous route exists: {}, path: {:?}",
303 previous_route.is_some(),
304 previous_route.as_ref().map(|p| &p.path)
305 );
306
307 let old_content_opt = previous_route.map(|prev| {
309 if let Some(builder) = prev.builder.as_ref() {
310 builder(window, cx, &prev.params)
311 } else {
312 not_found_page().into_any_element()
313 }
314 });
315
316 let new_content = if let Some(builder) = builder_opt.as_ref() {
317 builder(window, cx, &route_params)
318 } else {
319 not_found_page().into_any_element()
320 };
321
322 match &route_transition {
325 Transition::Slide { direction, .. } => {
326 let animation_id = SharedString::from(format!(
328 "outlet_slide_{:?}_{}",
329 self.name, animation_counter
330 ));
331
332 match direction {
333 SlideDirection::Left | SlideDirection::Right => {
334 let is_left = matches!(direction, SlideDirection::Left);
336
337 div()
338 .relative()
339 .w_full()
340 .h_full()
341 .overflow_hidden()
342 .when_some(old_content_opt, |container, old| {
344 container.child(
345 div()
346 .absolute()
347 .w_full()
348 .h_full()
349 .child(old)
350 .left(relative(0.0)) .with_animation(
352 animation_id.clone(),
353 Animation::new(Duration::from_millis(duration_ms)),
354 move |this, delta| {
355 let progress = delta.clamp(0.0, 1.0);
356 let offset = if is_left {
358 -progress } else {
360 progress };
362 this.left(relative(offset))
363 },
364 ),
365 )
366 })
367 .child(
369 div()
370 .absolute()
371 .w_full()
372 .h_full()
373 .child(new_content)
374 .left(relative(if is_left { 1.0 } else { -1.0 })) .with_animation(
376 animation_id.clone(),
377 Animation::new(Duration::from_millis(duration_ms)),
378 move |this, delta| {
379 let progress = delta.clamp(0.0, 1.0);
380 let start = if is_left { 1.0 } else { -1.0 };
382 let offset = start * (1.0 - progress);
383 this.left(relative(offset))
384 },
385 ),
386 )
387 .into_any_element()
388 }
389 SlideDirection::Up | SlideDirection::Down => {
390 let is_up = matches!(direction, SlideDirection::Up);
392
393 div()
394 .relative()
395 .w_full()
396 .h_full()
397 .overflow_hidden()
398 .when_some(old_content_opt, |container, old| {
400 container.child(
401 div()
402 .absolute()
403 .w_full()
404 .h_full()
405 .child(old)
406 .top(relative(0.0)) .with_animation(
408 animation_id.clone(),
409 Animation::new(Duration::from_millis(duration_ms)),
410 move |this, delta| {
411 let progress = delta.clamp(0.0, 1.0);
412 let offset = if is_up {
414 -progress } else {
416 progress };
418 this.top(relative(offset))
419 },
420 ),
421 )
422 })
423 .child(
425 div()
426 .absolute()
427 .w_full()
428 .h_full()
429 .child(new_content)
430 .top(relative(if is_up { 1.0 } else { -1.0 })) .with_animation(
432 animation_id.clone(),
433 Animation::new(Duration::from_millis(duration_ms)),
434 move |this, delta| {
435 let progress = delta.clamp(0.0, 1.0);
436 let start = if is_up { 1.0 } else { -1.0 };
438 let offset = start * (1.0 - progress);
439 this.top(relative(offset))
440 },
441 ),
442 )
443 .into_any_element()
444 }
445 }
446 }
447 Transition::Fade { .. } => {
448 div()
449 .relative()
450 .w_full()
451 .h_full()
452 .overflow_hidden()
453 .when_some(old_content_opt, |container, old| {
455 container.child(
456 div()
457 .absolute()
458 .w_full()
459 .h_full()
460 .child(old)
461 .with_animation(
462 SharedString::from(format!(
463 "outlet_fade_exit_{:?}_{}",
464 self.name, animation_counter
465 )),
466 Animation::new(Duration::from_millis(duration_ms)),
467 |this, delta| {
468 let progress = delta.clamp(0.0, 1.0);
469 this.opacity(1.0 - progress)
470 },
471 ),
472 )
473 })
474 .child(
476 div()
477 .absolute()
478 .w_full()
479 .h_full()
480 .child(new_content)
481 .opacity(0.0)
482 .with_animation(
483 SharedString::from(format!(
484 "outlet_fade_enter_{:?}_{}",
485 self.name, animation_counter
486 )),
487 Animation::new(Duration::from_millis(duration_ms)),
488 |this, delta| {
489 let progress = delta.clamp(0.0, 1.0);
490 this.opacity(progress)
491 },
492 ),
493 )
494 .into_any_element()
495 }
496 _ => {
497 div()
499 .relative()
500 .w_full()
501 .h_full()
502 .child(new_content)
503 .into_any_element()
504 }
505 }
506 }
507
508 #[cfg(not(feature = "transition"))]
509 {
510 let animation_id = SharedString::from(format!("outlet_{:?}", self.name));
512 build_animated_route_content(
513 cx,
514 window,
515 builder_opt.as_ref(),
516 &route_params,
517 animation_id,
518 &(),
519 0,
520 )
521 }
522 }
523}
524
525#[deprecated(since = "0.1.3", note = "Use RouterOutlet entity instead")]
548pub fn router_outlet(window: &mut Window, cx: &mut App) -> impl IntoElement {
549 render_router_outlet(window, cx, None)
550}
551
552#[deprecated(since = "0.1.3", note = "Use RouterOutlet::named() entity instead")]
577pub fn router_outlet_named(
578 window: &mut Window,
579 cx: &mut App,
580 name: impl Into<String>,
581) -> impl IntoElement {
582 render_router_outlet(window, cx, Some(&name.into()))
583}
584
585pub fn render_router_outlet(window: &mut Window, cx: &mut App, name: Option<&str>) -> AnyElement {
604 trace_log!("render_router_outlet called with name: {:?}", name);
605
606 let router = cx.try_global::<GlobalRouter>();
608
609 let Some(router) = router else {
610 error_log!("No global router found - call init_router() first");
611 return div()
612 .child("RouterOutlet: No global router found. Call init_router() first.")
613 .into_any_element();
614 };
615
616 let current_path = router.current_path();
617 trace_log!("Current path: '{}'", current_path);
618
619 let parent_route = find_parent_route_for_path(router.state().routes(), current_path);
622
623 let Some(parent_route) = parent_route else {
624 warn_log!(
625 "No parent route with children found for path '{}'",
626 current_path
627 );
628 return div()
629 .child(format!(
630 "RouterOutlet: No parent route with children found for path '{}'",
631 current_path
632 ))
633 .into_any_element();
634 };
635
636 trace_log!(
637 "Found parent route: '{}' with {} children",
638 parent_route.config.path,
639 parent_route.get_children().len()
640 );
641
642 if parent_route.get_children().is_empty() {
644 return div()
645 .child(format!(
646 "RouterOutlet: Route '{}' has no child routes",
647 parent_route.config.path
648 ))
649 .into_any_element();
650 }
651
652 let route_params = crate::RouteParams::new();
655
656 let resolved = resolve_child_route(parent_route, current_path, &route_params, name);
657
658 let Some((child_route, child_params)) = resolved else {
659 warn_log!("No child route matched for path '{}'", current_path);
660 return div()
661 .child(format!(
662 "RouterOutlet: No child route matched for path '{}'",
663 current_path
664 ))
665 .into_any_element();
666 };
667
668 trace_log!("Matched child route: '{}'", child_route.config.path);
669
670 if let Some(builder) = &child_route.builder {
672 builder(window, cx, &child_params)
674 } else {
675 div()
676 .child(format!(
677 "RouterOutlet: Child route '{}' has no builder",
678 child_route.config.path
679 ))
680 .into_any_element()
681 }
682}
683
684fn find_parent_route_for_path<'a>(
716 routes: &'a [std::sync::Arc<crate::route::Route>],
717 current_path: &str,
718) -> Option<&'a std::sync::Arc<crate::route::Route>> {
719 find_parent_route_internal(routes, current_path, "")
720}
721
722fn find_parent_route_internal<'a>(
723 routes: &'a [std::sync::Arc<crate::route::Route>],
724 current_path: &str,
725 accumulated_path: &str,
726) -> Option<&'a std::sync::Arc<crate::route::Route>> {
727 let current_normalized = current_path.trim_start_matches('/').trim_end_matches('/');
728
729 for route in routes {
730 if route.get_children().is_empty() {
732 continue;
733 }
734
735 let route_segment = route
736 .config
737 .path
738 .trim_start_matches('/')
739 .trim_end_matches('/');
740
741 let full_route_path = if accumulated_path.is_empty() {
743 if route_segment.is_empty() || route_segment == "/" {
744 String::new()
745 } else {
746 route_segment.to_string()
747 }
748 } else if route_segment.is_empty() || route_segment == "/" {
749 accumulated_path.to_string()
750 } else {
751 format!("{}/{}", accumulated_path, route_segment)
752 };
753
754 let is_under = if full_route_path.is_empty() {
756 !current_normalized.is_empty()
757 } else {
758 current_normalized.starts_with(&full_route_path)
759 && (current_normalized.len() == full_route_path.len()
760 || current_normalized[full_route_path.len()..].starts_with('/'))
761 };
762
763 if is_under {
764 if let Some(deeper) =
766 find_parent_route_internal(route.get_children(), current_path, &full_route_path)
767 {
768 return Some(deeper);
769 }
770
771 for child in route.get_children() {
774 let child_segment = child
775 .config
776 .path
777 .trim_start_matches('/')
778 .trim_end_matches('/');
779 let child_full_path = if full_route_path.is_empty() {
780 child_segment.to_string()
781 } else {
782 format!("{}/{}", full_route_path, child_segment)
783 };
784
785 if current_normalized == child_full_path
787 || current_normalized.starts_with(&format!("{}/", child_full_path))
788 {
789 return Some(route);
790 }
791 }
792
793 if current_normalized == full_route_path && accumulated_path.is_empty() {
797 return Some(route);
798 }
799 }
800 }
801
802 None
803}
804
805use crate::Navigator;
813use gpui::*;
814
815pub struct RouterLink {
827 path: SharedString,
829 active_class: Option<Box<dyn Fn(Div) -> Div>>,
831 children: Vec<AnyElement>,
833}
834
835impl RouterLink {
836 pub fn new(path: impl Into<SharedString>) -> Self {
838 Self {
839 path: path.into(),
840 active_class: None,
841 children: Vec::new(),
842 }
843 }
844
845 pub fn child(mut self, child: impl IntoElement) -> Self {
847 self.children.push(child.into_any_element());
848 self
849 }
850
851 pub fn active_class(mut self, style: impl Fn(Div) -> Div + 'static) -> Self {
853 self.active_class = Some(Box::new(style));
854 self
855 }
856
857 pub fn build<V: 'static>(self, cx: &mut Context<'_, V>) -> Div {
859 let path = self.path.clone();
860 let current_path = Navigator::current_path(cx);
861 let is_active = current_path == path.as_ref();
862
863 let mut link = div().cursor_pointer().on_mouse_down(
864 MouseButton::Left,
865 cx.listener(move |_view, _event, _window, cx| {
866 Navigator::push(cx, path.to_string());
867 cx.notify();
868 }),
869 );
870
871 if is_active {
873 if let Some(active_fn) = self.active_class {
874 link = active_fn(link);
875 }
876 }
877
878 for child in self.children {
880 link = link.child(child);
881 }
882
883 link
884 }
885}
886
887pub fn router_link<V: 'static>(
889 cx: &mut Context<'_, V>,
890 path: impl Into<SharedString>,
891 label: impl Into<SharedString>,
892) -> Div {
893 let path_str: SharedString = path.into();
894 let label_str: SharedString = label.into();
895 let current_path = Navigator::current_path(cx);
896 let is_active = current_path == path_str.as_ref();
897
898 div()
899 .cursor_pointer()
900 .text_color(if is_active {
901 rgb(0x2196f3)
902 } else {
903 rgb(0x333333)
904 })
905 .hover(|this| this.text_color(rgb(0x2196f3)))
906 .child(label_str)
907 .on_mouse_down(
908 MouseButton::Left,
909 cx.listener(move |_view, _event, _window, cx| {
910 Navigator::push(cx, path_str.to_string());
911 cx.notify();
912 }),
913 )
914}
915
916pub struct DefaultPages {
922 pub not_found: Option<Box<dyn Fn() -> AnyElement + Send + Sync>>,
924 pub loading: Option<Box<dyn Fn() -> AnyElement + Send + Sync>>,
926 pub error: Option<Box<dyn Fn(&str) -> AnyElement + Send + Sync>>,
928}
929
930impl DefaultPages {
931 pub fn new() -> Self {
933 Self {
934 not_found: None,
935 loading: None,
936 error: None,
937 }
938 }
939
940 pub fn with_not_found<F>(mut self, builder: F) -> Self
942 where
943 F: Fn() -> AnyElement + Send + Sync + 'static,
944 {
945 self.not_found = Some(Box::new(builder));
946 self
947 }
948
949 pub fn with_loading<F>(mut self, builder: F) -> Self
951 where
952 F: Fn() -> AnyElement + Send + Sync + 'static,
953 {
954 self.loading = Some(Box::new(builder));
955 self
956 }
957
958 pub fn with_error<F>(mut self, builder: F) -> Self
960 where
961 F: Fn(&str) -> AnyElement + Send + Sync + 'static,
962 {
963 self.error = Some(Box::new(builder));
964 self
965 }
966
967 pub fn render_not_found(&self) -> AnyElement {
969 if let Some(builder) = &self.not_found {
970 builder()
971 } else {
972 default_not_found_page().into_any_element()
973 }
974 }
975
976 pub fn render_loading(&self) -> AnyElement {
978 if let Some(builder) = &self.loading {
979 builder()
980 } else {
981 default_loading_page().into_any_element()
982 }
983 }
984
985 pub fn render_error(&self, message: &str) -> AnyElement {
987 if let Some(builder) = &self.error {
988 builder(message)
989 } else {
990 default_error_page(message).into_any_element()
991 }
992 }
993}
994
995impl Default for DefaultPages {
996 fn default() -> Self {
997 Self::new()
998 }
999}
1000
1001fn not_found_page() -> impl IntoElement {
1007 default_not_found_page()
1010}
1011
1012fn default_not_found_page() -> impl IntoElement {
1014 use gpui::{div, relative, rgb, ParentElement, Styled};
1015
1016 div()
1017 .flex()
1018 .flex_col()
1019 .items_center()
1020 .justify_center()
1021 .size_full()
1022 .bg(rgb(0x1e1e1e))
1023 .p_8()
1024 .gap_6()
1025 .child(
1026 div()
1027 .flex()
1028 .items_center()
1029 .justify_center()
1030 .w(px(140.))
1031 .h(px(140.))
1032 .rounded(px(24.))
1033 .bg(rgb(0xf44336))
1034 .shadow_lg()
1035 .child(
1036 div()
1037 .text_color(rgb(0xffffff))
1038 .text_size(px(64.))
1039 .child("404"),
1040 ),
1041 )
1042 .child(
1043 div()
1044 .text_3xl()
1045 .font_weight(FontWeight::BOLD)
1046 .text_color(rgb(0xffffff))
1047 .child("Page Not Found"),
1048 )
1049 .child(
1050 div()
1051 .text_base()
1052 .text_color(rgb(0xcccccc))
1053 .text_center()
1054 .max_w(px(500.))
1055 .line_height(relative(1.6))
1056 .child("The page you're looking for doesn't exist or has been moved."),
1057 )
1058 .child(
1059 div()
1060 .mt_4()
1061 .p_6()
1062 .bg(rgb(0x252526))
1063 .rounded(px(12.))
1064 .border_1()
1065 .border_color(rgb(0x3e3e3e))
1066 .max_w(px(600.))
1067 .child(
1068 div()
1069 .flex()
1070 .flex_col()
1071 .gap_3()
1072 .child(
1073 div()
1074 .text_sm()
1075 .font_weight(FontWeight::BOLD)
1076 .text_color(rgb(0xf44336))
1077 .mb_2()
1078 .child("What happened?"),
1079 )
1080 .child(not_found_item("•", "The route doesn't exist in the router"))
1081 .child(not_found_item("•", "The URL might be mistyped"))
1082 .child(not_found_item("•", "The page may have been removed")),
1083 ),
1084 )
1085}
1086
1087fn not_found_item(bullet: &str, text: &str) -> impl IntoElement {
1088 use gpui::{div, rgb, ParentElement, Styled};
1089
1090 div()
1091 .flex()
1092 .items_start()
1093 .gap_3()
1094 .child(
1095 div()
1096 .text_sm()
1097 .text_color(rgb(0xf44336))
1098 .child(bullet.to_string()),
1099 )
1100 .child(
1101 div()
1102 .text_sm()
1103 .text_color(rgb(0xcccccc))
1104 .line_height(relative(1.5))
1105 .child(text.to_string()),
1106 )
1107}
1108
1109fn default_loading_page() -> impl IntoElement {
1111 use gpui::{div, rgb, ParentElement, Styled};
1112
1113 div()
1114 .flex()
1115 .flex_col()
1116 .items_center()
1117 .justify_center()
1118 .size_full()
1119 .bg(rgb(0x1e1e1e))
1120 .gap_4()
1121 .child(
1122 div()
1123 .flex()
1124 .items_center()
1125 .justify_center()
1126 .w(px(80.))
1127 .h(px(80.))
1128 .rounded(px(16.))
1129 .bg(rgb(0x2196f3))
1130 .shadow_lg()
1131 .child(
1132 div()
1133 .text_color(rgb(0xffffff))
1134 .text_size(px(36.))
1135 .child("⏳"),
1136 ),
1137 )
1138 .child(
1139 div()
1140 .text_xl()
1141 .font_weight(FontWeight::MEDIUM)
1142 .text_color(rgb(0xffffff))
1143 .child("Loading..."),
1144 )
1145 .child(
1146 div()
1147 .text_sm()
1148 .text_color(rgb(0x888888))
1149 .child("Please wait"),
1150 )
1151}
1152
1153fn default_error_page(message: &str) -> impl IntoElement {
1155 use gpui::{div, relative, rgb, ParentElement, Styled};
1156
1157 div()
1158 .flex()
1159 .flex_col()
1160 .items_center()
1161 .justify_center()
1162 .size_full()
1163 .bg(rgb(0x1e1e1e))
1164 .p_8()
1165 .gap_6()
1166 .child(
1167 div()
1168 .flex()
1169 .items_center()
1170 .justify_center()
1171 .w(px(120.))
1172 .h(px(120.))
1173 .rounded(px(20.))
1174 .bg(rgb(0xff9800))
1175 .shadow_lg()
1176 .child(
1177 div()
1178 .text_color(rgb(0xffffff))
1179 .text_size(px(48.))
1180 .child("⚠️"),
1181 ),
1182 )
1183 .child(
1184 div()
1185 .text_2xl()
1186 .font_weight(FontWeight::BOLD)
1187 .text_color(rgb(0xffffff))
1188 .child("Something Went Wrong"),
1189 )
1190 .child(
1191 div()
1192 .text_base()
1193 .text_color(rgb(0xcccccc))
1194 .text_center()
1195 .max_w(px(500.))
1196 .line_height(relative(1.6))
1197 .child(message.to_string()),
1198 )
1199 .child(
1200 div()
1201 .mt_2()
1202 .px_6()
1203 .py_3()
1204 .bg(rgb(0x252526))
1205 .rounded(px(8.))
1206 .border_1()
1207 .border_color(rgb(0x3e3e3e))
1208 .child(
1209 div()
1210 .text_sm()
1211 .text_color(rgb(0x888888))
1212 .child("Try refreshing the page or contact support"),
1213 ),
1214 )
1215}
1216
1217#[cfg(test)]
1218mod tests {
1219 use super::{find_parent_route_for_path, RouterOutlet};
1220 use crate::route::Route;
1221 use gpui::{div, IntoElement, ParentElement};
1222 use std::sync::Arc;
1223
1224 #[test]
1225 fn test_outlet_creation() {
1226 let outlet = RouterOutlet::default();
1227 assert!(outlet.name.is_none());
1228
1229 let named = RouterOutlet::named("sidebar");
1230 assert_eq!(named.name.as_deref(), Some("sidebar"));
1231 }
1232
1233 #[test]
1234 fn test_outlet_name() {
1235 let outlet = RouterOutlet::new();
1236 assert!(outlet.name.is_none());
1237
1238 let named = RouterOutlet::named("main");
1239 assert_eq!(named.name, Some("main".to_string()));
1240 }
1241
1242 fn dummy_builder(
1244 _window: &mut gpui::Window,
1245 _cx: &mut gpui::App,
1246 _params: &crate::RouteParams,
1247 ) -> gpui::AnyElement {
1248 div().child("test").into_any_element()
1249 }
1250
1251 #[test]
1252 fn test_find_parent_route_simple() {
1253 let routes = vec![Arc::new(Route::new("/dashboard", dummy_builder).children(
1258 vec![
1259 Arc::new(Route::new("overview", dummy_builder)),
1260 Arc::new(Route::new("analytics", dummy_builder)),
1261 ],
1262 ))];
1263
1264 let result = find_parent_route_for_path(&routes, "/dashboard/analytics");
1266 assert!(result.is_some());
1267 assert_eq!(result.unwrap().config.path, "/dashboard");
1268 }
1269
1270 #[test]
1271 fn test_find_parent_route_exact_match() {
1272 let routes = vec![Arc::new(
1273 Route::new("/dashboard", dummy_builder)
1274 .children(vec![Arc::new(Route::new("settings", dummy_builder))]),
1275 )];
1276
1277 let result = find_parent_route_for_path(&routes, "/dashboard");
1279 assert!(result.is_some());
1280 assert_eq!(result.unwrap().config.path, "/dashboard");
1281 }
1282
1283 #[test]
1284 fn test_find_parent_route_no_children() {
1285 let routes = vec![Arc::new(Route::new("/about", dummy_builder))];
1287
1288 let result = find_parent_route_for_path(&routes, "/about");
1290 assert!(result.is_none());
1291 }
1292
1293 #[test]
1294 fn test_find_parent_route_nested_parents() {
1295 let routes = vec![Arc::new(Route::new("/dashboard", dummy_builder).children(
1300 vec![Arc::new(Route::new("settings", dummy_builder).children(
1301 vec![Arc::new(Route::new("profile", dummy_builder))],
1302 ))],
1303 ))];
1304
1305 let result = find_parent_route_for_path(&routes, "/dashboard/settings/profile");
1307 assert!(result.is_some());
1308 assert_eq!(result.unwrap().config.path, "settings");
1309 }
1310
1311 #[test]
1312 fn test_find_parent_route_root() {
1313 let routes = vec![Arc::new(Route::new("/", dummy_builder).children(vec![
1315 Arc::new(Route::new("home", dummy_builder)),
1316 Arc::new(Route::new("about", dummy_builder)),
1317 ]))];
1318
1319 let result = find_parent_route_for_path(&routes, "/home");
1321 assert!(result.is_some());
1322 assert_eq!(result.unwrap().config.path, "/");
1323
1324 let result = find_parent_route_for_path(&routes, "/about");
1325 assert!(result.is_some());
1326 assert_eq!(result.unwrap().config.path, "/");
1327 }
1328
1329 #[test]
1330 fn test_find_parent_route_multiple_top_level() {
1331 let routes = vec![
1333 Arc::new(
1334 Route::new("/dashboard", dummy_builder)
1335 .children(vec![Arc::new(Route::new("overview", dummy_builder))]),
1336 ),
1337 Arc::new(
1338 Route::new("/settings", dummy_builder)
1339 .children(vec![Arc::new(Route::new("profile", dummy_builder))]),
1340 ),
1341 ];
1342
1343 let result = find_parent_route_for_path(&routes, "/dashboard/overview");
1345 assert!(result.is_some());
1346 assert_eq!(result.unwrap().config.path, "/dashboard");
1347
1348 let result = find_parent_route_for_path(&routes, "/settings/profile");
1349 assert!(result.is_some());
1350 assert_eq!(result.unwrap().config.path, "/settings");
1351 }
1352
1353 #[test]
1354 fn test_find_parent_route_no_match() {
1355 let routes = vec![Arc::new(
1356 Route::new("/dashboard", dummy_builder)
1357 .children(vec![Arc::new(Route::new("overview", dummy_builder))]),
1358 )];
1359
1360 let result = find_parent_route_for_path(&routes, "/nonexistent/path");
1362 assert!(result.is_none());
1363 }
1364
1365 #[test]
1366 fn test_find_parent_route_trailing_slash() {
1367 let routes = vec![Arc::new(
1368 Route::new("/dashboard/", dummy_builder)
1369 .children(vec![Arc::new(Route::new("settings", dummy_builder))]),
1370 )];
1371
1372 let result = find_parent_route_for_path(&routes, "/dashboard/settings");
1374 assert!(result.is_some());
1375 }
1376
1377 #[test]
1378 fn test_find_parent_route_empty_child_path() {
1379 let routes = vec![Arc::new(Route::new("/dashboard", dummy_builder).children(
1381 vec![
1382 Arc::new(Route::new("", dummy_builder)), Arc::new(Route::new("settings", dummy_builder)),
1384 ],
1385 ))];
1386
1387 let result = find_parent_route_for_path(&routes, "/dashboard");
1389 assert!(result.is_some());
1390 assert_eq!(result.unwrap().config.path, "/dashboard");
1391 }
1392
1393 #[test]
1394 fn test_find_parent_prefers_deepest() {
1395 let routes = vec![Arc::new(Route::new("/", dummy_builder).children(vec![
1401 Arc::new(
1402 Route::new("dashboard", dummy_builder).children(vec![Arc::new(
1403 Route::new("settings", dummy_builder)
1404 .children(vec![Arc::new(Route::new("profile", dummy_builder))]),
1405 )]),
1406 ),
1407 ]))];
1408
1409 let result = find_parent_route_for_path(&routes, "/dashboard/settings/profile");
1411 assert!(result.is_some());
1412 assert_eq!(result.unwrap().config.path, "settings");
1413
1414 let result = find_parent_route_for_path(&routes, "/dashboard/settings");
1416 assert!(result.is_some());
1417 assert_eq!(result.unwrap().config.path, "dashboard");
1418 }
1419}