1#![forbid(unsafe_code)]
2
3use crate::{Alignment, Constraint, Direction, Sides};
34use ftui_core::geometry::Rect;
35use std::fmt::Write as _;
36use std::sync::atomic::{AtomicBool, Ordering};
37use std::sync::{Arc, Mutex};
38use std::time::Duration;
39
40#[derive(Debug, Clone)]
42pub struct LayoutRecord {
43 pub name: String,
45 pub constraints: Vec<Constraint>,
47 pub available_size: u16,
49 pub computed_sizes: Vec<u16>,
51 pub direction: Direction,
53 pub alignment: Alignment,
55 pub margin: Sides,
57 pub gap: u16,
59 pub input_area: Rect,
61 pub result_rects: Vec<Rect>,
63 pub solve_time: Option<Duration>,
65 pub parent_index: Option<usize>,
67}
68
69impl LayoutRecord {
70 pub fn new(name: impl Into<String>) -> Self {
72 Self {
73 name: name.into(),
74 constraints: Vec::new(),
75 available_size: 0,
76 computed_sizes: Vec::new(),
77 direction: Direction::default(),
78 alignment: Alignment::default(),
79 margin: Sides::default(),
80 gap: 0,
81 input_area: Rect::default(),
82 result_rects: Vec::new(),
83 solve_time: None,
84 parent_index: None,
85 }
86 }
87
88 pub fn has_overflow(&self) -> bool {
90 let total_computed: u16 = self.computed_sizes.iter().sum();
91 let total_gaps = if self.computed_sizes.len() > 1 {
92 self.gap
93 .saturating_mul((self.computed_sizes.len() - 1) as u16)
94 } else {
95 0
96 };
97 total_computed.saturating_add(total_gaps) > self.available_size
98 }
99
100 pub fn has_underflow(&self) -> bool {
104 let total_computed: u16 = self.computed_sizes.iter().sum();
105 let total_gaps = if self.computed_sizes.len() > 1 {
106 self.gap
107 .saturating_mul((self.computed_sizes.len() - 1) as u16)
108 } else {
109 0
110 };
111 let total_used = total_computed.saturating_add(total_gaps);
112 let unused = self.available_size.saturating_sub(total_used);
113 self.available_size > 0 && (unused as f32 / self.available_size as f32) > 0.2
115 }
116
117 pub fn utilization(&self) -> f32 {
119 if self.available_size == 0 {
120 return 0.0;
121 }
122 let total_computed: u16 = self.computed_sizes.iter().sum();
123 let total_gaps = if self.computed_sizes.len() > 1 {
124 self.gap
125 .saturating_mul((self.computed_sizes.len() - 1) as u16)
126 } else {
127 0
128 };
129 let total_used = total_computed.saturating_add(total_gaps);
130 (total_used as f32 / self.available_size as f32).min(1.0) * 100.0
131 }
132
133 fn format_constraint(c: &Constraint) -> String {
135 match c {
136 Constraint::Fixed(n) => format!("Fixed({n})"),
137 Constraint::Percentage(p) => format!("Pct({p:.0}%)"),
138 Constraint::Min(n) => format!("Min({n})"),
139 Constraint::Max(n) => format!("Max({n})"),
140 Constraint::Ratio(n, d) => format!("Ratio({n}/{d})"),
141 Constraint::Fill => "Fill".to_string(),
142 Constraint::FitContent => "FitContent".to_string(),
143 Constraint::FitContentBounded { min, max } => format!("FitContent({min}..{max})"),
144 Constraint::FitMin => "FitMin".to_string(),
145 }
146 }
147
148 pub fn summary(&self) -> String {
150 let mut s = String::new();
151 let _ = writeln!(s, "{} ({:?}):", self.name, self.direction);
152 let _ = writeln!(
153 s,
154 " Input: {}x{} at ({},{})",
155 self.input_area.width, self.input_area.height, self.input_area.x, self.input_area.y
156 );
157 let _ = writeln!(s, " Available: {} (after margin)", self.available_size);
158 let _ = writeln!(s, " Gap: {}", self.gap);
159
160 for (i, (constraint, size)) in self
161 .constraints
162 .iter()
163 .zip(self.computed_sizes.iter())
164 .enumerate()
165 {
166 let constraint_str = Self::format_constraint(constraint);
167 let rect = self.result_rects.get(i);
168 let rect_str = rect.map_or_else(
169 || "?".to_string(),
170 |r| format!("({},{} {}x{})", r.x, r.y, r.width, r.height),
171 );
172 let _ = writeln!(s, " [{i}] {constraint_str} -> {size} @ {rect_str}");
173 }
174
175 let _ = writeln!(s, " Utilization: {:.1}%", self.utilization());
176 if self.has_overflow() {
177 let _ = writeln!(s, " ⚠ OVERFLOW");
178 }
179 if self.has_underflow() {
180 let _ = writeln!(s, " ⚠ UNDERFLOW (>20% unused)");
181 }
182 if let Some(t) = self.solve_time {
183 let _ = writeln!(s, " Solve time: {:?}", t);
184 }
185 s
186 }
187
188 #[must_use]
192 pub fn to_jsonl(&self) -> String {
193 let constraints_json: Vec<String> = self
194 .constraints
195 .iter()
196 .map(|c| format!("\"{}\"", Self::format_constraint(c)))
197 .collect();
198 let sizes_json: Vec<String> = self.computed_sizes.iter().map(|s| s.to_string()).collect();
199 let solve_time_us = self.solve_time.map(|d| d.as_micros() as u64).unwrap_or(0);
200
201 format!(
202 r#"{{"event":"layout_solve","name":"{}","direction":"{:?}","alignment":"{:?}","available_size":{},"gap":{},"margin":{{"top":{},"right":{},"bottom":{},"left":{}}},"constraints":[{}],"computed_sizes":[{}],"utilization":{:.1},"has_overflow":{},"has_underflow":{},"solve_time_us":{}}}"#,
203 self.name,
204 self.direction,
205 self.alignment,
206 self.available_size,
207 self.gap,
208 self.margin.top,
209 self.margin.right,
210 self.margin.bottom,
211 self.margin.left,
212 constraints_json.join(","),
213 sizes_json.join(","),
214 self.utilization(),
215 self.has_overflow(),
216 self.has_underflow(),
217 solve_time_us
218 )
219 }
220}
221
222#[derive(Debug, Clone)]
224pub struct GridLayoutRecord {
225 pub name: String,
227 pub row_constraints: Vec<Constraint>,
229 pub col_constraints: Vec<Constraint>,
231 pub available_width: u16,
233 pub available_height: u16,
235 pub row_heights: Vec<u16>,
237 pub col_widths: Vec<u16>,
239 pub input_area: Rect,
241 pub solve_time: Option<Duration>,
243}
244
245impl GridLayoutRecord {
246 pub fn new(name: impl Into<String>) -> Self {
248 Self {
249 name: name.into(),
250 row_constraints: Vec::new(),
251 col_constraints: Vec::new(),
252 available_width: 0,
253 available_height: 0,
254 row_heights: Vec::new(),
255 col_widths: Vec::new(),
256 input_area: Rect::default(),
257 solve_time: None,
258 }
259 }
260
261 pub fn has_row_overflow(&self) -> bool {
263 self.row_heights.iter().sum::<u16>() > self.available_height
264 }
265
266 pub fn has_col_overflow(&self) -> bool {
268 self.col_widths.iter().sum::<u16>() > self.available_width
269 }
270
271 #[must_use]
273 pub fn to_jsonl(&self) -> String {
274 let row_heights_json: Vec<String> =
275 self.row_heights.iter().map(|h| h.to_string()).collect();
276 let col_widths_json: Vec<String> = self.col_widths.iter().map(|w| w.to_string()).collect();
277 let solve_time_us = self.solve_time.map(|d| d.as_micros() as u64).unwrap_or(0);
278
279 format!(
280 r#"{{"event":"grid_layout_solve","name":"{}","available_width":{},"available_height":{},"row_heights":[{}],"col_widths":[{}],"has_row_overflow":{},"has_col_overflow":{},"solve_time_us":{}}}"#,
281 self.name,
282 self.available_width,
283 self.available_height,
284 row_heights_json.join(","),
285 col_widths_json.join(","),
286 self.has_row_overflow(),
287 self.has_col_overflow(),
288 solve_time_us
289 )
290 }
291}
292
293type LayoutHook = Box<dyn Fn(&LayoutRecord) + Send + Sync>;
315type GridHook = Box<dyn Fn(&GridLayoutRecord) + Send + Sync>;
316
317pub struct LayoutTelemetryHooks {
318 on_layout_solve: Option<LayoutHook>,
319 on_grid_solve: Option<GridHook>,
320 on_overflow: Option<LayoutHook>,
321 on_underflow: Option<LayoutHook>,
322}
323
324impl Default for LayoutTelemetryHooks {
325 fn default() -> Self {
326 Self::new()
327 }
328}
329
330impl std::fmt::Debug for LayoutTelemetryHooks {
331 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332 f.debug_struct("LayoutTelemetryHooks")
333 .field("on_layout_solve", &self.on_layout_solve.is_some())
334 .field("on_grid_solve", &self.on_grid_solve.is_some())
335 .field("on_overflow", &self.on_overflow.is_some())
336 .field("on_underflow", &self.on_underflow.is_some())
337 .finish()
338 }
339}
340
341impl LayoutTelemetryHooks {
342 #[must_use]
344 pub fn new() -> Self {
345 Self {
346 on_layout_solve: None,
347 on_grid_solve: None,
348 on_overflow: None,
349 on_underflow: None,
350 }
351 }
352
353 #[must_use]
355 pub fn on_layout_solve<F>(mut self, f: F) -> Self
356 where
357 F: Fn(&LayoutRecord) + Send + Sync + 'static,
358 {
359 self.on_layout_solve = Some(Box::new(f));
360 self
361 }
362
363 #[must_use]
365 pub fn on_grid_solve<F>(mut self, f: F) -> Self
366 where
367 F: Fn(&GridLayoutRecord) + Send + Sync + 'static,
368 {
369 self.on_grid_solve = Some(Box::new(f));
370 self
371 }
372
373 #[must_use]
375 pub fn on_overflow<F>(mut self, f: F) -> Self
376 where
377 F: Fn(&LayoutRecord) + Send + Sync + 'static,
378 {
379 self.on_overflow = Some(Box::new(f));
380 self
381 }
382
383 #[must_use]
385 pub fn on_underflow<F>(mut self, f: F) -> Self
386 where
387 F: Fn(&LayoutRecord) + Send + Sync + 'static,
388 {
389 self.on_underflow = Some(Box::new(f));
390 self
391 }
392
393 pub fn fire_layout_solve(&self, record: &LayoutRecord) {
395 if let Some(ref f) = self.on_layout_solve {
396 f(record);
397 }
398 }
399
400 pub fn fire_grid_solve(&self, record: &GridLayoutRecord) {
402 if let Some(ref f) = self.on_grid_solve {
403 f(record);
404 }
405 }
406
407 pub fn fire_overflow(&self, record: &LayoutRecord) {
409 if let Some(ref f) = self.on_overflow {
410 f(record);
411 }
412 }
413
414 pub fn fire_underflow(&self, record: &LayoutRecord) {
416 if let Some(ref f) = self.on_underflow {
417 f(record);
418 }
419 }
420}
421
422pub struct LayoutDebugger {
429 enabled: AtomicBool,
430 records: Mutex<Vec<LayoutRecord>>,
431 grid_records: Mutex<Vec<GridLayoutRecord>>,
432 telemetry_hooks: Mutex<Option<LayoutTelemetryHooks>>,
433}
434
435impl std::fmt::Debug for LayoutDebugger {
436 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
437 f.debug_struct("LayoutDebugger")
438 .field("enabled", &self.enabled.load(Ordering::Relaxed))
439 .field(
440 "records_count",
441 &self.records.lock().map(|r| r.len()).unwrap_or(0),
442 )
443 .field(
444 "grid_records_count",
445 &self.grid_records.lock().map(|r| r.len()).unwrap_or(0),
446 )
447 .field(
448 "has_telemetry_hooks",
449 &self
450 .telemetry_hooks
451 .lock()
452 .map(|h| h.is_some())
453 .unwrap_or(false),
454 )
455 .finish()
456 }
457}
458
459impl LayoutDebugger {
460 pub fn new() -> Arc<Self> {
462 Arc::new(Self {
463 enabled: AtomicBool::new(false),
464 records: Mutex::new(Vec::new()),
465 grid_records: Mutex::new(Vec::new()),
466 telemetry_hooks: Mutex::new(None),
467 })
468 }
469
470 pub fn set_telemetry_hooks(&self, hooks: LayoutTelemetryHooks) {
472 if let Ok(mut h) = self.telemetry_hooks.lock() {
473 *h = Some(hooks);
474 }
475 }
476
477 pub fn clear_telemetry_hooks(&self) {
479 if let Ok(mut h) = self.telemetry_hooks.lock() {
480 *h = None;
481 }
482 }
483
484 #[inline]
486 pub fn enabled(&self) -> bool {
487 self.enabled.load(Ordering::Relaxed)
488 }
489
490 pub fn set_enabled(&self, enabled: bool) {
492 self.enabled.store(enabled, Ordering::Relaxed);
493 }
494
495 pub fn toggle(&self) -> bool {
497 !self.enabled.fetch_xor(true, Ordering::Relaxed)
498 }
499
500 pub fn clear(&self) {
502 if let Ok(mut records) = self.records.lock() {
503 records.clear();
504 }
505 if let Ok(mut grid_records) = self.grid_records.lock() {
506 grid_records.clear();
507 }
508 }
509
510 pub fn record(&self, record: LayoutRecord) {
517 if !self.enabled() {
518 return;
519 }
520
521 if let Ok(hooks) = self.telemetry_hooks.lock()
523 && let Some(ref h) = *hooks
524 {
525 h.fire_layout_solve(&record);
526 if record.has_overflow() {
527 h.fire_overflow(&record);
528 }
529 if record.has_underflow() {
530 h.fire_underflow(&record);
531 }
532 }
533
534 if let Ok(mut records) = self.records.lock() {
535 records.push(record);
536 }
537 }
538
539 pub fn record_grid(&self, record: GridLayoutRecord) {
543 if !self.enabled() {
544 return;
545 }
546
547 if let Ok(hooks) = self.telemetry_hooks.lock()
549 && let Some(ref h) = *hooks
550 {
551 h.fire_grid_solve(&record);
552 }
553
554 if let Ok(mut grid_records) = self.grid_records.lock() {
555 grid_records.push(record);
556 }
557 }
558
559 pub fn snapshot(&self) -> Vec<LayoutRecord> {
561 self.records
562 .lock()
563 .ok()
564 .map(|r| r.clone())
565 .unwrap_or_default()
566 }
567
568 pub fn snapshot_grids(&self) -> Vec<GridLayoutRecord> {
570 self.grid_records
571 .lock()
572 .ok()
573 .map(|r| r.clone())
574 .unwrap_or_default()
575 }
576
577 pub fn overflows(&self) -> Vec<LayoutRecord> {
579 self.snapshot()
580 .into_iter()
581 .filter(|r| r.has_overflow())
582 .collect()
583 }
584
585 pub fn underflows(&self) -> Vec<LayoutRecord> {
587 self.snapshot()
588 .into_iter()
589 .filter(|r| r.has_underflow())
590 .collect()
591 }
592
593 pub fn report(&self) -> String {
595 let records = self.snapshot();
596 let grid_records = self.snapshot_grids();
597
598 let mut s = String::new();
599 let _ = writeln!(
600 s,
601 "=== Layout Debug Report ({} flex, {} grid) ===",
602 records.len(),
603 grid_records.len()
604 );
605
606 let overflows: Vec<_> = records.iter().filter(|r| r.has_overflow()).collect();
607 let underflows: Vec<_> = records.iter().filter(|r| r.has_underflow()).collect();
608
609 if !overflows.is_empty() {
610 let _ = writeln!(s, "\n⚠ {} layouts have OVERFLOW:", overflows.len());
611 for r in &overflows {
612 let _ = writeln!(s, " - {}", r.name);
613 }
614 }
615
616 if !underflows.is_empty() {
617 let _ = writeln!(s, "\n⚠ {} layouts have UNDERFLOW:", underflows.len());
618 for r in &underflows {
619 let _ = writeln!(s, " - {} ({:.1}% utilization)", r.name, r.utilization());
620 }
621 }
622
623 let _ = writeln!(s, "\n--- Flex Layouts ---");
624 for record in &records {
625 let _ = write!(s, "\n{}", record.summary());
626 }
627
628 if !grid_records.is_empty() {
629 let _ = writeln!(s, "\n--- Grid Layouts ---");
630 for record in &grid_records {
631 let _ = writeln!(s, "\n{} (Grid):", record.name);
632 let _ = writeln!(
633 s,
634 " Input: {}x{}",
635 record.input_area.width, record.input_area.height
636 );
637 let _ = writeln!(s, " Rows: {:?}", record.row_heights);
638 let _ = writeln!(s, " Cols: {:?}", record.col_widths);
639 if record.has_row_overflow() {
640 let _ = writeln!(s, " ⚠ ROW OVERFLOW");
641 }
642 if record.has_col_overflow() {
643 let _ = writeln!(s, " ⚠ COLUMN OVERFLOW");
644 }
645 }
646 }
647
648 s
649 }
650
651 pub fn export_dot(&self) -> String {
656 let records = self.snapshot();
657
658 let mut s = String::new();
659 let _ = writeln!(s, "digraph LayoutDebug {{");
660 let _ = writeln!(s, " rankdir=TB;");
661 let _ = writeln!(s, " node [shape=record];");
662
663 for (i, r) in records.iter().enumerate() {
664 let color = if r.has_overflow() {
665 "red"
666 } else if r.has_underflow() {
667 "yellow"
668 } else {
669 "green"
670 };
671
672 let label = format!(
673 "{}|dir: {:?}|avail: {}|util: {:.0}%",
674 r.name,
675 r.direction,
676 r.available_size,
677 r.utilization()
678 );
679
680 let _ = writeln!(
681 s,
682 " n{} [label=\"{{{}}}\", color=\"{}\"];",
683 i, label, color
684 );
685
686 if let Some(parent) = r.parent_index {
687 let _ = writeln!(s, " n{} -> n{};", parent, i);
688 }
689 }
690
691 let _ = writeln!(s, "}}");
692 s
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699
700 #[test]
701 fn layout_record_overflow_detection() {
702 let mut record = LayoutRecord::new("test");
703 record.available_size = 100;
704 record.computed_sizes = vec![60, 60];
705 record.gap = 0;
706
707 assert!(record.has_overflow());
708 }
709
710 #[test]
711 fn layout_record_no_overflow() {
712 let mut record = LayoutRecord::new("test");
713 record.available_size = 100;
714 record.computed_sizes = vec![40, 40];
715 record.gap = 0;
716
717 assert!(!record.has_overflow());
718 }
719
720 #[test]
721 fn layout_record_overflow_with_gaps() {
722 let mut record = LayoutRecord::new("test");
723 record.available_size = 100;
724 record.computed_sizes = vec![45, 45];
725 record.gap = 15; assert!(record.has_overflow());
728 }
729
730 #[test]
731 fn layout_record_underflow_detection() {
732 let mut record = LayoutRecord::new("test");
733 record.available_size = 100;
734 record.computed_sizes = vec![20, 20]; record.gap = 0;
736
737 assert!(record.has_underflow());
738 }
739
740 #[test]
741 fn layout_record_no_underflow() {
742 let mut record = LayoutRecord::new("test");
743 record.available_size = 100;
744 record.computed_sizes = vec![40, 45]; record.gap = 0;
746
747 assert!(!record.has_underflow());
748 }
749
750 #[test]
751 fn layout_record_utilization() {
752 let mut record = LayoutRecord::new("test");
753 record.available_size = 100;
754 record.computed_sizes = vec![25, 25];
755 record.gap = 0;
756
757 assert!((record.utilization() - 50.0).abs() < 0.1);
758 }
759
760 #[test]
761 fn layout_record_utilization_with_gap() {
762 let mut record = LayoutRecord::new("test");
763 record.available_size = 100;
764 record.computed_sizes = vec![20, 20];
765 record.gap = 10; assert!((record.utilization() - 50.0).abs() < 0.1);
768 }
769
770 #[test]
771 fn layout_record_utilization_clamped() {
772 let mut record = LayoutRecord::new("test");
773 record.available_size = 100;
774 record.computed_sizes = vec![150]; assert!((record.utilization() - 100.0).abs() < 0.1);
778 }
779
780 #[test]
781 fn layout_record_zero_available() {
782 let mut record = LayoutRecord::new("test");
783 record.available_size = 0;
784 record.computed_sizes = vec![];
785 record.gap = 0;
786
787 assert!(!record.has_overflow());
788 assert!(!record.has_underflow());
789 assert!((record.utilization() - 0.0).abs() < 0.1);
790 }
791
792 #[test]
793 fn layout_record_summary() {
794 let mut record = LayoutRecord::new("main_layout");
795 record.constraints = vec![Constraint::Fixed(30), Constraint::Min(10)];
796 record.available_size = 100;
797 record.computed_sizes = vec![30, 70];
798 record.direction = Direction::Horizontal;
799 record.input_area = Rect::new(0, 0, 100, 50);
800 record.result_rects = vec![Rect::new(0, 0, 30, 50), Rect::new(30, 0, 70, 50)];
801
802 let summary = record.summary();
803 assert!(summary.contains("main_layout"));
804 assert!(summary.contains("Horizontal"));
805 assert!(summary.contains("Fixed(30)"));
806 assert!(summary.contains("Min(10)"));
807 }
808
809 #[test]
810 fn debugger_disabled_by_default() {
811 let debugger = LayoutDebugger::new();
812 assert!(!debugger.enabled());
813 }
814
815 #[test]
816 fn debugger_enable_disable() {
817 let debugger = LayoutDebugger::new();
818 debugger.set_enabled(true);
819 assert!(debugger.enabled());
820 debugger.set_enabled(false);
821 assert!(!debugger.enabled());
822 }
823
824 #[test]
825 fn debugger_toggle() {
826 let debugger = LayoutDebugger::new();
827 assert!(!debugger.enabled());
828 let result = debugger.toggle();
829 assert!(result);
830 assert!(debugger.enabled());
831 let result = debugger.toggle();
832 assert!(!result);
833 assert!(!debugger.enabled());
834 }
835
836 #[test]
837 fn debugger_record_when_disabled() {
838 let debugger = LayoutDebugger::new();
839 debugger.record(LayoutRecord::new("test"));
840 assert!(debugger.snapshot().is_empty());
841 }
842
843 #[test]
844 fn debugger_record_when_enabled() {
845 let debugger = LayoutDebugger::new();
846 debugger.set_enabled(true);
847 debugger.record(LayoutRecord::new("test"));
848 let records = debugger.snapshot();
849 assert_eq!(records.len(), 1);
850 assert_eq!(records[0].name, "test");
851 }
852
853 #[test]
854 fn debugger_clear() {
855 let debugger = LayoutDebugger::new();
856 debugger.set_enabled(true);
857 debugger.record(LayoutRecord::new("test1"));
858 debugger.record(LayoutRecord::new("test2"));
859 assert_eq!(debugger.snapshot().len(), 2);
860
861 debugger.clear();
862 assert!(debugger.snapshot().is_empty());
863 }
864
865 #[test]
866 fn debugger_overflows() {
867 let debugger = LayoutDebugger::new();
868 debugger.set_enabled(true);
869
870 let mut overflow_record = LayoutRecord::new("overflow");
871 overflow_record.available_size = 100;
872 overflow_record.computed_sizes = vec![60, 60];
873 debugger.record(overflow_record);
874
875 let mut normal_record = LayoutRecord::new("normal");
876 normal_record.available_size = 100;
877 normal_record.computed_sizes = vec![30, 30];
878 debugger.record(normal_record);
879
880 let overflows = debugger.overflows();
881 assert_eq!(overflows.len(), 1);
882 assert_eq!(overflows[0].name, "overflow");
883 }
884
885 #[test]
886 fn debugger_underflows() {
887 let debugger = LayoutDebugger::new();
888 debugger.set_enabled(true);
889
890 let mut underflow_record = LayoutRecord::new("underflow");
891 underflow_record.available_size = 100;
892 underflow_record.computed_sizes = vec![10, 10]; debugger.record(underflow_record);
894
895 let mut normal_record = LayoutRecord::new("normal");
896 normal_record.available_size = 100;
897 normal_record.computed_sizes = vec![45, 45]; debugger.record(normal_record);
899
900 let underflows = debugger.underflows();
901 assert_eq!(underflows.len(), 1);
902 assert_eq!(underflows[0].name, "underflow");
903 }
904
905 #[test]
906 fn debugger_report() {
907 let debugger = LayoutDebugger::new();
908 debugger.set_enabled(true);
909
910 let mut record = LayoutRecord::new("test_layout");
911 record.available_size = 100;
912 record.computed_sizes = vec![50, 50];
913 record.direction = Direction::Horizontal;
914 debugger.record(record);
915
916 let report = debugger.report();
917 assert!(report.contains("Layout Debug Report"));
918 assert!(report.contains("test_layout"));
919 }
920
921 #[test]
922 fn debugger_export_dot() {
923 let debugger = LayoutDebugger::new();
924 debugger.set_enabled(true);
925
926 let mut record = LayoutRecord::new("root");
927 record.available_size = 100;
928 record.computed_sizes = vec![50, 50];
929 record.direction = Direction::Vertical;
930 debugger.record(record);
931
932 let mut child = LayoutRecord::new("child");
933 child.available_size = 50;
934 child.computed_sizes = vec![25, 25];
935 child.parent_index = Some(0);
936 debugger.record(child);
937
938 let dot = debugger.export_dot();
939 assert!(dot.contains("digraph LayoutDebug"));
940 assert!(dot.contains("root"));
941 assert!(dot.contains("child"));
942 assert!(dot.contains("n0 -> n1")); }
944
945 #[test]
946 fn debugger_export_dot_colors() {
947 let debugger = LayoutDebugger::new();
948 debugger.set_enabled(true);
949
950 let mut overflow = LayoutRecord::new("overflow");
951 overflow.available_size = 100;
952 overflow.computed_sizes = vec![120];
953 debugger.record(overflow);
954
955 let mut underflow = LayoutRecord::new("underflow");
956 underflow.available_size = 100;
957 underflow.computed_sizes = vec![10];
958 debugger.record(underflow);
959
960 let mut normal = LayoutRecord::new("normal");
961 normal.available_size = 100;
962 normal.computed_sizes = vec![90];
963 debugger.record(normal);
964
965 let dot = debugger.export_dot();
966 assert!(dot.contains("color=\"red\"")); assert!(dot.contains("color=\"yellow\"")); assert!(dot.contains("color=\"green\"")); }
970
971 #[test]
972 fn grid_record_overflow() {
973 let mut record = GridLayoutRecord::new("grid");
974 record.available_width = 100;
975 record.available_height = 100;
976 record.row_heights = vec![60, 60];
977 record.col_widths = vec![50, 50];
978
979 assert!(record.has_row_overflow());
980 assert!(!record.has_col_overflow());
981 }
982
983 #[test]
984 fn debugger_record_grid() {
985 let debugger = LayoutDebugger::new();
986 debugger.set_enabled(true);
987
988 let mut record = GridLayoutRecord::new("grid");
989 record.available_width = 100;
990 record.available_height = 100;
991 record.row_heights = vec![50, 50];
992 record.col_widths = vec![50, 50];
993 debugger.record_grid(record);
994
995 let records = debugger.snapshot_grids();
996 assert_eq!(records.len(), 1);
997 assert_eq!(records[0].name, "grid");
998 }
999
1000 #[test]
1001 fn format_constraint_all_types() {
1002 assert_eq!(
1003 LayoutRecord::format_constraint(&Constraint::Fixed(10)),
1004 "Fixed(10)"
1005 );
1006 assert_eq!(
1007 LayoutRecord::format_constraint(&Constraint::Percentage(50.0)),
1008 "Pct(50%)"
1009 );
1010 assert_eq!(
1011 LayoutRecord::format_constraint(&Constraint::Min(5)),
1012 "Min(5)"
1013 );
1014 assert_eq!(
1015 LayoutRecord::format_constraint(&Constraint::Max(20)),
1016 "Max(20)"
1017 );
1018 assert_eq!(
1019 LayoutRecord::format_constraint(&Constraint::Ratio(1, 3)),
1020 "Ratio(1/3)"
1021 );
1022 }
1023
1024 #[test]
1027 fn layout_record_to_jsonl() {
1028 let mut record = LayoutRecord::new("test_layout");
1029 record.constraints = vec![Constraint::Fixed(30), Constraint::Min(10)];
1030 record.available_size = 100;
1031 record.computed_sizes = vec![30, 70];
1032 record.direction = Direction::Horizontal;
1033 record.gap = 2;
1034
1035 let jsonl = record.to_jsonl();
1036 assert!(jsonl.contains("\"event\":\"layout_solve\""));
1037 assert!(jsonl.contains("\"name\":\"test_layout\""));
1038 assert!(jsonl.contains("\"direction\":\"Horizontal\""));
1039 assert!(jsonl.contains("\"available_size\":100"));
1040 assert!(jsonl.contains("\"gap\":2"));
1041 assert!(jsonl.contains("\"Fixed(30)\""));
1042 assert!(jsonl.contains("\"Min(10)\""));
1043 assert!(jsonl.contains("\"computed_sizes\":[30,70]"));
1044 assert!(!jsonl.contains('\n'));
1046 }
1047
1048 #[test]
1049 fn grid_record_to_jsonl() {
1050 let mut record = GridLayoutRecord::new("test_grid");
1051 record.available_width = 100;
1052 record.available_height = 50;
1053 record.row_heights = vec![10, 20, 20];
1054 record.col_widths = vec![30, 30, 40];
1055
1056 let jsonl = record.to_jsonl();
1057 assert!(jsonl.contains("\"event\":\"grid_layout_solve\""));
1058 assert!(jsonl.contains("\"name\":\"test_grid\""));
1059 assert!(jsonl.contains("\"available_width\":100"));
1060 assert!(jsonl.contains("\"available_height\":50"));
1061 assert!(jsonl.contains("\"row_heights\":[10,20,20]"));
1062 assert!(jsonl.contains("\"col_widths\":[30,30,40]"));
1063 assert!(jsonl.contains("\"has_row_overflow\":false"));
1064 assert!(jsonl.contains("\"has_col_overflow\":false"));
1065 assert!(!jsonl.contains('\n'));
1066 }
1067
1068 #[test]
1069 fn telemetry_hooks_fire_on_layout_solve() {
1070 use std::sync::atomic::{AtomicU32, Ordering};
1071 let counter = Arc::new(AtomicU32::new(0));
1072 let counter_clone = counter.clone();
1073
1074 let hooks = LayoutTelemetryHooks::new().on_layout_solve(move |_record| {
1075 counter_clone.fetch_add(1, Ordering::SeqCst);
1076 });
1077
1078 let debugger = LayoutDebugger::new();
1079 debugger.set_enabled(true);
1080 debugger.set_telemetry_hooks(hooks);
1081
1082 let mut record = LayoutRecord::new("test");
1083 record.available_size = 100;
1084 record.computed_sizes = vec![50, 50];
1085 debugger.record(record);
1086
1087 assert_eq!(counter.load(Ordering::SeqCst), 1);
1088 }
1089
1090 #[test]
1091 fn telemetry_hooks_fire_on_overflow() {
1092 use std::sync::atomic::{AtomicU32, Ordering};
1093 let overflow_counter = Arc::new(AtomicU32::new(0));
1094 let overflow_clone = overflow_counter.clone();
1095
1096 let hooks = LayoutTelemetryHooks::new().on_overflow(move |_record| {
1097 overflow_clone.fetch_add(1, Ordering::SeqCst);
1098 });
1099
1100 let debugger = LayoutDebugger::new();
1101 debugger.set_enabled(true);
1102 debugger.set_telemetry_hooks(hooks);
1103
1104 let mut overflow_record = LayoutRecord::new("overflow");
1106 overflow_record.available_size = 100;
1107 overflow_record.computed_sizes = vec![60, 60]; debugger.record(overflow_record);
1109
1110 let mut normal_record = LayoutRecord::new("normal");
1112 normal_record.available_size = 100;
1113 normal_record.computed_sizes = vec![30, 30];
1114 debugger.record(normal_record);
1115
1116 assert_eq!(overflow_counter.load(Ordering::SeqCst), 1);
1118 }
1119
1120 #[test]
1121 fn telemetry_hooks_fire_on_underflow() {
1122 use std::sync::atomic::{AtomicU32, Ordering};
1123 let underflow_counter = Arc::new(AtomicU32::new(0));
1124 let underflow_clone = underflow_counter.clone();
1125
1126 let hooks = LayoutTelemetryHooks::new().on_underflow(move |_record| {
1127 underflow_clone.fetch_add(1, Ordering::SeqCst);
1128 });
1129
1130 let debugger = LayoutDebugger::new();
1131 debugger.set_enabled(true);
1132 debugger.set_telemetry_hooks(hooks);
1133
1134 let mut underflow_record = LayoutRecord::new("underflow");
1136 underflow_record.available_size = 100;
1137 underflow_record.computed_sizes = vec![10, 10]; debugger.record(underflow_record);
1139
1140 assert_eq!(underflow_counter.load(Ordering::SeqCst), 1);
1141 }
1142
1143 #[test]
1144 fn telemetry_hooks_fire_on_grid_solve() {
1145 use std::sync::atomic::{AtomicU32, Ordering};
1146 let counter = Arc::new(AtomicU32::new(0));
1147 let counter_clone = counter.clone();
1148
1149 let hooks = LayoutTelemetryHooks::new().on_grid_solve(move |_record| {
1150 counter_clone.fetch_add(1, Ordering::SeqCst);
1151 });
1152
1153 let debugger = LayoutDebugger::new();
1154 debugger.set_enabled(true);
1155 debugger.set_telemetry_hooks(hooks);
1156
1157 let mut record = GridLayoutRecord::new("grid");
1158 record.available_width = 100;
1159 record.available_height = 50;
1160 record.row_heights = vec![25, 25];
1161 record.col_widths = vec![50, 50];
1162 debugger.record_grid(record);
1163
1164 assert_eq!(counter.load(Ordering::SeqCst), 1);
1165 }
1166
1167 #[test]
1168 fn telemetry_hooks_not_fired_when_disabled() {
1169 use std::sync::atomic::{AtomicU32, Ordering};
1170 let counter = Arc::new(AtomicU32::new(0));
1171 let counter_clone = counter.clone();
1172
1173 let hooks = LayoutTelemetryHooks::new().on_layout_solve(move |_record| {
1174 counter_clone.fetch_add(1, Ordering::SeqCst);
1175 });
1176
1177 let debugger = LayoutDebugger::new();
1178 debugger.set_telemetry_hooks(hooks);
1180
1181 let mut record = LayoutRecord::new("test");
1182 record.available_size = 100;
1183 record.computed_sizes = vec![50, 50];
1184 debugger.record(record);
1185
1186 assert_eq!(counter.load(Ordering::SeqCst), 0);
1188 }
1189
1190 #[test]
1191 fn clear_telemetry_hooks() {
1192 use std::sync::atomic::{AtomicU32, Ordering};
1193 let counter = Arc::new(AtomicU32::new(0));
1194 let counter_clone = counter.clone();
1195
1196 let hooks = LayoutTelemetryHooks::new().on_layout_solve(move |_record| {
1197 counter_clone.fetch_add(1, Ordering::SeqCst);
1198 });
1199
1200 let debugger = LayoutDebugger::new();
1201 debugger.set_enabled(true);
1202 debugger.set_telemetry_hooks(hooks);
1203
1204 let mut record1 = LayoutRecord::new("test1");
1205 record1.available_size = 100;
1206 record1.computed_sizes = vec![50, 50];
1207 debugger.record(record1);
1208
1209 assert_eq!(counter.load(Ordering::SeqCst), 1);
1210
1211 debugger.clear_telemetry_hooks();
1213
1214 let mut record2 = LayoutRecord::new("test2");
1215 record2.available_size = 100;
1216 record2.computed_sizes = vec![50, 50];
1217 debugger.record(record2);
1218
1219 assert_eq!(counter.load(Ordering::SeqCst), 1);
1221 }
1222
1223 #[test]
1224 fn layout_record_jsonl_overflow_flags() {
1225 let mut record = LayoutRecord::new("overflow_test");
1226 record.available_size = 100;
1227 record.computed_sizes = vec![60, 60]; let jsonl = record.to_jsonl();
1230 assert!(jsonl.contains("\"has_overflow\":true"));
1231 }
1232
1233 #[test]
1234 fn layout_record_jsonl_underflow_flags() {
1235 let mut record = LayoutRecord::new("underflow_test");
1236 record.available_size = 100;
1237 record.computed_sizes = vec![10, 10]; let jsonl = record.to_jsonl();
1240 assert!(jsonl.contains("\"has_underflow\":true"));
1241 }
1242}