Skip to main content

gpui_navigator/
state.rs

1//! Router state management
2
3use crate::route::Route;
4use crate::{NavigationDirection, RouteChangeEvent, RouteMatch};
5use std::collections::HashMap;
6use std::sync::Arc;
7
8/// Router state
9#[derive(Debug, Clone)]
10pub struct RouterState {
11    /// Navigation history stack
12    history: Vec<String>,
13    /// Current position in history
14    current: usize,
15    /// Registered routes
16    routes: Vec<Arc<Route>>,
17    /// Route cache
18    cache: HashMap<String, RouteMatch>,
19}
20
21impl RouterState {
22    /// Create a new router state
23    pub fn new() -> Self {
24        Self {
25            history: vec!["/".to_string()],
26            current: 0,
27            routes: Vec::new(),
28            cache: HashMap::new(),
29        }
30    }
31
32    /// Register a route
33    pub fn add_route(&mut self, route: Route) {
34        self.routes.push(Arc::new(route));
35        // Routes have changed, so any cached matches may now be stale.
36        self.cache.clear();
37    }
38
39    /// Get current path
40    pub fn current_path(&self) -> &str {
41        &self.history[self.current]
42    }
43
44    /// Get all registered routes
45    pub fn routes(&self) -> &[Arc<Route>] {
46        &self.routes
47    }
48
49    /// Get current route match (with caching)
50    pub fn current_match(&mut self) -> Option<RouteMatch> {
51        let path = self.current_path();
52
53        // Check cache first
54        if let Some(cached) = self.cache.get(path) {
55            return Some(cached.clone());
56        }
57
58        // Find matching route
59        for route in &self.routes {
60            if let Some(route_match) = route.matches(path) {
61                self.cache.insert(path.to_string(), route_match.clone());
62                return Some(route_match);
63            }
64        }
65
66        None
67    }
68
69    /// Get current route match without caching (immutable)
70    ///
71    /// Use this when you need to access the current route from a non-mutable context,
72    /// such as in a GPUI Render implementation.
73    pub fn current_match_immutable(&self) -> Option<RouteMatch> {
74        let path = self.current_path();
75
76        // Check cache first
77        if let Some(cached) = self.cache.get(path) {
78            return Some(cached.clone());
79        }
80
81        // Find matching route without caching
82        for route in &self.routes {
83            if let Some(route_match) = route.matches(path) {
84                return Some(route_match);
85            }
86        }
87
88        None
89    }
90
91    /// Get the matched Route for current path
92    ///
93    /// Returns the Route object that matched, not just the RouteMatch.
94    /// This is needed for rendering and accessing the route's builder.
95    pub fn current_route(&self) -> Option<&Arc<Route>> {
96        let path = self.current_path();
97
98        self.routes
99            .iter()
100            .find(|route| route.matches(path).is_some())
101    }
102
103    /// Navigate to a new path
104    pub fn push(&mut self, path: String) -> RouteChangeEvent {
105        let from = Some(self.current_path().to_string());
106
107        // Remove forward history when pushing
108        self.history.truncate(self.current + 1);
109
110        // Add new path
111        self.history.push(path.clone());
112        self.current += 1;
113
114        RouteChangeEvent {
115            from,
116            to: path,
117            direction: NavigationDirection::Forward,
118        }
119    }
120
121    /// Replace current path
122    pub fn replace(&mut self, path: String) -> RouteChangeEvent {
123        let from = Some(self.current_path().to_string());
124
125        self.history[self.current] = path.clone();
126
127        RouteChangeEvent {
128            from,
129            to: path,
130            direction: NavigationDirection::Replace,
131        }
132    }
133
134    /// Go back in history
135    pub fn back(&mut self) -> Option<RouteChangeEvent> {
136        if self.current > 0 {
137            let from = Some(self.current_path().to_string());
138            self.current -= 1;
139            let to = self.current_path().to_string();
140
141            Some(RouteChangeEvent {
142                from,
143                to,
144                direction: NavigationDirection::Back,
145            })
146        } else {
147            None
148        }
149    }
150
151    /// Go forward in history
152    pub fn forward(&mut self) -> Option<RouteChangeEvent> {
153        if self.current < self.history.len() - 1 {
154            let from = Some(self.current_path().to_string());
155            self.current += 1;
156            let to = self.current_path().to_string();
157
158            Some(RouteChangeEvent {
159                from,
160                to,
161                direction: NavigationDirection::Forward,
162            })
163        } else {
164            None
165        }
166    }
167
168    /// Check if can go back
169    pub fn can_go_back(&self) -> bool {
170        self.current > 0
171    }
172
173    /// Check if can go forward
174    pub fn can_go_forward(&self) -> bool {
175        self.current < self.history.len() - 1
176    }
177
178    /// Clear navigation history
179    pub fn clear(&mut self) {
180        self.history.clear();
181        self.history.push("/".to_string());
182        self.current = 0;
183        self.cache.clear();
184    }
185}
186
187impl Default for RouterState {
188    fn default() -> Self {
189        Self::new()
190    }
191}
192
193/// Router - manages navigation state
194pub struct Router {
195    state: RouterState,
196}
197
198impl Router {
199    /// Create a new router
200    pub fn new() -> Self {
201        Self {
202            state: RouterState::new(),
203        }
204    }
205
206    /// Get mutable reference to state
207    pub fn state_mut(&mut self) -> &mut RouterState {
208        &mut self.state
209    }
210
211    /// Get reference to state
212    pub fn state(&self) -> &RouterState {
213        &self.state
214    }
215}
216
217impl Default for Router {
218    fn default() -> Self {
219        Self::new()
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_navigation() {
229        let mut state = RouterState::new();
230
231        assert_eq!(state.current_path(), "/");
232
233        state.push("/users".to_string());
234        assert_eq!(state.current_path(), "/users");
235
236        state.push("/users/123".to_string());
237        assert_eq!(state.current_path(), "/users/123");
238
239        state.back();
240        assert_eq!(state.current_path(), "/users");
241
242        state.forward();
243        assert_eq!(state.current_path(), "/users/123");
244    }
245
246    #[test]
247    fn test_replace() {
248        let mut state = RouterState::new();
249
250        state.push("/users".to_string());
251        state.replace("/posts".to_string());
252
253        assert_eq!(state.current_path(), "/posts");
254        assert_eq!(state.history.len(), 2);
255    }
256}