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: crate::Sizes,
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: crate::Rects,
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: 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    /// Check if the total computed size exceeds available space (overflow).
89    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    /// Check if significant space remains unused (underflow).
104    ///
105    /// Returns true if more than 20% of available space is unused.
106    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        // Consider underflow if >20% unused
120        self.available_size > 0 && (unused as f32 / self.available_size as f32) > 0.2
121    }
122
123    /// Percentage of available space used.
124    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    /// Format a single constraint for display.
143    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    /// Generate a human-readable summary.
158    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    /// Generate a JSONL-formatted record for structured logging.
198    ///
199    /// Returns a single-line JSON object suitable for appending to a log file.
200    #[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/// A record of a grid layout solve operation.
232#[derive(Debug, Clone)]
233pub struct GridLayoutRecord {
234    /// User-provided name for identification.
235    pub name: String,
236    /// Row constraints.
237    pub row_constraints: Vec<Constraint>,
238    /// Column constraints.
239    pub col_constraints: Vec<Constraint>,
240    /// Available width.
241    pub available_width: u16,
242    /// Available height.
243    pub available_height: u16,
244    /// Computed row heights.
245    pub row_heights: crate::Sizes,
246    /// Computed column widths.
247    pub col_widths: crate::Sizes,
248    /// The input area.
249    pub input_area: Rect,
250    /// Time taken to solve.
251    pub solve_time: Option<Duration>,
252}
253
254impl GridLayoutRecord {
255    /// Create a new grid layout record.
256    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    /// Check for row overflow.
271    pub fn has_row_overflow(&self) -> bool {
272        self.row_heights.iter().sum::<u16>() > self.available_height
273    }
274
275    /// Check for column overflow.
276    pub fn has_col_overflow(&self) -> bool {
277        self.col_widths.iter().sum::<u16>() > self.available_width
278    }
279
280    /// Generate a JSONL-formatted record for structured logging.
281    #[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
302/// Telemetry hooks for layout debugging observability (bd-32my.5).
303///
304/// Provides callback-based notifications for layout events, enabling
305/// external observability systems to monitor layout performance.
306///
307/// # Example
308///
309/// ```
310/// use ftui_layout::debug::{LayoutDebugger, LayoutTelemetryHooks, LayoutRecord};
311///
312/// let hooks = LayoutTelemetryHooks::new()
313///     .on_layout_solve(|record| {
314///         println!("Layout solved: {} ({:.1}% util)", record.name, record.utilization());
315///     })
316///     .on_overflow(|record| {
317///         eprintln!("OVERFLOW in {}", record.name);
318///     });
319///
320/// let debugger = LayoutDebugger::new();
321/// debugger.set_telemetry_hooks(hooks);
322/// ```
323type 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    /// Create a new hooks instance with no callbacks attached.
352    #[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    /// Attach a callback for flex layout solve events.
363    #[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    /// Attach a callback for grid layout solve events.
373    #[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    /// Attach a callback for layout overflow detection.
383    #[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    /// Attach a callback for layout underflow detection.
393    #[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    /// Fire the layout solve callback if attached.
403    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    /// Fire the grid solve callback if attached.
410    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    /// Fire the overflow callback if attached.
417    pub fn fire_overflow(&self, record: &LayoutRecord) {
418        if let Some(ref f) = self.on_overflow {
419            f(record);
420        }
421    }
422
423    /// Fire the underflow callback if attached.
424    pub fn fire_underflow(&self, record: &LayoutRecord) {
425        if let Some(ref f) = self.on_underflow {
426            f(record);
427        }
428    }
429}
430
431/// Layout constraint debugger.
432///
433/// Collects layout solve records for introspection. Thread-safe via internal
434/// synchronization; can be shared across the application.
435///
436/// Supports optional telemetry hooks for external observability (bd-32my.5).
437pub 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    /// Create a new debugger wrapped in Arc (disabled by default).
470    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    /// Attach telemetry hooks for external observability.
480    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    /// Remove telemetry hooks.
487    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    /// Check if debugging is enabled.
501    #[inline]
502    pub fn enabled(&self) -> bool {
503        self.enabled.load(Ordering::Relaxed)
504    }
505
506    /// Enable or disable debugging.
507    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    /// Toggle debugging on/off.
515    pub fn toggle(&self) -> bool {
516        let next = !self.enabled();
517        self.set_enabled(next);
518        next
519    }
520
521    /// Clear all recorded data.
522    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    /// Record a flex layout solve.
532    ///
533    /// Also fires telemetry hooks if attached:
534    /// - `on_layout_solve` for every recorded layout
535    /// - `on_overflow` if overflow detected
536    /// - `on_underflow` if underflow detected
537    pub fn record(&self, record: LayoutRecord) {
538        if !self.enabled() {
539            return;
540        }
541
542        // Fire telemetry hooks before recording
543        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    /// Record a grid layout solve.
562    ///
563    /// Also fires telemetry hooks if attached.
564    pub fn record_grid(&self, record: GridLayoutRecord) {
565        if !self.enabled() {
566            return;
567        }
568
569        // Fire telemetry hooks before recording
570        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    /// Get a snapshot of all flex layout records.
583    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    /// Get a snapshot of all grid layout records.
592    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    /// Get records with overflow conditions.
601    pub fn overflows(&self) -> Vec<LayoutRecord> {
602        self.snapshot()
603            .into_iter()
604            .filter(|r| r.has_overflow())
605            .collect()
606    }
607
608    /// Get records with underflow conditions.
609    pub fn underflows(&self) -> Vec<LayoutRecord> {
610        self.snapshot()
611            .into_iter()
612            .filter(|r| r.has_underflow())
613            .collect()
614    }
615
616    /// Generate a summary report of all recorded layouts.
617    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    /// Export to Graphviz DOT format for visualization.
675    ///
676    /// Each layout becomes a node, with edges representing parent-child
677    /// relationships (if parent_index is set).
678    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; // 45 + 15 + 45 = 105 > 100
749
750        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]; // 40% utilization
758        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]; // 85% utilization
768        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; // 20 + 10 + 20 = 50
789
790        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]; // Overflow
798
799        // Should clamp to 100%
800        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]; // 20% utilization
936        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]; // 90% utilization
941        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")); // Parent-child edge
986    }
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\"")); // Overflow
1010        assert!(dot.contains("color=\"yellow\"")); // Underflow
1011        assert!(dot.contains("color=\"green\"")); // Normal
1012    }
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    // --- Telemetry tests (bd-32my.5) ---
1068
1069    #[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        // Verify it's valid single-line JSON (no newlines)
1088        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        // Record with overflow
1148        let mut overflow_record = LayoutRecord::new("overflow");
1149        overflow_record.available_size = 100;
1150        overflow_record.computed_sizes = smallvec::smallvec![60u16, 60u16]; // 120 > 100
1151        debugger.record(overflow_record);
1152
1153        // Record without overflow
1154        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        // Only the overflow record should have triggered the hook
1160        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        // Record with underflow (< 80% utilization)
1178        let mut underflow_record = LayoutRecord::new("underflow");
1179        underflow_record.available_size = 100;
1180        underflow_record.computed_sizes = smallvec::smallvec![10u16, 10u16]; // 20% utilization
1181        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        // Note: NOT enabled
1222        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        // Hook should not fire because debugger is disabled
1230        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        // Clear hooks
1255        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        // Counter should still be 1 (hooks cleared)
1263        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]; // Overflow
1301
1302        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]; // 20% utilization
1311
1312        let jsonl = record.to_jsonl();
1313        assert!(jsonl.contains("\"has_underflow\":true"));
1314    }
1315}