Skip to main content

gpui_navigator/
context.rs

1//! Router context integration for GPUI
2//!
3//! This module provides the global router state management through GPUI's context system.
4//! It exposes the `Navigator` API for navigation operations and manages router lifecycle.
5
6#[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
14// ============================================================================
15// NavigationRequest
16// ============================================================================
17
18/// Request for navigation.
19///
20/// Contains information about the navigation being performed.
21///
22/// # Example
23///
24/// ```
25/// use gpui_navigator::NavigationRequest;
26///
27/// let request = NavigationRequest::new("/dashboard".to_string());
28/// assert_eq!(request.to, "/dashboard");
29/// ```
30pub struct NavigationRequest {
31    /// The path we're navigating from (if any)
32    pub from: Option<String>,
33
34    /// The path we're navigating to
35    pub to: String,
36
37    /// Route parameters extracted from the path
38    pub params: RouteParams,
39}
40
41impl NavigationRequest {
42    /// Create a new navigation request
43    pub fn new(to: String) -> Self {
44        Self {
45            from: None,
46            to,
47            params: RouteParams::new(),
48        }
49    }
50
51    /// Create a navigation request with a source path
52    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    /// Set route parameters
61    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// ============================================================================
78// GlobalRouter
79// ============================================================================
80
81/// Global router state accessible from any component
82#[derive(Clone)]
83pub struct GlobalRouter {
84    state: RouterState,
85    /// Cache for nested route resolution
86    #[cfg(feature = "cache")]
87    nested_cache: RouteCache,
88    /// Registry for named routes
89    named_routes: NamedRouteRegistry,
90    /// Transition override for next navigation
91    #[cfg(feature = "transition")]
92    next_transition: Option<Transition>,
93}
94
95impl GlobalRouter {
96    /// Create a new global router
97    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    /// Register a route
109    pub fn add_route(&mut self, route: Route) {
110        // Register named route if it has a name
111        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        // Clear cache when routes change
118        #[cfg(feature = "cache")]
119        self.nested_cache.clear();
120    }
121
122    /// Navigate to a named route with parameters
123    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    /// Generate URL for a named route
129    pub fn url_for(&self, name: &str, params: &RouteParams) -> Option<String> {
130        self.named_routes.url_for(name, params)
131    }
132
133    /// Navigate to a path
134    pub fn push(&mut self, path: String) -> RouteChangeEvent {
135        // Clear cache on navigation
136        #[cfg(feature = "cache")]
137        self.nested_cache.clear();
138        self.state.push(path)
139    }
140
141    /// Replace current path
142    pub fn replace(&mut self, path: String) -> RouteChangeEvent {
143        // Clear cache on navigation
144        #[cfg(feature = "cache")]
145        self.nested_cache.clear();
146        self.state.replace(path)
147    }
148
149    /// Go back
150    pub fn back(&mut self) -> Option<RouteChangeEvent> {
151        // Clear cache on navigation
152        #[cfg(feature = "cache")]
153        self.nested_cache.clear();
154        self.state.back()
155    }
156
157    /// Go forward
158    pub fn forward(&mut self) -> Option<RouteChangeEvent> {
159        // Clear cache on navigation
160        #[cfg(feature = "cache")]
161        self.nested_cache.clear();
162        self.state.forward()
163    }
164
165    /// Get current path
166    pub fn current_path(&self) -> &str {
167        self.state.current_path()
168    }
169
170    /// Get current route match (with caching, requires mutable)
171    pub fn current_match(&mut self) -> Option<crate::RouteMatch> {
172        self.state.current_match()
173    }
174
175    /// Get current route match (immutable, no caching)
176    ///
177    /// Use this from Render implementations and other immutable contexts
178    pub fn current_match_immutable(&self) -> Option<crate::RouteMatch> {
179        self.state.current_match_immutable()
180    }
181
182    /// Get the current matched Route
183    ///
184    /// Returns the shared `Arc<Route>` that matched the current path.
185    /// Useful for accessing the route's children and builder without cloning.
186    pub fn current_route(&self) -> Option<&std::sync::Arc<crate::route::Route>> {
187        self.state.current_route()
188    }
189
190    /// Check if can go back
191    pub fn can_go_back(&self) -> bool {
192        self.state.can_go_back()
193    }
194
195    /// Check if can go forward
196    pub fn can_go_forward(&self) -> bool {
197        self.state.can_go_forward()
198    }
199
200    /// Get mutable state reference
201    pub fn state_mut(&mut self) -> &mut RouterState {
202        &mut self.state
203    }
204
205    /// Get state reference
206    pub fn state(&self) -> &RouterState {
207        &self.state
208    }
209
210    /// Get nested route cache (mutable)
211    #[cfg(feature = "cache")]
212    pub fn nested_cache_mut(&mut self) -> &mut RouteCache {
213        &mut self.nested_cache
214    }
215
216    /// Get nested route cache statistics
217    #[cfg(feature = "cache")]
218    pub fn cache_stats(&self) -> &CacheStats {
219        self.nested_cache.stats()
220    }
221
222    /// Set transition for the next navigation
223    ///
224    /// This override will be used for the next push/replace operation,
225    /// then automatically cleared.
226    ///
227    /// # Example
228    /// ```ignore
229    /// use gpui_navigator::{GlobalRouter, Transition};
230    ///
231    /// cx.update_global::<GlobalRouter, _>(|router, _| {
232    ///     router.set_next_transition(Transition::fade(300));
233    ///     router.push("/page".to_string());
234    /// });
235    /// ```
236    #[cfg(feature = "transition")]
237    pub fn set_next_transition(&mut self, transition: Transition) {
238        self.next_transition = Some(transition);
239    }
240
241    /// Get and consume the next transition override
242    ///
243    /// Returns the transition override if set, and clears it.
244    /// Used internally by navigation methods.
245    #[cfg(feature = "transition")]
246    pub fn take_next_transition(&mut self) -> Option<Transition> {
247        self.next_transition.take()
248    }
249
250    /// Check if there's a transition override set
251    #[cfg(feature = "transition")]
252    pub fn has_next_transition(&self) -> bool {
253        self.next_transition.is_some()
254    }
255
256    /// Clear transition override without consuming it
257    #[cfg(feature = "transition")]
258    pub fn clear_next_transition(&mut self) {
259        self.next_transition = None;
260    }
261
262    /// Navigate with a specific transition
263    ///
264    /// Convenience method that sets the transition and navigates in one call.
265    ///
266    /// # Example
267    /// ```ignore
268    /// use gpui_navigator::{GlobalRouter, Transition};
269    ///
270    /// cx.update_global::<GlobalRouter, _>(|router, _| {
271    ///     router.push_with_transition("/page".to_string(), Transition::slide_left(300));
272    /// });
273    /// ```
274    #[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    /// Replace with a specific transition
285    #[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
304/// Trait for accessing the global router from context
305pub trait UseRouter {
306    /// Get reference to global router
307    fn router(&self) -> &GlobalRouter;
308
309    /// Update global router
310    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
328/// Initialize global router with routes
329///
330/// # Example
331///
332/// ```ignore
333/// use gpui_navigator::{init_router, Route};
334///
335/// fn main() {
336///     App::new().run(|cx| {
337///         init_router(cx, |router| {
338///             router.add_route(Route::new("/", |_, _cx, _params| gpui::div().into_any_element()));
339///             router.add_route(Route::new("/users/:id", |_, _cx, _params| gpui::div().into_any_element()));
340///         });
341///     });
342/// }
343/// ```
344pub 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
353/// Navigate to a path using global router
354///
355/// # Example
356///
357/// ```ignore
358/// use gpui_navigator::navigate;
359///
360/// // In any component with access to App
361/// navigate(cx, "/users/123");
362/// ```
363pub fn navigate(cx: &mut App, path: impl Into<String>) {
364    cx.update_router(|router, _cx| {
365        router.push(path.into());
366    });
367}
368
369/// Get current path from global router
370pub fn current_path(cx: &App) -> String {
371    cx.router().current_path().to_string()
372}
373
374/// Handle for Navigator.of(context) pattern
375///
376/// Provides instance methods for chained navigation calls.
377pub struct NavigatorHandle<'a, C: BorrowAppContext> {
378    cx: &'a mut C,
379}
380
381impl<C: BorrowAppContext> NavigatorHandle<'_, C> {
382    /// Navigate to a new path
383    ///
384    /// # Example
385    ///
386    /// ```ignore
387    /// use gpui_navigator::{Navigator, PageRoute};
388    ///
389    /// // Simple path
390    /// Navigator::of(cx).push("/users");
391    ///
392    /// // With PageRoute
393    /// Navigator::of(cx).push(PageRoute::builder("/users/:id", |_, _cx, _params| gpui::div())
394    ///     .with_param("id".into(), "123".into()));
395    /// ```
396    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    /// Replace current path without adding to history
405    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    /// Go back to the previous route
414    pub fn pop(self) -> Self {
415        self.cx.update_global::<GlobalRouter, _>(|router, _| {
416            router.back();
417        });
418        self
419    }
420
421    /// Go forward in history
422    pub fn forward(self) -> Self {
423        self.cx.update_global::<GlobalRouter, _>(|router, _| {
424            router.forward();
425        });
426        self
427    }
428}
429
430/// Navigation API for convenient route navigation
431///
432/// Provides static methods for navigation operations:
433/// - `Navigator::push(cx, "/path")` - Navigate to a new page
434/// - `Navigator::pop(cx)` - Go back to previous page
435/// - `Navigator::replace(cx, "/path")` - Replace current page
436///
437/// Works with any context that has access to App (`Context<V>`, `App`, etc.)
438///
439/// # Example
440///
441/// ```ignore
442/// use gpui_navigator::Navigator;
443///
444/// // Navigate to a new route
445/// Navigator::push(cx, "/users/123");
446///
447/// // Go back
448/// Navigator::pop(cx);
449///
450/// // Replace current route
451/// Navigator::replace(cx, "/login");
452/// ```
453pub struct Navigator;
454
455impl Navigator {
456    /// Get a NavigatorHandle for the given context
457    ///
458    /// This allows chained navigation calls:
459    /// ```ignore
460    /// use gpui_navigator::Navigator;
461    ///
462    /// // Chained style
463    /// Navigator::of(cx).push("/users");
464    /// Navigator::of(cx).pop();
465    ///
466    /// // Or direct style (also works)
467    /// Navigator::push(cx, "/users");
468    /// Navigator::pop(cx);
469    /// ```
470    pub fn of<C: BorrowAppContext>(cx: &mut C) -> NavigatorHandle<'_, C> {
471        NavigatorHandle { cx }
472    }
473
474    /// Navigate to a new path
475    ///
476    /// # Example
477    ///
478    /// ```ignore
479    /// use gpui_navigator::{Navigator, PageRoute};
480    ///
481    /// // Simple string path
482    /// Navigator::push(cx, "/users/123");
483    ///
484    /// // With PageRoute and params
485    /// Navigator::push(cx, PageRoute::builder("/profile", |_, _cx, _params| gpui::div())
486    ///     .with_param("userId".into(), "456".into()));
487    /// ```
488    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    /// Replace current path without adding to history
496    ///
497    /// # Example
498    ///
499    /// ```ignore
500    /// use gpui_navigator::{Navigator, PageRoute};
501    ///
502    /// // Simple string path
503    /// Navigator::replace(cx, "/login");
504    ///
505    /// // With PageRoute
506    /// Navigator::replace(cx, PageRoute::builder("/login", |_, _cx, _params| gpui::div())
507    ///     .with_param("redirect".into(), "/dashboard".into()));
508    /// ```
509    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    /// Go back to the previous route
517    ///
518    /// # Example
519    ///
520    /// ```ignore
521    /// use gpui_navigator::Navigator;
522    ///
523    /// if Navigator::can_pop(cx) {
524    ///     Navigator::pop(cx);
525    /// }
526    /// ```
527    pub fn pop(cx: &mut impl BorrowAppContext) {
528        cx.update_global::<GlobalRouter, _>(|router, _| {
529            router.back();
530        });
531    }
532
533    /// Alias for pop() - go back (kept for compatibility)
534    pub fn back(cx: &mut impl BorrowAppContext) {
535        Self::pop(cx);
536    }
537
538    /// Go forward in history
539    pub fn forward(cx: &mut impl BorrowAppContext) {
540        cx.update_global::<GlobalRouter, _>(|router, _| {
541            router.forward();
542        });
543    }
544
545    /// Get current path
546    ///
547    /// Works with `Context<V>` since it derefs to App
548    ///
549    /// # Example
550    ///
551    /// ```ignore
552    /// use gpui_navigator::Navigator;
553    ///
554    /// let path = Navigator::current_path(cx);
555    /// ```
556    pub fn current_path(cx: &App) -> String {
557        cx.global::<GlobalRouter>().current_path().to_string()
558    }
559
560    /// Check if can go back
561    pub fn can_pop(cx: &App) -> bool {
562        cx.global::<GlobalRouter>().can_go_back()
563    }
564
565    /// Alias for can_pop() - check if can go back (kept for compatibility)
566    pub fn can_go_back(cx: &App) -> bool {
567        Self::can_pop(cx)
568    }
569
570    /// Navigate to a named route with parameters
571    ///
572    /// # Example
573    ///
574    /// ```ignore
575    /// use gpui_navigator::{Navigator, RouteParams};
576    ///
577    /// let mut params = RouteParams::new();
578    /// params.set("id".into(), "123".into());
579    ///
580    /// Navigator::push_named(cx, "user.detail", &params);
581    /// ```
582    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    /// Generate URL for a named route
589    ///
590    /// # Example
591    ///
592    /// ```ignore
593    /// use gpui_navigator::{Navigator, RouteParams};
594    ///
595    /// let mut params = RouteParams::new();
596    /// params.set("id".into(), "123".into());
597    ///
598    /// let url = Navigator::url_for(cx, "user.detail", &params);
599    /// assert_eq!(url, Some("/users/123".to_string()));
600    /// ```
601    pub fn url_for(cx: &App, name: &str, params: &RouteParams) -> Option<String> {
602        cx.global::<GlobalRouter>().url_for(name, params)
603    }
604
605    /// Check if can go forward
606    pub fn can_go_forward(cx: &App) -> bool {
607        cx.global::<GlobalRouter>().can_go_forward()
608    }
609
610    /// Set transition for the next navigation
611    ///
612    /// The transition will be used for the next push/replace call,
613    /// then automatically cleared.
614    ///
615    /// # Example
616    /// ```ignore
617    /// use gpui_navigator::{Navigator, Transition};
618    ///
619    /// Navigator::set_next_transition(cx, Transition::fade(300));
620    /// Navigator::push(cx, "/page");
621    /// ```
622    #[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    /// Navigate with a specific transition
630    ///
631    /// # Example
632    /// ```ignore
633    /// use gpui_navigator::{Navigator, Transition};
634    ///
635    /// Navigator::push_with_transition(cx, "/page", Transition::slide_left(300));
636    /// ```
637    #[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    /// Replace with a specific transition
650    ///
651    /// # Example
652    /// ```ignore
653    /// use gpui_navigator::{Navigator, Transition};
654    ///
655    /// Navigator::replace_with_transition(cx, "/page", Transition::fade(200));
656    /// ```
657    #[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    /// Push named route with a specific transition
670    ///
671    /// # Example
672    /// ```ignore
673    /// use gpui_navigator::{Navigator, RouteParams, Transition};
674    ///
675    /// let mut params = RouteParams::new();
676    /// params.set("id".to_string(), "123".to_string());
677    /// Navigator::push_named_with_transition(
678    ///     cx,
679    ///     "user.detail",
680    ///     &params,
681    ///     Transition::slide_right(300)
682    /// );
683    /// ```
684    #[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        // Initialize router
706        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        // Test initial state
721        let initial_path = cx.read(Navigator::current_path);
722        assert_eq!(initial_path, "/");
723
724        // Test push navigation
725        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        // Test push with parameters
733        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        // Initialize router
744        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        // Navigate to multiple pages
759        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        // Test back navigation
768        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        // Test forward navigation
777        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        // Initialize router
788        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        // Navigate and then replace
803        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        // After replace, going back should skip the replaced route
811        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        // Initialize router
821        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        // At initial state, can't go back
830        assert!(!cx.read(Navigator::can_pop));
831
832        // After navigation, can go back
833        cx.update(|cx| {
834            Navigator::push(cx, "/page1");
835        });
836
837        assert!(cx.read(Navigator::can_pop));
838
839        // After going back, can't go back further
840        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        // Initialize router
850        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        // Navigate through multiple pages
868        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        // Go back multiple times
877        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        // Initialize router
896        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        // Navigate to routes with parameters
912        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        // Initialize router
928        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        // Test Flutter-style Navigator.of(context).push()
943        cx.update(|cx| {
944            Navigator::of(cx).push("/home");
945        });
946
947        assert_eq!(cx.read(Navigator::current_path), "/home");
948
949        // Test chaining
950        cx.update(|cx| {
951            Navigator::of(cx).push("/profile").pop();
952        });
953
954        assert_eq!(cx.read(Navigator::current_path), "/home");
955
956        // Test replace
957        cx.update(|cx| {
958            Navigator::of(cx).replace("/profile");
959        });
960
961        assert_eq!(cx.read(Navigator::current_path), "/profile");
962
963        // After replace, we're still at index 1 in history, so we can still go back to "/"
964        assert!(cx.read(Navigator::can_pop));
965
966        // Pop back to "/"
967        cx.update(|cx| {
968            Navigator::of(cx).pop();
969        });
970
971        assert_eq!(cx.read(Navigator::current_path), "/");
972
973        // Now we're at the root, can't go back anymore
974        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        // Initialize router
982        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        // Test PageRoute with params
994        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        // Test with Navigator.of() style
1007        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        // Initialize router
1022        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        // Test that strings still work with IntoRoute
1034        cx.update(|cx| {
1035            Navigator::push(cx, "/home");
1036        });
1037
1038        assert_eq!(cx.read(Navigator::current_path), "/home");
1039
1040        // Test with &str
1041        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        // Test String
1049        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        // Initialize router
1059        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        // Use static API
1074        cx.update(|cx| {
1075            Navigator::push(cx, "/page1");
1076        });
1077        assert_eq!(cx.read(Navigator::current_path), "/page1");
1078
1079        // Use Flutter-style API
1080        cx.update(|cx| {
1081            Navigator::of(cx).push("/page2");
1082        });
1083        assert_eq!(cx.read(Navigator::current_path), "/page2");
1084
1085        // Mix both styles
1086        cx.update(|cx| {
1087            Navigator::pop(cx); // Static API
1088        });
1089        assert_eq!(cx.read(Navigator::current_path), "/page1");
1090
1091        cx.update(|cx| {
1092            Navigator::of(cx).pop(); // Flutter style
1093        });
1094        assert_eq!(cx.read(Navigator::current_path), "/");
1095    }
1096}