1use std::collections::HashMap;
7use std::time::Instant;
8use web_sys::{DomRect, Element};
9
10#[derive(Debug, Clone)]
12pub struct LayoutChange {
13 pub element_id: String,
15 pub previous_layout: DomRect,
17 pub current_layout: DomRect,
19 pub timestamp: f64,
21 pub change_type: LayoutChangeType,
23 pub performance_impact: PerformanceImpact,
25}
26
27#[derive(Debug, Clone)]
29pub enum LayoutChangeType {
30 Position,
32 Size,
34 PositionAndSize,
36 None,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Hash)]
42pub enum PerformanceImpact {
43 Low,
45 Medium,
47 High,
49 Critical,
51}
52
53#[derive(Debug, Clone)]
55pub struct LayoutStats {
56 pub total_changes: usize,
58 pub changes_last_second: usize,
60 pub change_frequency: f64,
62 pub impact_distribution: HashMap<PerformanceImpact, usize>,
64 pub frame_rate: f64,
66}
67
68pub struct LayoutTracker {
70 tracked_elements: HashMap<String, TrackedElement>,
72 change_history: Vec<LayoutChange>,
74 performance_monitor: PerformanceMonitor,
76 stats: LayoutStats,
78 enabled: bool,
80}
81
82#[derive(Debug, Clone)]
84pub struct TrackedElement {
85 pub id: String,
87 pub element: Element,
89 pub current_layout: DomRect,
91 pub previous_layout: Option<DomRect>,
93 pub last_update: Instant,
95 pub change_count: usize,
97}
98
99pub struct PerformanceMonitor {
101 frame_times: Vec<f64>,
103 memory_usage: Vec<usize>,
105 #[allow(dead_code)]
107 performance_observer: Option<PerformanceObserver>,
108 pub last_frame_time: f64,
110}
111
112pub struct PerformanceObserver {
114 pub id: String,
116 pub callback: Box<dyn FnMut(f64)>,
118}
119
120impl LayoutTracker {
121 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 pub fn track_element(&mut self, element: Element) -> Result<(), String> {
134 let element_id = element.id();
135
136 let current_layout = element.get_bounding_client_rect();
138
139 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 self.tracked_elements.insert(element_id, tracked_element);
151
152 Ok(())
153 }
154
155 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 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 let element_ids: Vec<String> = self.tracked_elements.keys().cloned().collect();
175
176 for element_id in element_ids {
177 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 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(¤t_layout, &new_layout) {
193 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, performance_impact: PerformanceImpact::Low, };
202
203 changes.push(change.clone());
204
205 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 self.change_history.push(change);
215 }
216 }
217
218 self.performance_monitor.update()?;
220
221 self.update_stats(&changes);
223
224 self.cleanup_history();
226
227 Ok(changes)
228 }
229
230 pub fn get_element_layout(&self, element: &Element) -> Result<DomRect, String> {
232 Ok(element.get_bounding_client_rect())
233 }
234
235 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 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 pub fn get_stats(&self) -> &LayoutStats {
256 &self.stats
257 }
258
259 pub fn get_frame_rate(&self) -> f64 {
261 self.performance_monitor.get_frame_rate()
262 }
263
264 pub fn get_memory_usage(&self) -> usize {
266 self.performance_monitor.get_memory_usage()
267 }
268
269 pub fn set_enabled(&mut self, enabled: bool) {
271 self.enabled = enabled;
272 }
273
274 fn has_layout_changed(&self, old: &DomRect, new: &DomRect) -> bool {
276 const THRESHOLD: f64 = 0.1; (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 #[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 let change_type = self.determine_change_type(&previous_layout, new_layout);
296
297 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 #[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 #[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 fn update_stats(&mut self, changes: &[LayoutChange]) {
345 self.stats.total_changes += changes.len();
346
347 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 if self.stats.total_changes > 0 {
359 let total_time = current_time / 1000.0; self.stats.change_frequency = self.stats.total_changes as f64 / total_time;
361 }
362
363 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 self.stats.frame_rate = self.performance_monitor.get_frame_rate();
375 }
376
377 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 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 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 self.frame_times.push(frame_time);
406
407 if self.frame_times.len() > 60 {
409 self.frame_times.remove(0);
410 }
411
412 self.last_frame_time = current_time;
414
415 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 pub fn get_frame_rate(&self) -> f64 {
426 if self.frame_times.is_empty() {
427 return 60.0; }
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) } else {
436 60.0
437 }
438 }
439
440 pub fn get_memory_usage(&self) -> usize {
442 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 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 let change_type = tracker.determine_change_type(&old, &new_position);
506 assert!(matches!(change_type, LayoutChangeType::None));
507
508 let change_type = tracker.determine_change_type(&old, &new_size);
510 assert!(matches!(change_type, LayoutChangeType::None));
511
512 let change_type = tracker.determine_change_type(&old, &new_both);
514 assert!(matches!(change_type, LayoutChangeType::None));
515
516 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 let old = DomRect::new().unwrap();
527 let small_change = DomRect::new().unwrap();
528 let large_change = DomRect::new().unwrap();
529
530 let impact = tracker.calculate_performance_impact(&old, &small_change);
532 assert!(matches!(impact, PerformanceImpact::Low));
533
534 let impact = tracker.calculate_performance_impact(&old, &large_change);
536 assert!(matches!(impact, PerformanceImpact::Low));
537 }
538}