Skip to main content

ftui_layout/
debug.rs

1#![forbid(unsafe_code)]
2
3//! Layout constraint debugging utilities.
4//!
5//! Provides introspection into layout constraint solving:
6//! - Recording of constraint solving steps
7//! - Detection of overflow/underflow conditions
8//! - Export to Graphviz DOT format
9//!
10//! # Feature Gating
11//!
12//! This module is always compiled (the types are useful for testing),
13//! but recording is a no-op unless explicitly enabled at runtime.
14//!
15//! # Usage
16//!
17//! ```ignore
18//! use ftui_layout::debug::{LayoutDebugger, LayoutRecord};
19//!
20//! let debugger = LayoutDebugger::new();
21//! debugger.set_enabled(true);
22//!
23//! // ... perform layout ...
24//!
25//! for record in debugger.snapshot() {
26//!     println!("{}: {:?} -> {:?}", record.name, record.constraints, record.computed_sizes);
27//!     if record.has_overflow() {
28//!         eprintln!("  WARNING: overflow detected!");
29//!     }
30//! }
31//! ```
32
33use 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/// A record of a single layout solve operation.
41#[derive(Debug, Clone)]
42pub struct LayoutRecord {
43    /// User-provided name for identification.
44    pub name: String,
45    /// The constraints that were solved.
46    pub constraints: Vec<Constraint>,
47    /// Total available size before solving.
48    pub available_size: u16,
49    /// Computed sizes for each constraint.
50    pub computed_sizes: Vec<u16>,
51    /// Layout direction.
52    pub direction: Direction,
53    /// Alignment mode.
54    pub alignment: Alignment,
55    /// Margin applied before solving.
56    pub margin: Sides,
57    /// Gap between items.
58    pub gap: u16,
59    /// The input area.
60    pub input_area: Rect,
61    /// The resulting rectangles.
62    pub result_rects: Vec<Rect>,
63    /// Time taken to solve (if measured).
64    pub solve_time: Option<Duration>,
65    /// Parent record index (for nested layouts).
66    pub parent_index: Option<usize>,
67}
68
69impl LayoutRecord {
70    /// Create a new layout record.
71    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    /// Check if the total computed size exceeds available space (overflow).
89    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    /// Check if significant space remains unused (underflow).
101    ///
102    /// Returns true if more than 20% of available space is unused.
103    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        // Consider underflow if >20% unused
114        self.available_size > 0 && (unused as f32 / self.available_size as f32) > 0.2
115    }
116
117    /// Percentage of available space used.
118    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    /// Format a single constraint for display.
134    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    /// Generate a human-readable summary.
149    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    /// Generate a JSONL-formatted record for structured logging.
189    ///
190    /// Returns a single-line JSON object suitable for appending to a log file.
191    #[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/// A record of a grid layout solve operation.
223#[derive(Debug, Clone)]
224pub struct GridLayoutRecord {
225    /// User-provided name for identification.
226    pub name: String,
227    /// Row constraints.
228    pub row_constraints: Vec<Constraint>,
229    /// Column constraints.
230    pub col_constraints: Vec<Constraint>,
231    /// Available width.
232    pub available_width: u16,
233    /// Available height.
234    pub available_height: u16,
235    /// Computed row heights.
236    pub row_heights: Vec<u16>,
237    /// Computed column widths.
238    pub col_widths: Vec<u16>,
239    /// The input area.
240    pub input_area: Rect,
241    /// Time taken to solve.
242    pub solve_time: Option<Duration>,
243}
244
245impl GridLayoutRecord {
246    /// Create a new grid layout record.
247    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    /// Check for row overflow.
262    pub fn has_row_overflow(&self) -> bool {
263        self.row_heights.iter().sum::<u16>() > self.available_height
264    }
265
266    /// Check for column overflow.
267    pub fn has_col_overflow(&self) -> bool {
268        self.col_widths.iter().sum::<u16>() > self.available_width
269    }
270
271    /// Generate a JSONL-formatted record for structured logging.
272    #[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
293/// Telemetry hooks for layout debugging observability (bd-32my.5).
294///
295/// Provides callback-based notifications for layout events, enabling
296/// external observability systems to monitor layout performance.
297///
298/// # Example
299///
300/// ```
301/// use ftui_layout::debug::{LayoutDebugger, LayoutTelemetryHooks, LayoutRecord};
302///
303/// let hooks = LayoutTelemetryHooks::new()
304///     .on_layout_solve(|record| {
305///         println!("Layout solved: {} ({:.1}% util)", record.name, record.utilization());
306///     })
307///     .on_overflow(|record| {
308///         eprintln!("OVERFLOW in {}", record.name);
309///     });
310///
311/// let debugger = LayoutDebugger::new();
312/// debugger.set_telemetry_hooks(hooks);
313/// ```
314type 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    /// Create a new hooks instance with no callbacks attached.
343    #[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    /// Attach a callback for flex layout solve events.
354    #[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    /// Attach a callback for grid layout solve events.
364    #[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    /// Attach a callback for layout overflow detection.
374    #[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    /// Attach a callback for layout underflow detection.
384    #[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    /// Fire the layout solve callback if attached.
394    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    /// Fire the grid solve callback if attached.
401    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    /// Fire the overflow callback if attached.
408    pub fn fire_overflow(&self, record: &LayoutRecord) {
409        if let Some(ref f) = self.on_overflow {
410            f(record);
411        }
412    }
413
414    /// Fire the underflow callback if attached.
415    pub fn fire_underflow(&self, record: &LayoutRecord) {
416        if let Some(ref f) = self.on_underflow {
417            f(record);
418        }
419    }
420}
421
422/// Layout constraint debugger.
423///
424/// Collects layout solve records for introspection. Thread-safe via internal
425/// synchronization; can be shared across the application.
426///
427/// Supports optional telemetry hooks for external observability (bd-32my.5).
428pub 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    /// Create a new debugger wrapped in Arc (disabled by default).
461    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    /// Attach telemetry hooks for external observability.
471    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    /// Remove telemetry hooks.
478    pub fn clear_telemetry_hooks(&self) {
479        if let Ok(mut h) = self.telemetry_hooks.lock() {
480            *h = None;
481        }
482    }
483
484    /// Check if debugging is enabled.
485    #[inline]
486    pub fn enabled(&self) -> bool {
487        self.enabled.load(Ordering::Relaxed)
488    }
489
490    /// Enable or disable debugging.
491    pub fn set_enabled(&self, enabled: bool) {
492        self.enabled.store(enabled, Ordering::Relaxed);
493    }
494
495    /// Toggle debugging on/off.
496    pub fn toggle(&self) -> bool {
497        !self.enabled.fetch_xor(true, Ordering::Relaxed)
498    }
499
500    /// Clear all recorded data.
501    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    /// Record a flex layout solve.
511    ///
512    /// Also fires telemetry hooks if attached:
513    /// - `on_layout_solve` for every recorded layout
514    /// - `on_overflow` if overflow detected
515    /// - `on_underflow` if underflow detected
516    pub fn record(&self, record: LayoutRecord) {
517        if !self.enabled() {
518            return;
519        }
520
521        // Fire telemetry hooks before recording
522        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    /// Record a grid layout solve.
540    ///
541    /// Also fires telemetry hooks if attached.
542    pub fn record_grid(&self, record: GridLayoutRecord) {
543        if !self.enabled() {
544            return;
545        }
546
547        // Fire telemetry hooks before recording
548        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    /// Get a snapshot of all flex layout records.
560    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    /// Get a snapshot of all grid layout records.
569    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    /// Get records with overflow conditions.
578    pub fn overflows(&self) -> Vec<LayoutRecord> {
579        self.snapshot()
580            .into_iter()
581            .filter(|r| r.has_overflow())
582            .collect()
583    }
584
585    /// Get records with underflow conditions.
586    pub fn underflows(&self) -> Vec<LayoutRecord> {
587        self.snapshot()
588            .into_iter()
589            .filter(|r| r.has_underflow())
590            .collect()
591    }
592
593    /// Generate a summary report of all recorded layouts.
594    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    /// Export to Graphviz DOT format for visualization.
652    ///
653    /// Each layout becomes a node, with edges representing parent-child
654    /// relationships (if parent_index is set).
655    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; // 45 + 15 + 45 = 105 > 100
726
727        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]; // 40% utilization
735        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]; // 85% utilization
745        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; // 20 + 10 + 20 = 50
766
767        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]; // Overflow
775
776        // Should clamp to 100%
777        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]; // 20% utilization
893        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]; // 90% utilization
898        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")); // Parent-child edge
943    }
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\"")); // Overflow
967        assert!(dot.contains("color=\"yellow\"")); // Underflow
968        assert!(dot.contains("color=\"green\"")); // Normal
969    }
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    // --- Telemetry tests (bd-32my.5) ---
1025
1026    #[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        // Verify it's valid single-line JSON (no newlines)
1045        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        // Record with overflow
1105        let mut overflow_record = LayoutRecord::new("overflow");
1106        overflow_record.available_size = 100;
1107        overflow_record.computed_sizes = vec![60, 60]; // 120 > 100
1108        debugger.record(overflow_record);
1109
1110        // Record without overflow
1111        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        // Only the overflow record should have triggered the hook
1117        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        // Record with underflow (< 80% utilization)
1135        let mut underflow_record = LayoutRecord::new("underflow");
1136        underflow_record.available_size = 100;
1137        underflow_record.computed_sizes = vec![10, 10]; // 20% utilization
1138        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        // Note: NOT enabled
1179        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        // Hook should not fire because debugger is disabled
1187        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        // Clear hooks
1212        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        // Counter should still be 1 (hooks cleared)
1220        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]; // Overflow
1228
1229        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]; // 20% utilization
1238
1239        let jsonl = record.to_jsonl();
1240        assert!(jsonl.contains("\"has_underflow\":true"));
1241    }
1242}