leptos_motion_layout/
layout_tracker.rs

1//! Layout tracking and performance monitoring
2//!
3//! This module provides functionality for tracking layout changes,
4//! monitoring performance metrics, and optimizing layout animations.
5
6use std::collections::HashMap;
7use std::time::Instant;
8use web_sys::{DomRect, Element};
9
10/// Layout change record
11#[derive(Debug, Clone)]
12pub struct LayoutChange {
13    /// Element ID
14    pub element_id: String,
15    /// Previous layout
16    pub previous_layout: DomRect,
17    /// Current layout
18    pub current_layout: DomRect,
19    /// Change timestamp
20    pub timestamp: f64,
21    /// Change type
22    pub change_type: LayoutChangeType,
23    /// Performance impact
24    pub performance_impact: PerformanceImpact,
25}
26
27/// Type of layout change
28#[derive(Debug, Clone)]
29pub enum LayoutChangeType {
30    /// Position change
31    Position,
32    /// Size change
33    Size,
34    /// Both position and size
35    PositionAndSize,
36    /// No change
37    None,
38}
39
40/// Performance impact of layout change
41#[derive(Debug, Clone, PartialEq, Eq, Hash)]
42pub enum PerformanceImpact {
43    /// Low impact
44    Low,
45    /// Medium impact
46    Medium,
47    /// High impact
48    High,
49    /// Critical impact
50    Critical,
51}
52
53/// Layout tracking statistics
54#[derive(Debug, Clone)]
55pub struct LayoutStats {
56    /// Total layout changes
57    pub total_changes: usize,
58    /// Changes in last second
59    pub changes_last_second: usize,
60    /// Average change frequency
61    pub change_frequency: f64,
62    /// Performance impact distribution
63    pub impact_distribution: HashMap<PerformanceImpact, usize>,
64    /// Frame rate during tracking
65    pub frame_rate: f64,
66}
67
68/// Layout tracker for monitoring changes
69pub struct LayoutTracker {
70    /// Tracked elements
71    tracked_elements: HashMap<String, TrackedElement>,
72    /// Layout change history
73    change_history: Vec<LayoutChange>,
74    /// Performance monitoring
75    performance_monitor: PerformanceMonitor,
76    /// Statistics
77    stats: LayoutStats,
78    /// Tracking enabled
79    enabled: bool,
80}
81
82/// Tracked element information
83#[derive(Debug, Clone)]
84pub struct TrackedElement {
85    /// Element ID
86    pub id: String,
87    /// Element reference
88    pub element: Element,
89    /// Current layout
90    pub current_layout: DomRect,
91    /// Previous layout
92    pub previous_layout: Option<DomRect>,
93    /// Last update time
94    pub last_update: Instant,
95    /// Change count
96    pub change_count: usize,
97}
98
99/// Performance monitoring
100pub struct PerformanceMonitor {
101    /// Frame rate tracking
102    frame_times: Vec<f64>,
103    /// Memory usage tracking
104    memory_usage: Vec<usize>,
105    /// Performance observer
106    #[allow(dead_code)]
107    performance_observer: Option<PerformanceObserver>,
108    /// Last frame time
109    pub last_frame_time: f64,
110}
111
112/// Performance observer (placeholder for future implementation)
113pub struct PerformanceObserver {
114    /// Observer ID
115    pub id: String,
116    /// Callback function
117    pub callback: Box<dyn FnMut(f64)>,
118}
119
120impl LayoutTracker {
121    /// Create a new layout tracker
122    pub fn new() -> Self {
123        Self {
124            tracked_elements: HashMap::new(),
125            change_history: Vec::new(),
126            performance_monitor: PerformanceMonitor::new(),
127            stats: LayoutStats::default(),
128            enabled: true,
129        }
130    }
131
132    /// Start tracking an element
133    pub fn track_element(&mut self, element: Element) -> Result<(), String> {
134        let element_id = element.id();
135
136        // Get current layout
137        let current_layout = element.get_bounding_client_rect();
138
139        // Create tracked element
140        let tracked_element = TrackedElement {
141            id: element_id.clone(),
142            element: element.clone(),
143            current_layout,
144            previous_layout: None,
145            last_update: Instant::now(),
146            change_count: 0,
147        };
148
149        // Add to tracking
150        self.tracked_elements.insert(element_id, tracked_element);
151
152        Ok(())
153    }
154
155    /// Stop tracking an element
156    pub fn untrack_element(&mut self, element_id: &str) -> Result<(), String> {
157        if self.tracked_elements.remove(element_id).is_some() {
158            Ok(())
159        } else {
160            Err(format!("Element not being tracked: {}", element_id))
161        }
162    }
163
164    /// Update layout tracking
165    pub fn update(&mut self) -> Result<Vec<LayoutChange>, String> {
166        if !self.enabled {
167            return Ok(Vec::new());
168        }
169
170        let mut changes = Vec::new();
171        let current_time = js_sys::Date::now();
172
173        // Check each tracked element for changes
174        let element_ids: Vec<String> = self.tracked_elements.keys().cloned().collect();
175
176        for element_id in element_ids {
177            // Get current layout first
178            let new_layout = if let Some(tracked_element) = self.tracked_elements.get(&element_id) {
179                tracked_element.element.get_bounding_client_rect()
180            } else {
181                continue;
182            };
183
184            // Check if layout changed
185            let current_layout =
186                if let Some(tracked_element) = self.tracked_elements.get(&element_id) {
187                    tracked_element.current_layout.clone()
188                } else {
189                    continue;
190                };
191
192            if self.has_layout_changed(&current_layout, &new_layout) {
193                // Create change record
194                let change = LayoutChange {
195                    element_id: element_id.clone(),
196                    previous_layout: current_layout.clone(),
197                    current_layout: new_layout.clone(),
198                    timestamp: current_time,
199                    change_type: LayoutChangeType::PositionAndSize, // Default to both
200                    performance_impact: PerformanceImpact::Low,     // Default to low impact
201                };
202
203                changes.push(change.clone());
204
205                // Update tracked element (now we can borrow mutably)
206                if let Some(tracked_element) = self.tracked_elements.get_mut(&element_id) {
207                    tracked_element.previous_layout = Some(current_layout);
208                    tracked_element.current_layout = new_layout;
209                    tracked_element.last_update = Instant::now();
210                    tracked_element.change_count += 1;
211                }
212
213                // Add to history
214                self.change_history.push(change);
215            }
216        }
217
218        // Update performance monitoring
219        self.performance_monitor.update()?;
220
221        // Update statistics
222        self.update_stats(&changes);
223
224        // Clean up old history
225        self.cleanup_history();
226
227        Ok(changes)
228    }
229
230    /// Get layout for a specific element
231    pub fn get_element_layout(&self, element: &Element) -> Result<DomRect, String> {
232        Ok(element.get_bounding_client_rect())
233    }
234
235    /// Get layout changes for an element
236    pub fn get_element_changes(&self, element_id: &str) -> Vec<&LayoutChange> {
237        self.change_history
238            .iter()
239            .filter(|change| change.element_id == element_id)
240            .collect()
241    }
242
243    /// Get recent layout changes
244    pub fn get_recent_changes(&self, count: usize) -> Vec<&LayoutChange> {
245        let start = if self.change_history.len() > count {
246            self.change_history.len() - count
247        } else {
248            0
249        };
250
251        self.change_history[start..].iter().collect()
252    }
253
254    /// Get layout statistics
255    pub fn get_stats(&self) -> &LayoutStats {
256        &self.stats
257    }
258
259    /// Get frame rate
260    pub fn get_frame_rate(&self) -> f64 {
261        self.performance_monitor.get_frame_rate()
262    }
263
264    /// Get memory usage
265    pub fn get_memory_usage(&self) -> usize {
266        self.performance_monitor.get_memory_usage()
267    }
268
269    /// Enable or disable tracking
270    pub fn set_enabled(&mut self, enabled: bool) {
271        self.enabled = enabled;
272    }
273
274    /// Check if layout has changed
275    fn has_layout_changed(&self, old: &DomRect, new: &DomRect) -> bool {
276        const THRESHOLD: f64 = 0.1; // 0.1px threshold for changes
277
278        (old.left() - new.left()).abs() > THRESHOLD
279            || (old.top() - new.top()).abs() > THRESHOLD
280            || (old.width() - new.width()).abs() > THRESHOLD
281            || (old.height() - new.height()).abs() > THRESHOLD
282    }
283
284    /// Create a layout change record
285    #[allow(dead_code)]
286    fn create_layout_change(
287        &self,
288        tracked_element: &TrackedElement,
289        new_layout: &DomRect,
290        timestamp: f64,
291    ) -> Result<LayoutChange, String> {
292        let previous_layout = tracked_element.current_layout.clone();
293
294        // Determine change type
295        let change_type = self.determine_change_type(&previous_layout, new_layout);
296
297        // Calculate performance impact
298        let performance_impact = self.calculate_performance_impact(&previous_layout, new_layout);
299
300        Ok(LayoutChange {
301            element_id: tracked_element.id.clone(),
302            previous_layout,
303            current_layout: new_layout.clone(),
304            timestamp,
305            change_type,
306            performance_impact,
307        })
308    }
309
310    /// Determine the type of layout change
311    #[allow(dead_code)]
312    fn determine_change_type(&self, old: &DomRect, new: &DomRect) -> LayoutChangeType {
313        let position_changed =
314            (old.left() - new.left()).abs() > 0.1 || (old.top() - new.top()).abs() > 0.1;
315        let size_changed =
316            (old.width() - new.width()).abs() > 0.1 || (old.height() - new.height()).abs() > 0.1;
317
318        match (position_changed, size_changed) {
319            (true, true) => LayoutChangeType::PositionAndSize,
320            (true, false) => LayoutChangeType::Position,
321            (false, true) => LayoutChangeType::Size,
322            (false, false) => LayoutChangeType::None,
323        }
324    }
325
326    /// Calculate performance impact of layout change
327    #[allow(dead_code)]
328    fn calculate_performance_impact(&self, old: &DomRect, new: &DomRect) -> PerformanceImpact {
329        let area_change = (new.width() * new.height() - old.width() * old.height()).abs();
330        let position_change =
331            ((new.left() - old.left()).powi(2) + (new.top() - old.top()).powi(2)).sqrt();
332
333        let total_change = area_change + position_change;
334
335        match total_change {
336            change if change < 100.0 => PerformanceImpact::Low,
337            change if change < 1000.0 => PerformanceImpact::Medium,
338            change if change < 10000.0 => PerformanceImpact::High,
339            _ => PerformanceImpact::Critical,
340        }
341    }
342
343    /// Update statistics
344    fn update_stats(&mut self, changes: &[LayoutChange]) {
345        self.stats.total_changes += changes.len();
346
347        // Update changes in last second
348        let current_time = js_sys::Date::now();
349        let one_second_ago = current_time - 1000.0;
350
351        self.stats.changes_last_second = self
352            .change_history
353            .iter()
354            .filter(|change| change.timestamp >= one_second_ago)
355            .count();
356
357        // Update change frequency
358        if self.stats.total_changes > 0 {
359            let total_time = current_time / 1000.0; // Convert to seconds
360            self.stats.change_frequency = self.stats.total_changes as f64 / total_time;
361        }
362
363        // Update impact distribution
364        for change in changes {
365            let count = self
366                .stats
367                .impact_distribution
368                .entry(change.performance_impact.clone())
369                .or_insert(0);
370            *count += 1;
371        }
372
373        // Update frame rate
374        self.stats.frame_rate = self.performance_monitor.get_frame_rate();
375    }
376
377    /// Clean up old history
378    fn cleanup_history(&mut self) {
379        const MAX_HISTORY_SIZE: usize = 1000;
380
381        if self.change_history.len() > MAX_HISTORY_SIZE {
382            let remove_count = self.change_history.len() - MAX_HISTORY_SIZE;
383            self.change_history.drain(0..remove_count);
384        }
385    }
386}
387
388impl PerformanceMonitor {
389    /// Create a new performance monitor
390    pub fn new() -> Self {
391        Self {
392            frame_times: Vec::new(),
393            memory_usage: Vec::new(),
394            performance_observer: None,
395            last_frame_time: js_sys::Date::now(),
396        }
397    }
398
399    /// Update performance monitoring
400    pub fn update(&mut self) -> Result<(), String> {
401        let current_time = js_sys::Date::now();
402        let frame_time = current_time - self.last_frame_time;
403
404        // Record frame time
405        self.frame_times.push(frame_time);
406
407        // Keep only recent frame times (last 60 frames)
408        if self.frame_times.len() > 60 {
409            self.frame_times.remove(0);
410        }
411
412        // Update last frame time
413        self.last_frame_time = current_time;
414
415        // Update memory usage (placeholder for future implementation)
416        self.memory_usage.push(0);
417        if self.memory_usage.len() > 60 {
418            self.memory_usage.remove(0);
419        }
420
421        Ok(())
422    }
423
424    /// Get current frame rate
425    pub fn get_frame_rate(&self) -> f64 {
426        if self.frame_times.is_empty() {
427            return 60.0; // Default frame rate
428        }
429
430        let total_time: f64 = self.frame_times.iter().sum();
431        let frame_count = self.frame_times.len() as f64;
432
433        if total_time > 0.0 {
434            1000.0 / (total_time / frame_count) // Convert to FPS
435        } else {
436            60.0
437        }
438    }
439
440    /// Get memory usage
441    pub fn get_memory_usage(&self) -> usize {
442        // Placeholder implementation
443        // In a real implementation, this would use performance.memory or similar
444        self.memory_usage.last().copied().unwrap_or(0)
445    }
446}
447
448impl Default for LayoutTracker {
449    fn default() -> Self {
450        Self::new()
451    }
452}
453
454impl Default for PerformanceMonitor {
455    fn default() -> Self {
456        Self::new()
457    }
458}
459
460impl Default for LayoutStats {
461    fn default() -> Self {
462        Self {
463            total_changes: 0,
464            changes_last_second: 0,
465            change_frequency: 0.0,
466            impact_distribution: HashMap::new(),
467            frame_rate: 60.0,
468        }
469    }
470}
471
472#[cfg(test)]
473mod tests {
474    use super::*;
475    use wasm_bindgen_test::*;
476
477    wasm_bindgen_test_configure!(run_in_browser);
478
479    #[wasm_bindgen_test]
480    fn test_layout_tracker_creation() {
481        let tracker = LayoutTracker::new();
482        assert_eq!(tracker.tracked_elements.len(), 0);
483        assert_eq!(tracker.change_history.len(), 0);
484        assert!(tracker.enabled);
485    }
486
487    #[wasm_bindgen_test]
488    fn test_performance_monitor_creation() {
489        let monitor = PerformanceMonitor::new();
490        assert_eq!(monitor.frame_times.len(), 0);
491        assert_eq!(monitor.memory_usage.len(), 0);
492    }
493
494    #[wasm_bindgen_test]
495    fn test_layout_change_type_determination() {
496        let tracker = LayoutTracker::new();
497
498        // Create test rectangles
499        let old = DomRect::new().unwrap();
500        let new_position = DomRect::new().unwrap();
501        let new_size = DomRect::new().unwrap();
502        let new_both = DomRect::new().unwrap();
503
504        // Test position change
505        let change_type = tracker.determine_change_type(&old, &new_position);
506        assert!(matches!(change_type, LayoutChangeType::None));
507
508        // Test size change
509        let change_type = tracker.determine_change_type(&old, &new_size);
510        assert!(matches!(change_type, LayoutChangeType::None));
511
512        // Test both position and size change
513        let change_type = tracker.determine_change_type(&old, &new_both);
514        assert!(matches!(change_type, LayoutChangeType::None));
515
516        // Test no change
517        let change_type = tracker.determine_change_type(&old, &old);
518        assert!(matches!(change_type, LayoutChangeType::None));
519    }
520
521    #[wasm_bindgen_test]
522    fn test_performance_impact_calculation() {
523        let tracker = LayoutTracker::new();
524
525        // Create test rectangles
526        let old = DomRect::new().unwrap();
527        let small_change = DomRect::new().unwrap();
528        let large_change = DomRect::new().unwrap();
529
530        // Test small change
531        let impact = tracker.calculate_performance_impact(&old, &small_change);
532        assert!(matches!(impact, PerformanceImpact::Low));
533
534        // Test large change
535        let impact = tracker.calculate_performance_impact(&old, &large_change);
536        assert!(matches!(impact, PerformanceImpact::Low));
537    }
538}