Skip to main content

gpui_navigator/
lifecycle.rs

1//! Route lifecycle hooks
2
3use crate::NavigationRequest;
4use gpui::App;
5use std::future::Future;
6use std::pin::Pin;
7
8/// Result of a lifecycle hook
9#[derive(Debug, Clone, PartialEq)]
10pub enum LifecycleResult {
11    /// Continue with navigation
12    Continue,
13
14    /// Abort navigation with reason
15    Abort { reason: String },
16
17    /// Redirect to another path
18    Redirect { to: String },
19}
20
21impl LifecycleResult {
22    /// Create a continue result
23    pub fn cont() -> Self {
24        Self::Continue
25    }
26
27    /// Create an abort result
28    pub fn abort(reason: impl Into<String>) -> Self {
29        Self::Abort {
30            reason: reason.into(),
31        }
32    }
33
34    /// Create a redirect result
35    pub fn redirect(to: impl Into<String>) -> Self {
36        Self::Redirect { to: to.into() }
37    }
38
39    /// Check if lifecycle allows continuation
40    pub fn allows_continue(&self) -> bool {
41        matches!(self, LifecycleResult::Continue)
42    }
43
44    /// Check if lifecycle aborts
45    pub fn is_abort(&self) -> bool {
46        matches!(self, LifecycleResult::Abort { .. })
47    }
48
49    /// Check if lifecycle redirects
50    pub fn is_redirect(&self) -> bool {
51        matches!(self, LifecycleResult::Redirect { .. })
52    }
53}
54
55/// Route lifecycle hooks
56///
57/// Lifecycle hooks allow you to run code at key points in the navigation process:
58/// - `on_enter`: Called when entering a route (for data loading, setup)
59/// - `on_exit`: Called when leaving a route (for cleanup, saving state)
60/// - `can_deactivate`: Called to check if user can leave (for unsaved changes warning)
61///
62/// # Example
63///
64/// ```no_run
65/// use gpui_navigator::{RouteLifecycle, LifecycleResult, NavigationRequest};
66/// use std::future::Future;
67/// use std::pin::Pin;
68///
69/// struct FormLifecycle;
70///
71/// impl RouteLifecycle for FormLifecycle {
72///     type Future = Pin<Box<dyn Future<Output = LifecycleResult> + Send>>;
73///
74///     fn on_enter(&self, _cx: &gpui::App, _request: &NavigationRequest) -> Self::Future {
75///         // Load form data
76///         Box::pin(async { LifecycleResult::Continue })
77///     }
78///
79///     fn on_exit(&self, _cx: &gpui::App) -> Self::Future {
80///         Box::pin(async { LifecycleResult::Continue })
81///     }
82///
83///     fn can_deactivate(&self, _cx: &gpui::App) -> Self::Future {
84///         // Check for unsaved changes
85///         Box::pin(async { LifecycleResult::Continue })
86///     }
87/// }
88/// ```
89pub trait RouteLifecycle: Send + Sync + 'static {
90    /// The future returned by lifecycle methods
91    type Future: Future<Output = LifecycleResult> + Send + 'static;
92
93    /// Called when entering the route
94    ///
95    /// Use this to:
96    /// - Load data for the route
97    /// - Set up subscriptions
98    /// - Initialize state
99    /// - Validate navigation parameters
100    ///
101    /// Return `LifecycleResult::Abort` to prevent navigation.
102    /// Return `LifecycleResult::Redirect` to navigate elsewhere.
103    fn on_enter(&self, cx: &App, request: &NavigationRequest) -> Self::Future;
104
105    /// Called when exiting the route
106    ///
107    /// Use this to:
108    /// - Save state
109    /// - Clean up subscriptions
110    /// - Cancel pending operations
111    ///
112    /// Return `LifecycleResult::Abort` to prevent navigation.
113    fn on_exit(&self, cx: &App) -> Self::Future;
114
115    /// Check if the route can be deactivated
116    ///
117    /// Use this to:
118    /// - Check for unsaved changes
119    /// - Confirm navigation away
120    /// - Validate state before leaving
121    ///
122    /// Return `LifecycleResult::Abort` to prevent navigation.
123    fn can_deactivate(&self, cx: &App) -> Self::Future;
124}
125
126/// Type-erased lifecycle for dynamic dispatch
127pub type BoxedLifecycle =
128    Box<dyn RouteLifecycle<Future = Pin<Box<dyn Future<Output = LifecycleResult> + Send>>>>;
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use gpui::TestAppContext;
134
135    struct TestLifecycle {
136        should_abort: bool,
137        should_redirect: bool,
138    }
139
140    impl RouteLifecycle for TestLifecycle {
141        type Future = Pin<Box<dyn Future<Output = LifecycleResult> + Send>>;
142
143        fn on_enter(&self, _cx: &App, _request: &NavigationRequest) -> Self::Future {
144            if self.should_abort {
145                Box::pin(async { LifecycleResult::abort("Test abort") })
146            } else if self.should_redirect {
147                Box::pin(async { LifecycleResult::redirect("/redirect") })
148            } else {
149                Box::pin(async { LifecycleResult::Continue })
150            }
151        }
152
153        fn on_exit(&self, _cx: &App) -> Self::Future {
154            Box::pin(async { LifecycleResult::Continue })
155        }
156
157        fn can_deactivate(&self, _cx: &App) -> Self::Future {
158            if self.should_abort {
159                Box::pin(async { LifecycleResult::abort("Cannot leave") })
160            } else {
161                Box::pin(async { LifecycleResult::Continue })
162            }
163        }
164    }
165
166    #[gpui::test]
167    fn test_lifecycle_result_continue(_cx: &mut TestAppContext) {
168        let result = LifecycleResult::Continue;
169        assert!(result.allows_continue());
170        assert!(!result.is_abort());
171        assert!(!result.is_redirect());
172    }
173
174    #[gpui::test]
175    fn test_lifecycle_result_abort(_cx: &mut TestAppContext) {
176        let result = LifecycleResult::abort("Test");
177        assert!(!result.allows_continue());
178        assert!(result.is_abort());
179        assert!(!result.is_redirect());
180    }
181
182    #[gpui::test]
183    fn test_lifecycle_result_redirect(_cx: &mut TestAppContext) {
184        let result = LifecycleResult::redirect("/test");
185        assert!(!result.allows_continue());
186        assert!(!result.is_abort());
187        assert!(result.is_redirect());
188    }
189
190    #[gpui::test]
191    fn test_lifecycle_on_enter_continue(cx: &mut TestAppContext) {
192        let lifecycle = TestLifecycle {
193            should_abort: false,
194            should_redirect: false,
195        };
196        let request = NavigationRequest::new("/test".to_string());
197
198        let result = cx.update(|cx| pollster::block_on(lifecycle.on_enter(cx, &request)));
199
200        assert_eq!(result, LifecycleResult::Continue);
201    }
202
203    #[gpui::test]
204    fn test_lifecycle_on_enter_abort(cx: &mut TestAppContext) {
205        let lifecycle = TestLifecycle {
206            should_abort: true,
207            should_redirect: false,
208        };
209        let request = NavigationRequest::new("/test".to_string());
210
211        let result = cx.update(|cx| pollster::block_on(lifecycle.on_enter(cx, &request)));
212
213        assert!(result.is_abort());
214    }
215
216    #[gpui::test]
217    fn test_lifecycle_on_enter_redirect(cx: &mut TestAppContext) {
218        let lifecycle = TestLifecycle {
219            should_abort: false,
220            should_redirect: true,
221        };
222        let request = NavigationRequest::new("/test".to_string());
223
224        let result = cx.update(|cx| pollster::block_on(lifecycle.on_enter(cx, &request)));
225
226        assert!(result.is_redirect());
227    }
228
229    #[gpui::test]
230    fn test_lifecycle_can_deactivate_allow(cx: &mut TestAppContext) {
231        let lifecycle = TestLifecycle {
232            should_abort: false,
233            should_redirect: false,
234        };
235
236        let result = cx.update(|cx| pollster::block_on(lifecycle.can_deactivate(cx)));
237
238        assert_eq!(result, LifecycleResult::Continue);
239    }
240
241    #[gpui::test]
242    fn test_lifecycle_can_deactivate_block(cx: &mut TestAppContext) {
243        let lifecycle = TestLifecycle {
244            should_abort: true,
245            should_redirect: false,
246        };
247
248        let result = cx.update(|cx| pollster::block_on(lifecycle.can_deactivate(cx)));
249
250        assert!(result.is_abort());
251    }
252}