Skip to main content

gpui_navigator/
transition.rs

1//! Route transition animations
2//!
3//! This module provides a transition system for route changes,
4//! allowing separate enter and exit animations for incoming and outgoing content.
5
6use gpui::{div, px, Div, IntoElement, ParentElement, Styled};
7use std::time::Duration;
8
9/// Direction for slide transitions
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum SlideDirection {
12    /// Slide from left to right
13    Left,
14    /// Slide from right to left
15    Right,
16    /// Slide from top to bottom
17    Up,
18    /// Slide from bottom to top
19    Down,
20}
21
22/// Built-in transition types
23#[derive(Default)]
24pub enum Transition {
25    /// No transition animation
26    #[default]
27    None,
28
29    /// Fade transition (simple opacity animation)
30    Fade {
31        /// Duration in milliseconds
32        duration_ms: u64,
33    },
34
35    /// Slide transition
36    Slide {
37        /// Direction to slide
38        direction: SlideDirection,
39        /// Duration in milliseconds
40        duration_ms: u64,
41    },
42}
43
44impl std::fmt::Debug for Transition {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        match self {
47            Self::None => write!(f, "Transition::None"),
48            Self::Fade { duration_ms } => f
49                .debug_struct("Transition::Fade")
50                .field("duration_ms", duration_ms)
51                .finish(),
52            Self::Slide {
53                direction,
54                duration_ms,
55            } => f
56                .debug_struct("Transition::Slide")
57                .field("direction", direction)
58                .field("duration_ms", duration_ms)
59                .finish(),
60        }
61    }
62}
63
64impl Clone for Transition {
65    fn clone(&self) -> Self {
66        match self {
67            Self::None => Self::None,
68            Self::Fade { duration_ms } => Self::Fade {
69                duration_ms: *duration_ms,
70            },
71            Self::Slide {
72                direction,
73                duration_ms,
74            } => Self::Slide {
75                direction: *direction,
76                duration_ms: *duration_ms,
77            },
78        }
79    }
80}
81
82impl Transition {
83    /// Create a fade transition
84    pub fn fade(duration_ms: u64) -> Self {
85        Self::Fade { duration_ms }
86    }
87
88    /// Create a slide-left transition
89    pub fn slide_left(duration_ms: u64) -> Self {
90        Self::Slide {
91            direction: SlideDirection::Left,
92            duration_ms,
93        }
94    }
95
96    /// Create a slide-right transition
97    pub fn slide_right(duration_ms: u64) -> Self {
98        Self::Slide {
99            direction: SlideDirection::Right,
100            duration_ms,
101        }
102    }
103
104    /// Create a slide-up transition
105    pub fn slide_up(duration_ms: u64) -> Self {
106        Self::Slide {
107            direction: SlideDirection::Up,
108            duration_ms,
109        }
110    }
111
112    /// Create a slide-down transition
113    pub fn slide_down(duration_ms: u64) -> Self {
114        Self::Slide {
115            direction: SlideDirection::Down,
116            duration_ms,
117        }
118    }
119
120    /// Get the duration of this transition
121    pub fn duration(&self) -> Duration {
122        match self {
123            Self::None => Duration::ZERO,
124            Self::Fade { duration_ms, .. } => Duration::from_millis(*duration_ms),
125            Self::Slide { duration_ms, .. } => Duration::from_millis(*duration_ms),
126        }
127    }
128
129    /// Check if this is a no-op transition
130    pub fn is_none(&self) -> bool {
131        matches!(self, Self::None)
132    }
133}
134
135/// Transition configuration for route navigation
136#[derive(Clone)]
137pub struct TransitionConfig {
138    /// Default transition for this route
139    pub default: Transition,
140
141    /// Override transition for specific navigation
142    pub override_next: Option<Transition>,
143}
144
145impl Default for TransitionConfig {
146    fn default() -> Self {
147        Self {
148            default: Transition::None,
149            override_next: None,
150        }
151    }
152}
153
154impl TransitionConfig {
155    /// Create a new transition config with a default transition
156    pub fn new(default: Transition) -> Self {
157        Self {
158            default,
159            override_next: None,
160        }
161    }
162
163    /// Get the active transition (override if set, otherwise default)
164    pub fn active(&self) -> &Transition {
165        self.override_next.as_ref().unwrap_or(&self.default)
166    }
167
168    /// Set an override transition for the next navigation
169    pub fn set_override(&mut self, transition: Transition) {
170        self.override_next = Some(transition);
171    }
172
173    /// Clear the override transition
174    pub fn clear_override(&mut self) {
175        self.override_next = None;
176    }
177
178    /// Check if there's an active override
179    pub fn has_override(&self) -> bool {
180        self.override_next.is_some()
181    }
182}
183
184// ============================================================================
185// Transition Builder
186// ============================================================================
187
188/// Transition context passed to transition builder
189pub struct TransitionContext {
190    /// Animation progress from 0.0 to 1.0
191    pub animation: f32,
192    /// Secondary animation for exit transitions (1.0 to 0.0)
193    pub secondary_animation: f32,
194}
195
196/// Applies transition effect to element based on Transition type
197///
198/// Takes an element, a transition type, and a progress value (0.0 to 1.0),
199/// then returns a `Div` with the appropriate visual transformation applied.
200pub fn apply_transition(element: impl IntoElement, transition: &Transition, progress: f32) -> Div {
201    // Always use consistent method chain to avoid recursion limit
202    // Calculate all values first, then apply them in one chain
203    let (x, y, opacity) = match transition {
204        Transition::None => (0.0, 0.0, 1.0),
205
206        Transition::Fade { .. } => {
207            // Simple fade in effect
208            (0.0, 0.0, progress)
209        }
210
211        Transition::Slide { direction, .. } => {
212            let offset_px = (1.0 - progress) * 100.0;
213            let (x, y) = match direction {
214                SlideDirection::Left => (offset_px, 0.0),
215                SlideDirection::Right => (-offset_px, 0.0),
216                SlideDirection::Up => (0.0, offset_px),
217                SlideDirection::Down => (0.0, -offset_px),
218            };
219            (x, y, progress)
220        }
221    };
222
223    // Unified return type - same method chain for all branches
224    div()
225        .relative()
226        .left(px(x))
227        .top(px(y))
228        .opacity(opacity)
229        .child(element)
230}
231
232/// Easing function - ease in out cubic
233pub fn ease_in_out_cubic(t: f32) -> f32 {
234    if t < 0.5 {
235        4.0 * t * t * t
236    } else {
237        1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
238    }
239}
240
241/// Apply easing to progress
242pub fn apply_easing(progress: f32) -> f32 {
243    ease_in_out_cubic(progress.clamp(0.0, 1.0))
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn test_slide_direction() {
252        assert_eq!(SlideDirection::Left, SlideDirection::Left);
253        assert_ne!(SlideDirection::Left, SlideDirection::Right);
254    }
255
256    #[test]
257    fn test_transition_none() {
258        let transition = Transition::None;
259        assert!(transition.is_none());
260        assert_eq!(transition.duration(), Duration::ZERO);
261    }
262
263    #[test]
264    fn test_transition_fade() {
265        let transition = Transition::fade(200);
266        assert!(!transition.is_none());
267        assert_eq!(transition.duration(), Duration::from_millis(200));
268    }
269
270    #[test]
271    fn test_transition_slide() {
272        let transition = Transition::slide_left(300);
273        assert!(!transition.is_none());
274        assert_eq!(transition.duration(), Duration::from_millis(300));
275
276        if let Transition::Slide { direction, .. } = transition {
277            assert_eq!(direction, SlideDirection::Left);
278        } else {
279            panic!("Expected Slide transition");
280        }
281    }
282
283    #[test]
284    fn test_transition_config_default() {
285        let config = TransitionConfig::default();
286        assert!(config.active().is_none());
287        assert!(!config.has_override());
288    }
289
290    #[test]
291    fn test_transition_config_with_default() {
292        let config = TransitionConfig::new(Transition::fade(200));
293        assert!(!config.active().is_none());
294        assert!(!config.has_override());
295    }
296
297    #[test]
298    fn test_transition_config_override() {
299        let mut config = TransitionConfig::new(Transition::fade(200));
300
301        config.set_override(Transition::slide_left(300));
302        assert!(config.has_override());
303        assert_eq!(config.active().duration(), Duration::from_millis(300));
304
305        config.clear_override();
306        assert!(!config.has_override());
307        assert_eq!(config.active().duration(), Duration::from_millis(200));
308    }
309
310    #[test]
311    fn test_transition_helpers() {
312        // Test all helper methods
313        let _ = Transition::fade(200);
314        let _ = Transition::slide_left(300);
315        let _ = Transition::slide_right(300);
316        let _ = Transition::slide_up(300);
317        let _ = Transition::slide_down(300);
318    }
319}