leptos_motion_layout/
shared_elements.rs

1//! Shared element transitions for seamless view changes
2//!
3//! This module provides functionality for creating smooth transitions
4//! where elements appear to move between different views or states.
5
6use std::collections::{HashMap, VecDeque};
7use wasm_bindgen::prelude::*;
8use web_sys::Element;
9
10/// Shared element configuration
11#[derive(Debug, Clone)]
12pub struct SharedElementConfig {
13    /// Transition duration in seconds
14    pub duration: f64,
15    /// Easing function
16    pub easing: crate::flip::EasingFunction,
17    /// Whether to maintain aspect ratio
18    pub maintain_aspect_ratio: bool,
19    /// Whether to use hardware acceleration
20    pub hardware_accelerated: bool,
21}
22
23impl Default for SharedElementConfig {
24    fn default() -> Self {
25        Self {
26            duration: 0.3,
27            easing: crate::flip::EasingFunction::EaseOut,
28            maintain_aspect_ratio: false,
29            hardware_accelerated: true,
30        }
31    }
32}
33
34/// Z-index strategy for shared elements
35#[derive(Debug, Clone)]
36pub enum ZIndexStrategy {
37    /// Use fixed z-index values
38    Fixed {
39        /// Base z-index value
40        base: i32,
41        /// Increment per layer
42        increment: i32,
43    },
44    /// Use dynamic z-index based on element depth
45    Dynamic {
46        /// Base z-index value
47        base: i32,
48        /// Maximum z-index value
49        max: i32,
50    },
51    /// Use CSS custom properties for z-index
52    CustomProperty {
53        /// CSS custom property name
54        property: String,
55    },
56    /// Elevate element during transition
57    Elevate,
58    /// Custom z-index value
59    Custom(i32),
60    /// Maintain current z-index
61    Maintain,
62}
63
64impl Default for ZIndexStrategy {
65    fn default() -> Self {
66        ZIndexStrategy::Fixed {
67            base: 1000,
68            increment: 1,
69        }
70    }
71}
72
73/// Shared element transition
74#[derive(Debug)]
75pub struct SharedElementTransition {
76    /// Unique transition ID
77    pub id: String,
78    /// Source element
79    pub source_element: Element,
80    /// Target element
81    pub target_element: Element,
82    /// Transition configuration
83    pub config: SharedElementConfig,
84    /// Current progress (0.0 to 1.0)
85    pub progress: f64,
86    /// Whether transition is active
87    pub active: bool,
88    /// Start time
89    pub start_time: f64,
90}
91
92/// Manager for shared element transitions
93pub struct SharedElementManager {
94    /// Active transitions
95    active_transitions: HashMap<String, SharedElementTransition>,
96    /// Transition queue
97    transition_queue: VecDeque<QueuedTransition>,
98    /// Z-index strategy
99    #[allow(dead_code)]
100    z_index_strategy: ZIndexStrategy,
101    /// Performance tracking
102    performance_metrics: SharedElementMetrics,
103}
104
105/// Queued transition waiting to start
106#[derive(Debug)]
107pub struct QueuedTransition {
108    /// Source element
109    pub source_element: Element,
110    /// Target element
111    pub target_element: Element,
112    /// Transition configuration
113    pub config: SharedElementConfig,
114    /// Priority level
115    pub priority: TransitionPriority,
116}
117
118/// Transition priority levels
119#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
120pub enum TransitionPriority {
121    /// Low priority (background elements)
122    Low = 0,
123    /// Normal priority (default)
124    Normal = 1,
125    /// High priority (foreground elements)
126    High = 2,
127    /// Critical priority (modal overlays)
128    Critical = 3,
129}
130
131/// Transition record for tracking
132#[derive(Debug, Clone)]
133pub struct TransitionRecord {
134    /// Transition ID
135    pub id: String,
136    /// Start time
137    pub start_time: f64,
138    /// End time
139    pub end_time: Option<f64>,
140    /// Duration
141    pub duration: f64,
142    /// Success status
143    pub success: bool,
144    /// Performance metrics
145    pub performance: SharedElementMetrics,
146}
147
148/// Shared element performance metrics
149#[derive(Debug, Clone)]
150pub struct SharedElementMetrics {
151    /// Total transitions
152    pub total_transitions: usize,
153    /// Successful transitions
154    pub successful_transitions: usize,
155    /// Average transition duration
156    pub average_duration: f64,
157    /// Frame rate during transitions
158    pub frame_rate: f64,
159}
160
161impl SharedElementManager {
162    /// Create a new shared element manager
163    pub fn new(z_index_strategy: ZIndexStrategy) -> Self {
164        Self {
165            active_transitions: HashMap::new(),
166            transition_queue: VecDeque::new(),
167            z_index_strategy,
168            performance_metrics: SharedElementMetrics::default(),
169        }
170    }
171
172    /// Register a shared element
173    pub fn register_element(
174        &mut self,
175        _element: &Element,
176        config: &SharedElementConfig,
177    ) -> Result<(), String> {
178        // For now, just log the registration
179        // In a real implementation, this would set up element tracking
180        log::info!("Registering shared element with config: {:?}", config);
181
182        Ok(())
183    }
184
185    /// Unregister a shared element
186    pub fn unregister_element(&mut self, _element: &Element) -> Result<(), String> {
187        // For now, just log the unregistration
188        // In a real implementation, this would clean up element tracking
189        log::info!("Unregistering shared element");
190
191        Ok(())
192    }
193
194    /// Create a shared element transition
195    pub fn create_transition(
196        &mut self,
197        source_element: &Element,
198        target_element: &Element,
199        config: &SharedElementConfig,
200    ) -> Result<String, String> {
201        // Generate transition ID
202        let id = self.generate_transition_id();
203
204        // Create queued transition
205        let queued = QueuedTransition {
206            source_element: source_element.clone(),
207            target_element: target_element.clone(),
208            config: config.clone(),
209            priority: TransitionPriority::Normal,
210        };
211
212        // Add to queue
213        self.transition_queue.push_back(queued);
214
215        // Try to start transitions
216        self.process_transition_queue()?;
217
218        Ok(id)
219    }
220
221    /// Start a shared element transition
222    pub fn start_transition(&mut self, transition_id: &str) -> Result<(), String> {
223        // Find and start the transition
224        if let Some(transition) = self.active_transitions.get_mut(transition_id) {
225            transition.active = true;
226            transition.start_time = js_sys::Date::now();
227
228            // Clone transition to avoid borrow checker issues
229            let transition_clone = SharedElementTransition {
230                id: transition.id.clone(),
231                source_element: transition.source_element.clone(),
232                target_element: transition.target_element.clone(),
233                config: transition.config.clone(),
234                progress: transition.progress,
235                active: transition.active,
236                start_time: transition.start_time,
237            };
238
239            // Apply initial transforms
240            self.apply_initial_transforms(&transition_clone)?;
241
242            Ok(())
243        } else {
244            Err("Transition not found".to_string())
245        }
246    }
247
248    /// Update transition progress
249    pub fn update_transition(&mut self, transition_id: &str, progress: f64) -> Result<(), String> {
250        if let Some(transition) = self.active_transitions.get_mut(transition_id) {
251            transition.progress = progress.clamp(0.0, 1.0);
252
253            // Clone transition to avoid borrow checker issues
254            let transition_clone = SharedElementTransition {
255                id: transition.id.clone(),
256                source_element: transition.source_element.clone(),
257                target_element: transition.target_element.clone(),
258                config: transition.config.clone(),
259                progress: transition.progress,
260                active: transition.active,
261                start_time: transition.start_time,
262            };
263
264            // Apply transforms based on progress
265            self.apply_transition_transforms(&transition_clone)?;
266
267            // Check if transition is complete
268            if progress >= 1.0 {
269                self.complete_transition(transition_id)?;
270            }
271
272            Ok(())
273        } else {
274            Err("Transition not found".to_string())
275        }
276    }
277
278    /// Cancel a transition
279    pub fn cancel_transition(&mut self, transition_id: &str) -> Result<(), String> {
280        if let Some(transition) = self.active_transitions.remove(transition_id) {
281            // Reset element transforms
282            self.reset_element_transforms(&transition.source_element)?;
283            self.reset_element_transforms(&transition.target_element)?;
284
285            // Update metrics
286            self.performance_metrics.total_transitions += 1;
287
288            Ok(())
289        } else {
290            Err("Transition not found".to_string())
291        }
292    }
293
294    /// Cancel all active transitions
295    pub fn cancel_all_transitions(&mut self) {
296        self.active_transitions.clear();
297        self.transition_queue.clear();
298    }
299
300    /// Process the transition queue
301    fn process_transition_queue(&mut self) -> Result<(), String> {
302        while let Some(queued) = self.transition_queue.pop_front() {
303            // Create transition
304            let transition = SharedElementTransition {
305                id: self.generate_transition_id(),
306                source_element: queued.source_element.clone(),
307                target_element: queued.target_element.clone(),
308                config: queued.config.clone(),
309                progress: 0.0,
310                active: false,
311                start_time: 0.0,
312            };
313
314            // Add to active transitions
315            let id = transition.id.clone();
316            self.active_transitions.insert(id.clone(), transition);
317
318            // Start the transition
319            self.start_transition(&id)?;
320        }
321
322        Ok(())
323    }
324
325    /// Apply initial transforms for transition
326    fn apply_initial_transforms(&self, transition: &SharedElementTransition) -> Result<(), String> {
327        // Set up initial transform state for both elements
328        self.setup_transition_element(&transition.source_element, &transition.config)?;
329        self.setup_transition_element(&transition.target_element, &transition.config)?;
330
331        log::info!(
332            "Applied initial transforms for transition {}",
333            transition.id
334        );
335
336        Ok(())
337    }
338
339    /// Apply transforms during transition
340    fn apply_transition_transforms(
341        &self,
342        transition: &SharedElementTransition,
343    ) -> Result<(), String> {
344        // Interpolate transforms based on progress
345        let progress = transition.progress;
346        let inverse_progress = 1.0 - progress;
347
348        // Apply transforms to both source and target elements
349        if let Some(source_html) = transition.source_element.dyn_ref::<web_sys::HtmlElement>() {
350            let style = source_html.style();
351            let opacity = inverse_progress;
352            style
353                .set_property("opacity", &opacity.to_string())
354                .map_err(|_| "Failed to set source opacity")?;
355        }
356
357        if let Some(target_html) = transition.target_element.dyn_ref::<web_sys::HtmlElement>() {
358            let style = target_html.style();
359            let opacity = progress;
360            style
361                .set_property("opacity", &opacity.to_string())
362                .map_err(|_| "Failed to set target opacity")?;
363        }
364
365        log::info!(
366            "Applied transition transforms for {} at {}%",
367            transition.id,
368            (progress * 100.0) as i32
369        );
370
371        Ok(())
372    }
373
374    /// Set up an element for transition
375    fn setup_transition_element(
376        &self,
377        element: &Element,
378        _config: &SharedElementConfig,
379    ) -> Result<(), String> {
380        if let Some(html_element) = element.dyn_ref::<web_sys::HtmlElement>() {
381            let style = html_element.style();
382            style
383                .set_property("will-change", "transform, opacity")
384                .map_err(|_| "Failed to set will-change")?;
385
386            // Use default elevated z-index for transitions
387            style
388                .set_property("z-index", "9999")
389                .map_err(|_| "Failed to set elevated z-index")?;
390        } else {
391            return Err("Element is not an HtmlElement".to_string());
392        }
393
394        Ok(())
395    }
396
397    /// Complete a transition
398    fn complete_transition(&mut self, transition_id: &str) -> Result<(), String> {
399        if let Some(transition) = self.active_transitions.remove(transition_id) {
400            // Reset element transforms
401            self.reset_element_transforms(&transition.source_element)?;
402            self.reset_element_transforms(&transition.target_element)?;
403
404            // Update performance metrics
405            let duration = js_sys::Date::now() - transition.start_time;
406            self.update_performance_metrics(duration);
407
408            Ok(())
409        } else {
410            Err("Transition not found".to_string())
411        }
412    }
413
414    /// Reset element transforms
415    fn reset_element_transforms(&self, element: &Element) -> Result<(), String> {
416        // Reset CSS transforms on the element
417        if let Some(html_element) = element.dyn_ref::<web_sys::HtmlElement>() {
418            let style = html_element.style();
419            style
420                .remove_property("transform")
421                .map_err(|_| "Failed to remove transform")?;
422            style
423                .remove_property("will-change")
424                .map_err(|_| "Failed to remove will-change")?;
425            style
426                .remove_property("z-index")
427                .map_err(|_| "Failed to remove z-index")?;
428        } else {
429            return Err("Element is not an HtmlElement".to_string());
430        }
431
432        log::info!("Reset element transforms");
433        Ok(())
434    }
435
436    /// Generate unique transition ID
437    fn generate_transition_id(&self) -> String {
438        format!("shared_trans_{}", js_sys::Date::now())
439    }
440
441    /// Update performance metrics
442    fn update_performance_metrics(&mut self, duration: f64) {
443        self.performance_metrics.total_transitions += 1;
444        self.performance_metrics.successful_transitions += 1;
445
446        // Update average duration
447        let total = self.performance_metrics.total_transitions as f64;
448        let current_avg = self.performance_metrics.average_duration;
449        self.performance_metrics.average_duration =
450            (current_avg * (total - 1.0) + duration) / total;
451    }
452
453    /// Get performance metrics
454    pub fn get_performance_metrics(&self) -> &SharedElementMetrics {
455        &self.performance_metrics
456    }
457
458    /// Get active transition count
459    pub fn get_active_transition_count(&self) -> usize {
460        self.active_transitions.len()
461    }
462
463    /// Get queued transition count
464    pub fn get_queued_transition_count(&self) -> usize {
465        self.transition_queue.len()
466    }
467}
468
469impl Default for SharedElementManager {
470    fn default() -> Self {
471        Self::new(ZIndexStrategy::default())
472    }
473}
474
475impl Default for SharedElementMetrics {
476    fn default() -> Self {
477        Self {
478            total_transitions: 0,
479            successful_transitions: 0,
480            average_duration: 0.0,
481            frame_rate: 60.0,
482        }
483    }
484}
485
486impl Default for TransitionPriority {
487    fn default() -> Self {
488        Self::Normal
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495    use wasm_bindgen_test::*;
496
497    wasm_bindgen_test_configure!(run_in_browser);
498
499    #[test]
500    fn test_shared_element_config_default() {
501        let config = SharedElementConfig::default();
502        assert_eq!(config.duration, 0.3);
503        assert!(matches!(
504            config.easing,
505            crate::flip::EasingFunction::EaseOut
506        ));
507        assert!(!config.maintain_aspect_ratio);
508        assert!(config.hardware_accelerated);
509    }
510
511    #[test]
512    fn test_z_index_strategy_fixed() {
513        let strategy = ZIndexStrategy::Fixed {
514            base: 1000,
515            increment: 10,
516        };
517        match strategy {
518            ZIndexStrategy::Fixed { base, increment } => {
519                assert_eq!(base, 1000);
520                assert_eq!(increment, 10);
521            }
522            _ => panic!("Expected Fixed strategy"),
523        }
524    }
525
526    #[test]
527    fn test_z_index_strategy_dynamic() {
528        let strategy = ZIndexStrategy::Dynamic {
529            base: 500,
530            max: 2000,
531        };
532        match strategy {
533            ZIndexStrategy::Dynamic { base, max } => {
534                assert_eq!(base, 500);
535                assert_eq!(max, 2000);
536            }
537            _ => panic!("Expected Dynamic strategy"),
538        }
539    }
540
541    #[test]
542    fn test_z_index_strategy_custom_property() {
543        let strategy = ZIndexStrategy::CustomProperty {
544            property: "z-index".to_string(),
545        };
546        match strategy {
547            ZIndexStrategy::CustomProperty { property } => {
548                assert_eq!(property, "z-index");
549            }
550            _ => panic!("Expected CustomProperty strategy"),
551        }
552    }
553
554    #[test]
555    fn test_z_index_strategy_elevate() {
556        let strategy = ZIndexStrategy::Elevate;
557        match strategy {
558            ZIndexStrategy::Elevate => {}
559            _ => panic!("Expected Elevate strategy"),
560        }
561    }
562
563    #[test]
564    fn test_z_index_strategy_custom() {
565        let strategy = ZIndexStrategy::Custom(5000);
566        match strategy {
567            ZIndexStrategy::Custom(value) => assert_eq!(value, 5000),
568            _ => panic!("Expected Custom strategy"),
569        }
570    }
571
572    #[test]
573    fn test_z_index_strategy_maintain() {
574        let strategy = ZIndexStrategy::Maintain;
575        match strategy {
576            ZIndexStrategy::Maintain => {}
577            _ => panic!("Expected Maintain strategy"),
578        }
579    }
580
581    #[test]
582    fn test_z_index_strategy_default() {
583        let strategy = ZIndexStrategy::default();
584        match strategy {
585            ZIndexStrategy::Fixed { base, increment } => {
586                assert_eq!(base, 1000);
587                assert_eq!(increment, 1);
588            }
589            _ => panic!("Expected default Fixed strategy"),
590        }
591    }
592
593    #[test]
594    fn test_transition_priority_low() {
595        let priority = TransitionPriority::Low;
596        assert_eq!(priority as i32, 0);
597    }
598
599    #[test]
600    fn test_transition_priority_normal() {
601        let priority = TransitionPriority::Normal;
602        assert_eq!(priority as i32, 1);
603    }
604
605    #[test]
606    fn test_transition_priority_high() {
607        let priority = TransitionPriority::High;
608        assert_eq!(priority as i32, 2);
609    }
610
611    #[test]
612    fn test_transition_priority_critical() {
613        let priority = TransitionPriority::Critical;
614        assert_eq!(priority as i32, 3);
615    }
616
617    #[test]
618    fn test_transition_priority_default() {
619        let priority = TransitionPriority::default();
620        assert_eq!(priority, TransitionPriority::Normal);
621    }
622
623    #[test]
624    fn test_shared_element_metrics_default() {
625        let metrics = SharedElementMetrics::default();
626        assert_eq!(metrics.total_transitions, 0);
627        assert_eq!(metrics.successful_transitions, 0);
628        assert_eq!(metrics.average_duration, 0.0);
629        assert_eq!(metrics.frame_rate, 60.0);
630    }
631
632    #[test]
633    fn test_transition_record_creation() {
634        let record = TransitionRecord {
635            id: "test_trans_123".to_string(),
636            start_time: 1000.0,
637            end_time: Some(1300.0),
638            duration: 0.3,
639            success: true,
640            performance: SharedElementMetrics::default(),
641        };
642
643        assert_eq!(record.id, "test_trans_123");
644        assert_eq!(record.start_time, 1000.0);
645        assert_eq!(record.end_time, Some(1300.0));
646        assert_eq!(record.duration, 0.3);
647        assert!(record.success);
648    }
649
650    #[wasm_bindgen_test]
651    fn test_shared_element_manager_creation() {
652        let manager = SharedElementManager::new(ZIndexStrategy::default());
653        assert_eq!(manager.get_active_transition_count(), 0);
654        assert_eq!(manager.get_queued_transition_count(), 0);
655    }
656
657    #[wasm_bindgen_test]
658    fn test_shared_element_manager_with_fixed_strategy() {
659        let strategy = ZIndexStrategy::Fixed {
660            base: 1000,
661            increment: 10,
662        };
663        let manager = SharedElementManager::new(strategy);
664        assert_eq!(manager.get_active_transition_count(), 0);
665    }
666
667    #[wasm_bindgen_test]
668    fn test_shared_element_manager_with_dynamic_strategy() {
669        let strategy = ZIndexStrategy::Dynamic {
670            base: 500,
671            max: 2000,
672        };
673        let manager = SharedElementManager::new(strategy);
674        assert_eq!(manager.get_active_transition_count(), 0);
675    }
676
677    #[wasm_bindgen_test]
678    fn test_transition_priority_ordering() {
679        assert!(TransitionPriority::Low < TransitionPriority::Normal);
680        assert!(TransitionPriority::Normal < TransitionPriority::High);
681        assert!(TransitionPriority::High < TransitionPriority::Critical);
682    }
683
684    #[wasm_bindgen_test]
685    fn test_transition_id_generation() {
686        let manager = SharedElementManager::new(ZIndexStrategy::default());
687        let id1 = manager.generate_transition_id();
688        let id2 = manager.generate_transition_id();
689
690        assert_ne!(id1, id2);
691        assert!(id1.starts_with("shared_trans_"));
692        assert!(id2.starts_with("shared_trans_"));
693    }
694
695    #[wasm_bindgen_test]
696    fn test_manager_cancel_all_transitions() {
697        let mut manager = SharedElementManager::new(ZIndexStrategy::default());
698        manager.cancel_all_transitions();
699        assert_eq!(manager.get_active_transition_count(), 0);
700        assert_eq!(manager.get_queued_transition_count(), 0);
701    }
702}