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: crate::Sizes,
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: crate::Rects,
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: crate::Sizes::new(),
77 direction: Direction::default(),
78 alignment: Alignment::default(),
79 margin: Sides::default(),
80 gap: 0,
81 input_area: Rect::default(),
82 result_rects: crate::Rects::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
91 .computed_sizes
92 .iter()
93 .fold(0u16, |acc, &s| acc.saturating_add(s));
94 let total_gaps = if self.computed_sizes.len() > 1 {
95 self.gap
96 .saturating_mul((self.computed_sizes.len() - 1) as u16)
97 } else {
98 0
99 };
100 total_computed.saturating_add(total_gaps) > self.available_size
101 }
102
103 pub fn has_underflow(&self) -> bool {
107 let total_computed: u16 = self
108 .computed_sizes
109 .iter()
110 .fold(0u16, |acc, &s| acc.saturating_add(s));
111 let total_gaps = if self.computed_sizes.len() > 1 {
112 self.gap
113 .saturating_mul((self.computed_sizes.len() - 1) as u16)
114 } else {
115 0
116 };
117 let total_used = total_computed.saturating_add(total_gaps);
118 let unused = self.available_size.saturating_sub(total_used);
119 self.available_size > 0 && (unused as f32 / self.available_size as f32) > 0.2
121 }
122
123 pub fn utilization(&self) -> f32 {
125 if self.available_size == 0 {
126 return 0.0;
127 }
128 let total_computed: u16 = self
129 .computed_sizes
130 .iter()
131 .fold(0u16, |acc, &s| acc.saturating_add(s));
132 let total_gaps = if self.computed_sizes.len() > 1 {
133 self.gap
134 .saturating_mul((self.computed_sizes.len() - 1) as u16)
135 } else {
136 0
137 };
138 let total_used = total_computed.saturating_add(total_gaps);
139 (total_used as f32 / self.available_size as f32).min(1.0) * 100.0
140 }
141
142 fn format_constraint(c: &Constraint) -> String {
144 match c {
145 Constraint::Fixed(n) => format!("Fixed({n})"),
146 Constraint::Percentage(p) => format!("Pct({p:.0}%)"),
147 Constraint::Min(n) => format!("Min({n})"),
148 Constraint::Max(n) => format!("Max({n})"),
149 Constraint::Ratio(n, d) => format!("Ratio({n}/{d})"),
150 Constraint::Fill => "Fill".to_string(),
151 Constraint::FitContent => "FitContent".to_string(),
152 Constraint::FitContentBounded { min, max } => format!("FitContent({min}..{max})"),
153 Constraint::FitMin => "FitMin".to_string(),
154 }
155 }
156
157 pub fn summary(&self) -> String {
159 let mut s = String::new();
160 let _ = writeln!(s, "{} ({:?}):", self.name, self.direction);
161 let _ = writeln!(
162 s,
163 " Input: {}x{} at ({},{})",
164 self.input_area.width, self.input_area.height, self.input_area.x, self.input_area.y
165 );
166 let _ = writeln!(s, " Available: {} (after margin)", self.available_size);
167 let _ = writeln!(s, " Gap: {}", self.gap);
168
169 for (i, (constraint, size)) in self
170 .constraints
171 .iter()
172 .zip(self.computed_sizes.iter())
173 .enumerate()
174 {
175 let constraint_str = Self::format_constraint(constraint);
176 let rect = self.result_rects.get(i);
177 let rect_str = rect.map_or_else(
178 || "?".to_string(),
179 |r| format!("({},{} {}x{})", r.x, r.y, r.width, r.height),
180 );
181 let _ = writeln!(s, " [{i}] {constraint_str} -> {size} @ {rect_str}");
182 }
183
184 let _ = writeln!(s, " Utilization: {:.1}%", self.utilization());
185 if self.has_overflow() {
186 let _ = writeln!(s, " ⚠ OVERFLOW");
187 }
188 if self.has_underflow() {
189 let _ = writeln!(s, " ⚠ UNDERFLOW (>20% unused)");
190 }
191 if let Some(t) = self.solve_time {
192 let _ = writeln!(s, " Solve time: {:?}", t);
193 }
194 s
195 }
196
197 #[must_use]
201 pub fn to_jsonl(&self) -> String {
202 let constraints_json: Vec<String> = self
203 .constraints
204 .iter()
205 .map(|c| format!("\"{}\"", Self::format_constraint(c)))
206 .collect();
207 let sizes_json: Vec<String> = self.computed_sizes.iter().map(|s| s.to_string()).collect();
208 let solve_time_us = self.solve_time.map(|d| d.as_micros() as u64).unwrap_or(0);
209
210 format!(
211 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":{}}}"#,
212 self.name,
213 self.direction,
214 self.alignment,
215 self.available_size,
216 self.gap,
217 self.margin.top,
218 self.margin.right,
219 self.margin.bottom,
220 self.margin.left,
221 constraints_json.join(","),
222 sizes_json.join(","),
223 self.utilization(),
224 self.has_overflow(),
225 self.has_underflow(),
226 solve_time_us
227 )
228 }
229}
230
231#[derive(Debug, Clone)]
233pub struct GridLayoutRecord {
234 pub name: String,
236 pub row_constraints: Vec<Constraint>,
238 pub col_constraints: Vec<Constraint>,
240 pub available_width: u16,
242 pub available_height: u16,
244 pub row_heights: crate::Sizes,
246 pub col_widths: crate::Sizes,
248 pub input_area: Rect,
250 pub solve_time: Option<Duration>,
252}
253
254impl GridLayoutRecord {
255 pub fn new(name: impl Into<String>) -> Self {
257 Self {
258 name: name.into(),
259 row_constraints: Vec::new(),
260 col_constraints: Vec::new(),
261 available_width: 0,
262 available_height: 0,
263 row_heights: crate::Sizes::new(),
264 col_widths: crate::Sizes::new(),
265 input_area: Rect::default(),
266 solve_time: None,
267 }
268 }
269
270 pub fn has_row_overflow(&self) -> bool {
272 self.row_heights.iter().sum::<u16>() > self.available_height
273 }
274
275 pub fn has_col_overflow(&self) -> bool {
277 self.col_widths.iter().sum::<u16>() > self.available_width
278 }
279
280 #[must_use]
282 pub fn to_jsonl(&self) -> String {
283 let row_heights_json: Vec<String> =
284 self.row_heights.iter().map(|h| h.to_string()).collect();
285 let col_widths_json: Vec<String> = self.col_widths.iter().map(|w| w.to_string()).collect();
286 let solve_time_us = self.solve_time.map(|d| d.as_micros() as u64).unwrap_or(0);
287
288 format!(
289 r#"{{"event":"grid_layout_solve","name":"{}","available_width":{},"available_height":{},"row_heights":[{}],"col_widths":[{}],"has_row_overflow":{},"has_col_overflow":{},"solve_time_us":{}}}"#,
290 self.name,
291 self.available_width,
292 self.available_height,
293 row_heights_json.join(","),
294 col_widths_json.join(","),
295 self.has_row_overflow(),
296 self.has_col_overflow(),
297 solve_time_us
298 )
299 }
300}
301
302type LayoutHook = Box<dyn Fn(&LayoutRecord) + Send + Sync>;
324type GridHook = Box<dyn Fn(&GridLayoutRecord) + Send + Sync>;
325
326pub struct LayoutTelemetryHooks {
327 on_layout_solve: Option<LayoutHook>,
328 on_grid_solve: Option<GridHook>,
329 on_overflow: Option<LayoutHook>,
330 on_underflow: Option<LayoutHook>,
331}
332
333impl Default for LayoutTelemetryHooks {
334 fn default() -> Self {
335 Self::new()
336 }
337}
338
339impl std::fmt::Debug for LayoutTelemetryHooks {
340 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
341 f.debug_struct("LayoutTelemetryHooks")
342 .field("on_layout_solve", &self.on_layout_solve.is_some())
343 .field("on_grid_solve", &self.on_grid_solve.is_some())
344 .field("on_overflow", &self.on_overflow.is_some())
345 .field("on_underflow", &self.on_underflow.is_some())
346 .finish()
347 }
348}
349
350impl LayoutTelemetryHooks {
351 #[must_use]
353 pub fn new() -> Self {
354 Self {
355 on_layout_solve: None,
356 on_grid_solve: None,
357 on_overflow: None,
358 on_underflow: None,
359 }
360 }
361
362 #[must_use]
364 pub fn on_layout_solve<F>(mut self, f: F) -> Self
365 where
366 F: Fn(&LayoutRecord) + Send + Sync + 'static,
367 {
368 self.on_layout_solve = Some(Box::new(f));
369 self
370 }
371
372 #[must_use]
374 pub fn on_grid_solve<F>(mut self, f: F) -> Self
375 where
376 F: Fn(&GridLayoutRecord) + Send + Sync + 'static,
377 {
378 self.on_grid_solve = Some(Box::new(f));
379 self
380 }
381
382 #[must_use]
384 pub fn on_overflow<F>(mut self, f: F) -> Self
385 where
386 F: Fn(&LayoutRecord) + Send + Sync + 'static,
387 {
388 self.on_overflow = Some(Box::new(f));
389 self
390 }
391
392 #[must_use]
394 pub fn on_underflow<F>(mut self, f: F) -> Self
395 where
396 F: Fn(&LayoutRecord) + Send + Sync + 'static,
397 {
398 self.on_underflow = Some(Box::new(f));
399 self
400 }
401
402 pub fn fire_layout_solve(&self, record: &LayoutRecord) {
404 if let Some(ref f) = self.on_layout_solve {
405 f(record);
406 }
407 }
408
409 pub fn fire_grid_solve(&self, record: &GridLayoutRecord) {
411 if let Some(ref f) = self.on_grid_solve {
412 f(record);
413 }
414 }
415
416 pub fn fire_overflow(&self, record: &LayoutRecord) {
418 if let Some(ref f) = self.on_overflow {
419 f(record);
420 }
421 }
422
423 pub fn fire_underflow(&self, record: &LayoutRecord) {
425 if let Some(ref f) = self.on_underflow {
426 f(record);
427 }
428 }
429}
430
431pub struct LayoutDebugger {
438 enabled: AtomicBool,
439 records: Mutex<Vec<LayoutRecord>>,
440 grid_records: Mutex<Vec<GridLayoutRecord>>,
441 telemetry_hooks: Mutex<Option<Arc<LayoutTelemetryHooks>>>,
442}
443
444impl std::fmt::Debug for LayoutDebugger {
445 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
446 f.debug_struct("LayoutDebugger")
447 .field("enabled", &self.enabled.load(Ordering::Relaxed))
448 .field(
449 "records_count",
450 &self.records.lock().map(|r| r.len()).unwrap_or(0),
451 )
452 .field(
453 "grid_records_count",
454 &self.grid_records.lock().map(|r| r.len()).unwrap_or(0),
455 )
456 .field(
457 "has_telemetry_hooks",
458 &self
459 .telemetry_hooks
460 .lock()
461 .map(|h| h.is_some())
462 .unwrap_or(false),
463 )
464 .finish()
465 }
466}
467
468impl LayoutDebugger {
469 pub fn new() -> Arc<Self> {
471 Arc::new(Self {
472 enabled: AtomicBool::new(false),
473 records: Mutex::new(Vec::new()),
474 grid_records: Mutex::new(Vec::new()),
475 telemetry_hooks: Mutex::new(None),
476 })
477 }
478
479 pub fn set_telemetry_hooks(&self, hooks: LayoutTelemetryHooks) {
481 if let Ok(mut h) = self.telemetry_hooks.lock() {
482 *h = Some(Arc::new(hooks));
483 }
484 }
485
486 pub fn clear_telemetry_hooks(&self) {
488 if let Ok(mut h) = self.telemetry_hooks.lock() {
489 *h = None;
490 }
491 }
492
493 fn telemetry_hooks_snapshot(&self) -> Option<Arc<LayoutTelemetryHooks>> {
494 self.telemetry_hooks
495 .lock()
496 .ok()
497 .and_then(|hooks| hooks.clone())
498 }
499
500 #[inline]
502 pub fn enabled(&self) -> bool {
503 self.enabled.load(Ordering::Relaxed)
504 }
505
506 pub fn set_enabled(&self, enabled: bool) {
508 let was_enabled = self.enabled.swap(enabled, Ordering::Relaxed);
509 if was_enabled && !enabled {
510 self.clear();
511 }
512 }
513
514 pub fn toggle(&self) -> bool {
516 let next = !self.enabled();
517 self.set_enabled(next);
518 next
519 }
520
521 pub fn clear(&self) {
523 if let Ok(mut records) = self.records.lock() {
524 records.clear();
525 }
526 if let Ok(mut grid_records) = self.grid_records.lock() {
527 grid_records.clear();
528 }
529 }
530
531 pub fn record(&self, record: LayoutRecord) {
538 if !self.enabled() {
539 return;
540 }
541
542 if let Some(h) = self.telemetry_hooks_snapshot() {
544 h.fire_layout_solve(&record);
545 if record.has_overflow() {
546 h.fire_overflow(&record);
547 }
548 if record.has_underflow() {
549 h.fire_underflow(&record);
550 }
551 }
552
553 if let Ok(mut records) = self.records.lock() {
554 if !self.enabled() {
555 return;
556 }
557 records.push(record);
558 }
559 }
560
561 pub fn record_grid(&self, record: GridLayoutRecord) {
565 if !self.enabled() {
566 return;
567 }
568
569 if let Some(h) = self.telemetry_hooks_snapshot() {
571 h.fire_grid_solve(&record);
572 }
573
574 if let Ok(mut grid_records) = self.grid_records.lock() {
575 if !self.enabled() {
576 return;
577 }
578 grid_records.push(record);
579 }
580 }
581
582 pub fn snapshot(&self) -> Vec<LayoutRecord> {
584 self.records
585 .lock()
586 .ok()
587 .map(|r| r.clone())
588 .unwrap_or_default()
589 }
590
591 pub fn snapshot_grids(&self) -> Vec<GridLayoutRecord> {
593 self.grid_records
594 .lock()
595 .ok()
596 .map(|r| r.clone())
597 .unwrap_or_default()
598 }
599
600 pub fn overflows(&self) -> Vec<LayoutRecord> {
602 self.snapshot()
603 .into_iter()
604 .filter(|r| r.has_overflow())
605 .collect()
606 }
607
608 pub fn underflows(&self) -> Vec<LayoutRecord> {
610 self.snapshot()
611 .into_iter()
612 .filter(|r| r.has_underflow())
613 .collect()
614 }
615
616 pub fn report(&self) -> String {
618 let records = self.snapshot();
619 let grid_records = self.snapshot_grids();
620
621 let mut s = String::new();
622 let _ = writeln!(
623 s,
624 "=== Layout Debug Report ({} flex, {} grid) ===",
625 records.len(),
626 grid_records.len()
627 );
628
629 let overflows: Vec<_> = records.iter().filter(|r| r.has_overflow()).collect();
630 let underflows: Vec<_> = records.iter().filter(|r| r.has_underflow()).collect();
631
632 if !overflows.is_empty() {
633 let _ = writeln!(s, "\n⚠ {} layouts have OVERFLOW:", overflows.len());
634 for r in &overflows {
635 let _ = writeln!(s, " - {}", r.name);
636 }
637 }
638
639 if !underflows.is_empty() {
640 let _ = writeln!(s, "\n⚠ {} layouts have UNDERFLOW:", underflows.len());
641 for r in &underflows {
642 let _ = writeln!(s, " - {} ({:.1}% utilization)", r.name, r.utilization());
643 }
644 }
645
646 let _ = writeln!(s, "\n--- Flex Layouts ---");
647 for record in &records {
648 let _ = write!(s, "\n{}", record.summary());
649 }
650
651 if !grid_records.is_empty() {
652 let _ = writeln!(s, "\n--- Grid Layouts ---");
653 for record in &grid_records {
654 let _ = writeln!(s, "\n{} (Grid):", record.name);
655 let _ = writeln!(
656 s,
657 " Input: {}x{}",
658 record.input_area.width, record.input_area.height
659 );
660 let _ = writeln!(s, " Rows: {:?}", record.row_heights);
661 let _ = writeln!(s, " Cols: {:?}", record.col_widths);
662 if record.has_row_overflow() {
663 let _ = writeln!(s, " ⚠ ROW OVERFLOW");
664 }
665 if record.has_col_overflow() {
666 let _ = writeln!(s, " ⚠ COLUMN OVERFLOW");
667 }
668 }
669 }
670
671 s
672 }
673
674 pub fn export_dot(&self) -> String {
679 let records = self.snapshot();
680
681 let mut s = String::new();
682 let _ = writeln!(s, "digraph LayoutDebug {{");
683 let _ = writeln!(s, " rankdir=TB;");
684 let _ = writeln!(s, " node [shape=record];");
685
686 for (i, r) in records.iter().enumerate() {
687 let color = if r.has_overflow() {
688 "red"
689 } else if r.has_underflow() {
690 "yellow"
691 } else {
692 "green"
693 };
694
695 let label = format!(
696 "{}|dir: {:?}|avail: {}|util: {:.0}%",
697 r.name,
698 r.direction,
699 r.available_size,
700 r.utilization()
701 );
702
703 let _ = writeln!(
704 s,
705 " n{} [label=\"{{{}}}\", color=\"{}\"];",
706 i, label, color
707 );
708
709 if let Some(parent) = r.parent_index {
710 let _ = writeln!(s, " n{} -> n{};", parent, i);
711 }
712 }
713
714 let _ = writeln!(s, "}}");
715 s
716 }
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722
723 #[test]
724 fn layout_record_overflow_detection() {
725 let mut record = LayoutRecord::new("test");
726 record.available_size = 100;
727 record.computed_sizes = smallvec::smallvec![60u16, 60u16];
728 record.gap = 0;
729
730 assert!(record.has_overflow());
731 }
732
733 #[test]
734 fn layout_record_no_overflow() {
735 let mut record = LayoutRecord::new("test");
736 record.available_size = 100;
737 record.computed_sizes = smallvec::smallvec![40u16, 40u16];
738 record.gap = 0;
739
740 assert!(!record.has_overflow());
741 }
742
743 #[test]
744 fn layout_record_overflow_with_gaps() {
745 let mut record = LayoutRecord::new("test");
746 record.available_size = 100;
747 record.computed_sizes = smallvec::smallvec![45u16, 45u16];
748 record.gap = 15; assert!(record.has_overflow());
751 }
752
753 #[test]
754 fn layout_record_underflow_detection() {
755 let mut record = LayoutRecord::new("test");
756 record.available_size = 100;
757 record.computed_sizes = smallvec::smallvec![20u16, 20u16]; record.gap = 0;
759
760 assert!(record.has_underflow());
761 }
762
763 #[test]
764 fn layout_record_no_underflow() {
765 let mut record = LayoutRecord::new("test");
766 record.available_size = 100;
767 record.computed_sizes = smallvec::smallvec![40u16, 45u16]; record.gap = 0;
769
770 assert!(!record.has_underflow());
771 }
772
773 #[test]
774 fn layout_record_utilization() {
775 let mut record = LayoutRecord::new("test");
776 record.available_size = 100;
777 record.computed_sizes = smallvec::smallvec![25u16, 25u16];
778 record.gap = 0;
779
780 assert!((record.utilization() - 50.0).abs() < 0.1);
781 }
782
783 #[test]
784 fn layout_record_utilization_with_gap() {
785 let mut record = LayoutRecord::new("test");
786 record.available_size = 100;
787 record.computed_sizes = smallvec::smallvec![20u16, 20u16];
788 record.gap = 10; assert!((record.utilization() - 50.0).abs() < 0.1);
791 }
792
793 #[test]
794 fn layout_record_utilization_clamped() {
795 let mut record = LayoutRecord::new("test");
796 record.available_size = 100;
797 record.computed_sizes = smallvec::smallvec![150u16]; assert!((record.utilization() - 100.0).abs() < 0.1);
801 }
802
803 #[test]
804 fn layout_record_zero_available() {
805 let mut record = LayoutRecord::new("test");
806 record.available_size = 0;
807 record.computed_sizes = crate::Sizes::new();
808 record.gap = 0;
809
810 assert!(!record.has_overflow());
811 assert!(!record.has_underflow());
812 assert!((record.utilization() - 0.0).abs() < 0.1);
813 }
814
815 #[test]
816 fn layout_record_summary() {
817 let mut record = LayoutRecord::new("main_layout");
818 record.constraints = vec![Constraint::Fixed(30), Constraint::Min(10)];
819 record.available_size = 100;
820 record.computed_sizes = smallvec::smallvec![30u16, 70u16];
821 record.direction = Direction::Horizontal;
822 record.input_area = Rect::new(0, 0, 100, 50);
823 record.result_rects =
824 smallvec::smallvec![Rect::new(0, 0, 30, 50), Rect::new(30, 0, 70, 50)];
825
826 let summary = record.summary();
827 assert!(summary.contains("main_layout"));
828 assert!(summary.contains("Horizontal"));
829 assert!(summary.contains("Fixed(30)"));
830 assert!(summary.contains("Min(10)"));
831 }
832
833 #[test]
834 fn debugger_disabled_by_default() {
835 let debugger = LayoutDebugger::new();
836 assert!(!debugger.enabled());
837 }
838
839 #[test]
840 fn debugger_enable_disable() {
841 let debugger = LayoutDebugger::new();
842 debugger.set_enabled(true);
843 assert!(debugger.enabled());
844 debugger.record(LayoutRecord::new("stale"));
845 assert_eq!(debugger.snapshot().len(), 1);
846 debugger.set_enabled(false);
847 assert!(!debugger.enabled());
848 assert!(debugger.snapshot().is_empty());
849 assert!(debugger.snapshot_grids().is_empty());
850 }
851
852 #[test]
853 fn debugger_toggle() {
854 let debugger = LayoutDebugger::new();
855 assert!(!debugger.enabled());
856 let result = debugger.toggle();
857 assert!(result);
858 assert!(debugger.enabled());
859 debugger.record(LayoutRecord::new("stale"));
860 assert_eq!(debugger.snapshot().len(), 1);
861 let result = debugger.toggle();
862 assert!(!result);
863 assert!(!debugger.enabled());
864 assert!(debugger.snapshot().is_empty());
865 }
866
867 #[test]
868 fn debugger_disable_clears_grid_records() {
869 let debugger = LayoutDebugger::new();
870 debugger.set_enabled(true);
871 debugger.record_grid(GridLayoutRecord::new("grid"));
872 assert_eq!(debugger.snapshot_grids().len(), 1);
873
874 debugger.set_enabled(false);
875
876 assert!(debugger.snapshot_grids().is_empty());
877 }
878
879 #[test]
880 fn debugger_record_when_disabled() {
881 let debugger = LayoutDebugger::new();
882 debugger.record(LayoutRecord::new("test"));
883 assert!(debugger.snapshot().is_empty());
884 }
885
886 #[test]
887 fn debugger_record_when_enabled() {
888 let debugger = LayoutDebugger::new();
889 debugger.set_enabled(true);
890 debugger.record(LayoutRecord::new("test"));
891 let records = debugger.snapshot();
892 assert_eq!(records.len(), 1);
893 assert_eq!(records[0].name, "test");
894 }
895
896 #[test]
897 fn debugger_clear() {
898 let debugger = LayoutDebugger::new();
899 debugger.set_enabled(true);
900 debugger.record(LayoutRecord::new("test1"));
901 debugger.record(LayoutRecord::new("test2"));
902 assert_eq!(debugger.snapshot().len(), 2);
903
904 debugger.clear();
905 assert!(debugger.snapshot().is_empty());
906 }
907
908 #[test]
909 fn debugger_overflows() {
910 let debugger = LayoutDebugger::new();
911 debugger.set_enabled(true);
912
913 let mut overflow_record = LayoutRecord::new("overflow");
914 overflow_record.available_size = 100;
915 overflow_record.computed_sizes = smallvec::smallvec![60u16, 60u16];
916 debugger.record(overflow_record);
917
918 let mut normal_record = LayoutRecord::new("normal");
919 normal_record.available_size = 100;
920 normal_record.computed_sizes = smallvec::smallvec![30u16, 30u16];
921 debugger.record(normal_record);
922
923 let overflows = debugger.overflows();
924 assert_eq!(overflows.len(), 1);
925 assert_eq!(overflows[0].name, "overflow");
926 }
927
928 #[test]
929 fn debugger_underflows() {
930 let debugger = LayoutDebugger::new();
931 debugger.set_enabled(true);
932
933 let mut underflow_record = LayoutRecord::new("underflow");
934 underflow_record.available_size = 100;
935 underflow_record.computed_sizes = smallvec::smallvec![10u16, 10u16]; debugger.record(underflow_record);
937
938 let mut normal_record = LayoutRecord::new("normal");
939 normal_record.available_size = 100;
940 normal_record.computed_sizes = smallvec::smallvec![45u16, 45u16]; debugger.record(normal_record);
942
943 let underflows = debugger.underflows();
944 assert_eq!(underflows.len(), 1);
945 assert_eq!(underflows[0].name, "underflow");
946 }
947
948 #[test]
949 fn debugger_report() {
950 let debugger = LayoutDebugger::new();
951 debugger.set_enabled(true);
952
953 let mut record = LayoutRecord::new("test_layout");
954 record.available_size = 100;
955 record.computed_sizes = smallvec::smallvec![50u16, 50u16];
956 record.direction = Direction::Horizontal;
957 debugger.record(record);
958
959 let report = debugger.report();
960 assert!(report.contains("Layout Debug Report"));
961 assert!(report.contains("test_layout"));
962 }
963
964 #[test]
965 fn debugger_export_dot() {
966 let debugger = LayoutDebugger::new();
967 debugger.set_enabled(true);
968
969 let mut record = LayoutRecord::new("root");
970 record.available_size = 100;
971 record.computed_sizes = smallvec::smallvec![50u16, 50u16];
972 record.direction = Direction::Vertical;
973 debugger.record(record);
974
975 let mut child = LayoutRecord::new("child");
976 child.available_size = 50;
977 child.computed_sizes = smallvec::smallvec![25u16, 25u16];
978 child.parent_index = Some(0);
979 debugger.record(child);
980
981 let dot = debugger.export_dot();
982 assert!(dot.contains("digraph LayoutDebug"));
983 assert!(dot.contains("root"));
984 assert!(dot.contains("child"));
985 assert!(dot.contains("n0 -> n1")); }
987
988 #[test]
989 fn debugger_export_dot_colors() {
990 let debugger = LayoutDebugger::new();
991 debugger.set_enabled(true);
992
993 let mut overflow = LayoutRecord::new("overflow");
994 overflow.available_size = 100;
995 overflow.computed_sizes = smallvec::smallvec![120u16];
996 debugger.record(overflow);
997
998 let mut underflow = LayoutRecord::new("underflow");
999 underflow.available_size = 100;
1000 underflow.computed_sizes = smallvec::smallvec![10u16];
1001 debugger.record(underflow);
1002
1003 let mut normal = LayoutRecord::new("normal");
1004 normal.available_size = 100;
1005 normal.computed_sizes = smallvec::smallvec![90u16];
1006 debugger.record(normal);
1007
1008 let dot = debugger.export_dot();
1009 assert!(dot.contains("color=\"red\"")); assert!(dot.contains("color=\"yellow\"")); assert!(dot.contains("color=\"green\"")); }
1013
1014 #[test]
1015 fn grid_record_overflow() {
1016 let mut record = GridLayoutRecord::new("grid");
1017 record.available_width = 100;
1018 record.available_height = 100;
1019 record.row_heights = smallvec::smallvec![60u16, 60u16];
1020 record.col_widths = smallvec::smallvec![50u16, 50u16];
1021
1022 assert!(record.has_row_overflow());
1023 assert!(!record.has_col_overflow());
1024 }
1025
1026 #[test]
1027 fn debugger_record_grid() {
1028 let debugger = LayoutDebugger::new();
1029 debugger.set_enabled(true);
1030
1031 let mut record = GridLayoutRecord::new("grid");
1032 record.available_width = 100;
1033 record.available_height = 100;
1034 record.row_heights = smallvec::smallvec![50u16, 50u16];
1035 record.col_widths = smallvec::smallvec![50u16, 50u16];
1036 debugger.record_grid(record);
1037
1038 let records = debugger.snapshot_grids();
1039 assert_eq!(records.len(), 1);
1040 assert_eq!(records[0].name, "grid");
1041 }
1042
1043 #[test]
1044 fn format_constraint_all_types() {
1045 assert_eq!(
1046 LayoutRecord::format_constraint(&Constraint::Fixed(10)),
1047 "Fixed(10)"
1048 );
1049 assert_eq!(
1050 LayoutRecord::format_constraint(&Constraint::Percentage(50.0)),
1051 "Pct(50%)"
1052 );
1053 assert_eq!(
1054 LayoutRecord::format_constraint(&Constraint::Min(5)),
1055 "Min(5)"
1056 );
1057 assert_eq!(
1058 LayoutRecord::format_constraint(&Constraint::Max(20)),
1059 "Max(20)"
1060 );
1061 assert_eq!(
1062 LayoutRecord::format_constraint(&Constraint::Ratio(1, 3)),
1063 "Ratio(1/3)"
1064 );
1065 }
1066
1067 #[test]
1070 fn layout_record_to_jsonl() {
1071 let mut record = LayoutRecord::new("test_layout");
1072 record.constraints = vec![Constraint::Fixed(30), Constraint::Min(10)];
1073 record.available_size = 100;
1074 record.computed_sizes = smallvec::smallvec![30u16, 70u16];
1075 record.direction = Direction::Horizontal;
1076 record.gap = 2;
1077
1078 let jsonl = record.to_jsonl();
1079 assert!(jsonl.contains("\"event\":\"layout_solve\""));
1080 assert!(jsonl.contains("\"name\":\"test_layout\""));
1081 assert!(jsonl.contains("\"direction\":\"Horizontal\""));
1082 assert!(jsonl.contains("\"available_size\":100"));
1083 assert!(jsonl.contains("\"gap\":2"));
1084 assert!(jsonl.contains("\"Fixed(30)\""));
1085 assert!(jsonl.contains("\"Min(10)\""));
1086 assert!(jsonl.contains("\"computed_sizes\":[30,70]"));
1087 assert!(!jsonl.contains('\n'));
1089 }
1090
1091 #[test]
1092 fn grid_record_to_jsonl() {
1093 let mut record = GridLayoutRecord::new("test_grid");
1094 record.available_width = 100;
1095 record.available_height = 50;
1096 record.row_heights = smallvec::smallvec![10u16, 20u16, 20u16];
1097 record.col_widths = smallvec::smallvec![30u16, 30u16, 40u16];
1098
1099 let jsonl = record.to_jsonl();
1100 assert!(jsonl.contains("\"event\":\"grid_layout_solve\""));
1101 assert!(jsonl.contains("\"name\":\"test_grid\""));
1102 assert!(jsonl.contains("\"available_width\":100"));
1103 assert!(jsonl.contains("\"available_height\":50"));
1104 assert!(jsonl.contains("\"row_heights\":[10,20,20]"));
1105 assert!(jsonl.contains("\"col_widths\":[30,30,40]"));
1106 assert!(jsonl.contains("\"has_row_overflow\":false"));
1107 assert!(jsonl.contains("\"has_col_overflow\":false"));
1108 assert!(!jsonl.contains('\n'));
1109 }
1110
1111 #[test]
1112 fn telemetry_hooks_fire_on_layout_solve() {
1113 use std::sync::atomic::{AtomicU32, Ordering};
1114 let counter = Arc::new(AtomicU32::new(0));
1115 let counter_clone = counter.clone();
1116
1117 let hooks = LayoutTelemetryHooks::new().on_layout_solve(move |_record| {
1118 counter_clone.fetch_add(1, Ordering::SeqCst);
1119 });
1120
1121 let debugger = LayoutDebugger::new();
1122 debugger.set_enabled(true);
1123 debugger.set_telemetry_hooks(hooks);
1124
1125 let mut record = LayoutRecord::new("test");
1126 record.available_size = 100;
1127 record.computed_sizes = smallvec::smallvec![50u16, 50u16];
1128 debugger.record(record);
1129
1130 assert_eq!(counter.load(Ordering::SeqCst), 1);
1131 }
1132
1133 #[test]
1134 fn telemetry_hooks_fire_on_overflow() {
1135 use std::sync::atomic::{AtomicU32, Ordering};
1136 let overflow_counter = Arc::new(AtomicU32::new(0));
1137 let overflow_clone = overflow_counter.clone();
1138
1139 let hooks = LayoutTelemetryHooks::new().on_overflow(move |_record| {
1140 overflow_clone.fetch_add(1, Ordering::SeqCst);
1141 });
1142
1143 let debugger = LayoutDebugger::new();
1144 debugger.set_enabled(true);
1145 debugger.set_telemetry_hooks(hooks);
1146
1147 let mut overflow_record = LayoutRecord::new("overflow");
1149 overflow_record.available_size = 100;
1150 overflow_record.computed_sizes = smallvec::smallvec![60u16, 60u16]; debugger.record(overflow_record);
1152
1153 let mut normal_record = LayoutRecord::new("normal");
1155 normal_record.available_size = 100;
1156 normal_record.computed_sizes = smallvec::smallvec![30u16, 30u16];
1157 debugger.record(normal_record);
1158
1159 assert_eq!(overflow_counter.load(Ordering::SeqCst), 1);
1161 }
1162
1163 #[test]
1164 fn telemetry_hooks_fire_on_underflow() {
1165 use std::sync::atomic::{AtomicU32, Ordering};
1166 let underflow_counter = Arc::new(AtomicU32::new(0));
1167 let underflow_clone = underflow_counter.clone();
1168
1169 let hooks = LayoutTelemetryHooks::new().on_underflow(move |_record| {
1170 underflow_clone.fetch_add(1, Ordering::SeqCst);
1171 });
1172
1173 let debugger = LayoutDebugger::new();
1174 debugger.set_enabled(true);
1175 debugger.set_telemetry_hooks(hooks);
1176
1177 let mut underflow_record = LayoutRecord::new("underflow");
1179 underflow_record.available_size = 100;
1180 underflow_record.computed_sizes = smallvec::smallvec![10u16, 10u16]; debugger.record(underflow_record);
1182
1183 assert_eq!(underflow_counter.load(Ordering::SeqCst), 1);
1184 }
1185
1186 #[test]
1187 fn telemetry_hooks_fire_on_grid_solve() {
1188 use std::sync::atomic::{AtomicU32, Ordering};
1189 let counter = Arc::new(AtomicU32::new(0));
1190 let counter_clone = counter.clone();
1191
1192 let hooks = LayoutTelemetryHooks::new().on_grid_solve(move |_record| {
1193 counter_clone.fetch_add(1, Ordering::SeqCst);
1194 });
1195
1196 let debugger = LayoutDebugger::new();
1197 debugger.set_enabled(true);
1198 debugger.set_telemetry_hooks(hooks);
1199
1200 let mut record = GridLayoutRecord::new("grid");
1201 record.available_width = 100;
1202 record.available_height = 50;
1203 record.row_heights = smallvec::smallvec![25u16, 25u16];
1204 record.col_widths = smallvec::smallvec![50u16, 50u16];
1205 debugger.record_grid(record);
1206
1207 assert_eq!(counter.load(Ordering::SeqCst), 1);
1208 }
1209
1210 #[test]
1211 fn telemetry_hooks_not_fired_when_disabled() {
1212 use std::sync::atomic::{AtomicU32, Ordering};
1213 let counter = Arc::new(AtomicU32::new(0));
1214 let counter_clone = counter.clone();
1215
1216 let hooks = LayoutTelemetryHooks::new().on_layout_solve(move |_record| {
1217 counter_clone.fetch_add(1, Ordering::SeqCst);
1218 });
1219
1220 let debugger = LayoutDebugger::new();
1221 debugger.set_telemetry_hooks(hooks);
1223
1224 let mut record = LayoutRecord::new("test");
1225 record.available_size = 100;
1226 record.computed_sizes = smallvec::smallvec![50u16, 50u16];
1227 debugger.record(record);
1228
1229 assert_eq!(counter.load(Ordering::SeqCst), 0);
1231 }
1232
1233 #[test]
1234 fn clear_telemetry_hooks() {
1235 use std::sync::atomic::{AtomicU32, Ordering};
1236 let counter = Arc::new(AtomicU32::new(0));
1237 let counter_clone = counter.clone();
1238
1239 let hooks = LayoutTelemetryHooks::new().on_layout_solve(move |_record| {
1240 counter_clone.fetch_add(1, Ordering::SeqCst);
1241 });
1242
1243 let debugger = LayoutDebugger::new();
1244 debugger.set_enabled(true);
1245 debugger.set_telemetry_hooks(hooks);
1246
1247 let mut record1 = LayoutRecord::new("test1");
1248 record1.available_size = 100;
1249 record1.computed_sizes = smallvec::smallvec![50u16, 50u16];
1250 debugger.record(record1);
1251
1252 assert_eq!(counter.load(Ordering::SeqCst), 1);
1253
1254 debugger.clear_telemetry_hooks();
1256
1257 let mut record2 = LayoutRecord::new("test2");
1258 record2.available_size = 100;
1259 record2.computed_sizes = smallvec::smallvec![50u16, 50u16];
1260 debugger.record(record2);
1261
1262 assert_eq!(counter.load(Ordering::SeqCst), 1);
1264 }
1265
1266 #[test]
1267 fn telemetry_hook_can_clear_hooks_reentrantly_without_deadlocking() {
1268 use std::sync::mpsc;
1269 use std::time::Duration;
1270
1271 let debugger = LayoutDebugger::new();
1272 debugger.set_enabled(true);
1273
1274 let (done_tx, done_rx) = mpsc::channel();
1275 let debugger_for_hook = Arc::clone(&debugger);
1276 let hooks = LayoutTelemetryHooks::new().on_layout_solve(move |_record| {
1277 debugger_for_hook.clear_telemetry_hooks();
1278 done_tx.send(()).expect("completion signal");
1279 });
1280 debugger.set_telemetry_hooks(hooks);
1281
1282 let debugger_for_thread = Arc::clone(&debugger);
1283 let handle = std::thread::spawn(move || {
1284 let mut record = LayoutRecord::new("reentrant_clear");
1285 record.available_size = 100;
1286 record.computed_sizes = smallvec::smallvec![50u16, 50u16];
1287 debugger_for_thread.record(record);
1288 });
1289
1290 done_rx
1291 .recv_timeout(Duration::from_secs(1))
1292 .expect("reentrant hook should complete without deadlocking");
1293 handle.join().expect("layout debug thread");
1294 }
1295
1296 #[test]
1297 fn layout_record_jsonl_overflow_flags() {
1298 let mut record = LayoutRecord::new("overflow_test");
1299 record.available_size = 100;
1300 record.computed_sizes = smallvec::smallvec![60u16, 60u16]; let jsonl = record.to_jsonl();
1303 assert!(jsonl.contains("\"has_overflow\":true"));
1304 }
1305
1306 #[test]
1307 fn layout_record_jsonl_underflow_flags() {
1308 let mut record = LayoutRecord::new("underflow_test");
1309 record.available_size = 100;
1310 record.computed_sizes = smallvec::smallvec![10u16, 10u16]; let jsonl = record.to_jsonl();
1313 assert!(jsonl.contains("\"has_underflow\":true"));
1314 }
1315}