#![forbid(unsafe_code)]
use crate::{Alignment, Constraint, Direction, Sides};
use ftui_core::geometry::Rect;
use std::fmt::Write as _;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct LayoutRecord {
pub name: String,
pub constraints: Vec<Constraint>,
pub available_size: u16,
pub computed_sizes: crate::Sizes,
pub direction: Direction,
pub alignment: Alignment,
pub margin: Sides,
pub gap: u16,
pub input_area: Rect,
pub result_rects: crate::Rects,
pub solve_time: Option<Duration>,
pub parent_index: Option<usize>,
}
impl LayoutRecord {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
constraints: Vec::new(),
available_size: 0,
computed_sizes: crate::Sizes::new(),
direction: Direction::default(),
alignment: Alignment::default(),
margin: Sides::default(),
gap: 0,
input_area: Rect::default(),
result_rects: crate::Rects::new(),
solve_time: None,
parent_index: None,
}
}
pub fn has_overflow(&self) -> bool {
let total_computed: u16 = self
.computed_sizes
.iter()
.fold(0u16, |acc, &s| acc.saturating_add(s));
let total_gaps = if self.computed_sizes.len() > 1 {
self.gap
.saturating_mul((self.computed_sizes.len() - 1) as u16)
} else {
0
};
total_computed.saturating_add(total_gaps) > self.available_size
}
pub fn has_underflow(&self) -> bool {
let total_computed: u16 = self
.computed_sizes
.iter()
.fold(0u16, |acc, &s| acc.saturating_add(s));
let total_gaps = if self.computed_sizes.len() > 1 {
self.gap
.saturating_mul((self.computed_sizes.len() - 1) as u16)
} else {
0
};
let total_used = total_computed.saturating_add(total_gaps);
let unused = self.available_size.saturating_sub(total_used);
self.available_size > 0 && (unused as f32 / self.available_size as f32) > 0.2
}
pub fn utilization(&self) -> f32 {
if self.available_size == 0 {
return 0.0;
}
let total_computed: u16 = self
.computed_sizes
.iter()
.fold(0u16, |acc, &s| acc.saturating_add(s));
let total_gaps = if self.computed_sizes.len() > 1 {
self.gap
.saturating_mul((self.computed_sizes.len() - 1) as u16)
} else {
0
};
let total_used = total_computed.saturating_add(total_gaps);
(total_used as f32 / self.available_size as f32).min(1.0) * 100.0
}
fn format_constraint(c: &Constraint) -> String {
match c {
Constraint::Fixed(n) => format!("Fixed({n})"),
Constraint::Percentage(p) => format!("Pct({p:.0}%)"),
Constraint::Min(n) => format!("Min({n})"),
Constraint::Max(n) => format!("Max({n})"),
Constraint::Ratio(n, d) => format!("Ratio({n}/{d})"),
Constraint::Fill => "Fill".to_string(),
Constraint::FitContent => "FitContent".to_string(),
Constraint::FitContentBounded { min, max } => format!("FitContent({min}..{max})"),
Constraint::FitMin => "FitMin".to_string(),
}
}
pub fn summary(&self) -> String {
let mut s = String::new();
let _ = writeln!(s, "{} ({:?}):", self.name, self.direction);
let _ = writeln!(
s,
" Input: {}x{} at ({},{})",
self.input_area.width, self.input_area.height, self.input_area.x, self.input_area.y
);
let _ = writeln!(s, " Available: {} (after margin)", self.available_size);
let _ = writeln!(s, " Gap: {}", self.gap);
for (i, (constraint, size)) in self
.constraints
.iter()
.zip(self.computed_sizes.iter())
.enumerate()
{
let constraint_str = Self::format_constraint(constraint);
let rect = self.result_rects.get(i);
let rect_str = rect.map_or_else(
|| "?".to_string(),
|r| format!("({},{} {}x{})", r.x, r.y, r.width, r.height),
);
let _ = writeln!(s, " [{i}] {constraint_str} -> {size} @ {rect_str}");
}
let _ = writeln!(s, " Utilization: {:.1}%", self.utilization());
if self.has_overflow() {
let _ = writeln!(s, " âš OVERFLOW");
}
if self.has_underflow() {
let _ = writeln!(s, " âš UNDERFLOW (>20% unused)");
}
if let Some(t) = self.solve_time {
let _ = writeln!(s, " Solve time: {:?}", t);
}
s
}
#[must_use]
pub fn to_jsonl(&self) -> String {
let constraints_json: Vec<String> = self
.constraints
.iter()
.map(|c| format!("\"{}\"", Self::format_constraint(c)))
.collect();
let sizes_json: Vec<String> = self.computed_sizes.iter().map(|s| s.to_string()).collect();
let solve_time_us = self.solve_time.map(|d| d.as_micros() as u64).unwrap_or(0);
format!(
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":{}}}"#,
self.name,
self.direction,
self.alignment,
self.available_size,
self.gap,
self.margin.top,
self.margin.right,
self.margin.bottom,
self.margin.left,
constraints_json.join(","),
sizes_json.join(","),
self.utilization(),
self.has_overflow(),
self.has_underflow(),
solve_time_us
)
}
}
#[derive(Debug, Clone)]
pub struct GridLayoutRecord {
pub name: String,
pub row_constraints: Vec<Constraint>,
pub col_constraints: Vec<Constraint>,
pub available_width: u16,
pub available_height: u16,
pub row_heights: crate::Sizes,
pub col_widths: crate::Sizes,
pub input_area: Rect,
pub solve_time: Option<Duration>,
}
impl GridLayoutRecord {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
row_constraints: Vec::new(),
col_constraints: Vec::new(),
available_width: 0,
available_height: 0,
row_heights: crate::Sizes::new(),
col_widths: crate::Sizes::new(),
input_area: Rect::default(),
solve_time: None,
}
}
pub fn has_row_overflow(&self) -> bool {
self.row_heights.iter().sum::<u16>() > self.available_height
}
pub fn has_col_overflow(&self) -> bool {
self.col_widths.iter().sum::<u16>() > self.available_width
}
#[must_use]
pub fn to_jsonl(&self) -> String {
let row_heights_json: Vec<String> =
self.row_heights.iter().map(|h| h.to_string()).collect();
let col_widths_json: Vec<String> = self.col_widths.iter().map(|w| w.to_string()).collect();
let solve_time_us = self.solve_time.map(|d| d.as_micros() as u64).unwrap_or(0);
format!(
r#"{{"event":"grid_layout_solve","name":"{}","available_width":{},"available_height":{},"row_heights":[{}],"col_widths":[{}],"has_row_overflow":{},"has_col_overflow":{},"solve_time_us":{}}}"#,
self.name,
self.available_width,
self.available_height,
row_heights_json.join(","),
col_widths_json.join(","),
self.has_row_overflow(),
self.has_col_overflow(),
solve_time_us
)
}
}
type LayoutHook = Box<dyn Fn(&LayoutRecord) + Send + Sync>;
type GridHook = Box<dyn Fn(&GridLayoutRecord) + Send + Sync>;
pub struct LayoutTelemetryHooks {
on_layout_solve: Option<LayoutHook>,
on_grid_solve: Option<GridHook>,
on_overflow: Option<LayoutHook>,
on_underflow: Option<LayoutHook>,
}
impl Default for LayoutTelemetryHooks {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for LayoutTelemetryHooks {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LayoutTelemetryHooks")
.field("on_layout_solve", &self.on_layout_solve.is_some())
.field("on_grid_solve", &self.on_grid_solve.is_some())
.field("on_overflow", &self.on_overflow.is_some())
.field("on_underflow", &self.on_underflow.is_some())
.finish()
}
}
impl LayoutTelemetryHooks {
#[must_use]
pub fn new() -> Self {
Self {
on_layout_solve: None,
on_grid_solve: None,
on_overflow: None,
on_underflow: None,
}
}
#[must_use]
pub fn on_layout_solve<F>(mut self, f: F) -> Self
where
F: Fn(&LayoutRecord) + Send + Sync + 'static,
{
self.on_layout_solve = Some(Box::new(f));
self
}
#[must_use]
pub fn on_grid_solve<F>(mut self, f: F) -> Self
where
F: Fn(&GridLayoutRecord) + Send + Sync + 'static,
{
self.on_grid_solve = Some(Box::new(f));
self
}
#[must_use]
pub fn on_overflow<F>(mut self, f: F) -> Self
where
F: Fn(&LayoutRecord) + Send + Sync + 'static,
{
self.on_overflow = Some(Box::new(f));
self
}
#[must_use]
pub fn on_underflow<F>(mut self, f: F) -> Self
where
F: Fn(&LayoutRecord) + Send + Sync + 'static,
{
self.on_underflow = Some(Box::new(f));
self
}
pub fn fire_layout_solve(&self, record: &LayoutRecord) {
if let Some(ref f) = self.on_layout_solve {
f(record);
}
}
pub fn fire_grid_solve(&self, record: &GridLayoutRecord) {
if let Some(ref f) = self.on_grid_solve {
f(record);
}
}
pub fn fire_overflow(&self, record: &LayoutRecord) {
if let Some(ref f) = self.on_overflow {
f(record);
}
}
pub fn fire_underflow(&self, record: &LayoutRecord) {
if let Some(ref f) = self.on_underflow {
f(record);
}
}
}
pub struct LayoutDebugger {
enabled: AtomicBool,
records: Mutex<Vec<LayoutRecord>>,
grid_records: Mutex<Vec<GridLayoutRecord>>,
telemetry_hooks: Mutex<Option<Arc<LayoutTelemetryHooks>>>,
}
impl std::fmt::Debug for LayoutDebugger {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LayoutDebugger")
.field("enabled", &self.enabled.load(Ordering::Relaxed))
.field(
"records_count",
&self.records.lock().map(|r| r.len()).unwrap_or(0),
)
.field(
"grid_records_count",
&self.grid_records.lock().map(|r| r.len()).unwrap_or(0),
)
.field(
"has_telemetry_hooks",
&self
.telemetry_hooks
.lock()
.map(|h| h.is_some())
.unwrap_or(false),
)
.finish()
}
}
impl LayoutDebugger {
pub fn new() -> Arc<Self> {
Arc::new(Self {
enabled: AtomicBool::new(false),
records: Mutex::new(Vec::new()),
grid_records: Mutex::new(Vec::new()),
telemetry_hooks: Mutex::new(None),
})
}
pub fn set_telemetry_hooks(&self, hooks: LayoutTelemetryHooks) {
if let Ok(mut h) = self.telemetry_hooks.lock() {
*h = Some(Arc::new(hooks));
}
}
pub fn clear_telemetry_hooks(&self) {
if let Ok(mut h) = self.telemetry_hooks.lock() {
*h = None;
}
}
fn telemetry_hooks_snapshot(&self) -> Option<Arc<LayoutTelemetryHooks>> {
self.telemetry_hooks
.lock()
.ok()
.and_then(|hooks| hooks.clone())
}
#[inline]
pub fn enabled(&self) -> bool {
self.enabled.load(Ordering::Relaxed)
}
pub fn set_enabled(&self, enabled: bool) {
let was_enabled = self.enabled.swap(enabled, Ordering::Relaxed);
if was_enabled && !enabled {
self.clear();
}
}
pub fn toggle(&self) -> bool {
let next = !self.enabled();
self.set_enabled(next);
next
}
pub fn clear(&self) {
if let Ok(mut records) = self.records.lock() {
records.clear();
}
if let Ok(mut grid_records) = self.grid_records.lock() {
grid_records.clear();
}
}
pub fn record(&self, record: LayoutRecord) {
if !self.enabled() {
return;
}
if let Some(h) = self.telemetry_hooks_snapshot() {
h.fire_layout_solve(&record);
if record.has_overflow() {
h.fire_overflow(&record);
}
if record.has_underflow() {
h.fire_underflow(&record);
}
}
if let Ok(mut records) = self.records.lock() {
if !self.enabled() {
return;
}
records.push(record);
}
}
pub fn record_grid(&self, record: GridLayoutRecord) {
if !self.enabled() {
return;
}
if let Some(h) = self.telemetry_hooks_snapshot() {
h.fire_grid_solve(&record);
}
if let Ok(mut grid_records) = self.grid_records.lock() {
if !self.enabled() {
return;
}
grid_records.push(record);
}
}
pub fn snapshot(&self) -> Vec<LayoutRecord> {
self.records
.lock()
.ok()
.map(|r| r.clone())
.unwrap_or_default()
}
pub fn snapshot_grids(&self) -> Vec<GridLayoutRecord> {
self.grid_records
.lock()
.ok()
.map(|r| r.clone())
.unwrap_or_default()
}
pub fn overflows(&self) -> Vec<LayoutRecord> {
self.snapshot()
.into_iter()
.filter(|r| r.has_overflow())
.collect()
}
pub fn underflows(&self) -> Vec<LayoutRecord> {
self.snapshot()
.into_iter()
.filter(|r| r.has_underflow())
.collect()
}
pub fn report(&self) -> String {
let records = self.snapshot();
let grid_records = self.snapshot_grids();
let mut s = String::new();
let _ = writeln!(
s,
"=== Layout Debug Report ({} flex, {} grid) ===",
records.len(),
grid_records.len()
);
let overflows: Vec<_> = records.iter().filter(|r| r.has_overflow()).collect();
let underflows: Vec<_> = records.iter().filter(|r| r.has_underflow()).collect();
if !overflows.is_empty() {
let _ = writeln!(s, "\nâš {} layouts have OVERFLOW:", overflows.len());
for r in &overflows {
let _ = writeln!(s, " - {}", r.name);
}
}
if !underflows.is_empty() {
let _ = writeln!(s, "\nâš {} layouts have UNDERFLOW:", underflows.len());
for r in &underflows {
let _ = writeln!(s, " - {} ({:.1}% utilization)", r.name, r.utilization());
}
}
let _ = writeln!(s, "\n--- Flex Layouts ---");
for record in &records {
let _ = write!(s, "\n{}", record.summary());
}
if !grid_records.is_empty() {
let _ = writeln!(s, "\n--- Grid Layouts ---");
for record in &grid_records {
let _ = writeln!(s, "\n{} (Grid):", record.name);
let _ = writeln!(
s,
" Input: {}x{}",
record.input_area.width, record.input_area.height
);
let _ = writeln!(s, " Rows: {:?}", record.row_heights);
let _ = writeln!(s, " Cols: {:?}", record.col_widths);
if record.has_row_overflow() {
let _ = writeln!(s, " âš ROW OVERFLOW");
}
if record.has_col_overflow() {
let _ = writeln!(s, " âš COLUMN OVERFLOW");
}
}
}
s
}
pub fn export_dot(&self) -> String {
let records = self.snapshot();
let mut s = String::new();
let _ = writeln!(s, "digraph LayoutDebug {{");
let _ = writeln!(s, " rankdir=TB;");
let _ = writeln!(s, " node [shape=record];");
for (i, r) in records.iter().enumerate() {
let color = if r.has_overflow() {
"red"
} else if r.has_underflow() {
"yellow"
} else {
"green"
};
let label = format!(
"{}|dir: {:?}|avail: {}|util: {:.0}%",
r.name,
r.direction,
r.available_size,
r.utilization()
);
let _ = writeln!(
s,
" n{} [label=\"{{{}}}\", color=\"{}\"];",
i, label, color
);
if let Some(parent) = r.parent_index {
let _ = writeln!(s, " n{} -> n{};", parent, i);
}
}
let _ = writeln!(s, "}}");
s
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn layout_record_overflow_detection() {
let mut record = LayoutRecord::new("test");
record.available_size = 100;
record.computed_sizes = smallvec::smallvec![60u16, 60u16];
record.gap = 0;
assert!(record.has_overflow());
}
#[test]
fn layout_record_no_overflow() {
let mut record = LayoutRecord::new("test");
record.available_size = 100;
record.computed_sizes = smallvec::smallvec![40u16, 40u16];
record.gap = 0;
assert!(!record.has_overflow());
}
#[test]
fn layout_record_overflow_with_gaps() {
let mut record = LayoutRecord::new("test");
record.available_size = 100;
record.computed_sizes = smallvec::smallvec![45u16, 45u16];
record.gap = 15;
assert!(record.has_overflow());
}
#[test]
fn layout_record_underflow_detection() {
let mut record = LayoutRecord::new("test");
record.available_size = 100;
record.computed_sizes = smallvec::smallvec![20u16, 20u16]; record.gap = 0;
assert!(record.has_underflow());
}
#[test]
fn layout_record_no_underflow() {
let mut record = LayoutRecord::new("test");
record.available_size = 100;
record.computed_sizes = smallvec::smallvec![40u16, 45u16]; record.gap = 0;
assert!(!record.has_underflow());
}
#[test]
fn layout_record_utilization() {
let mut record = LayoutRecord::new("test");
record.available_size = 100;
record.computed_sizes = smallvec::smallvec![25u16, 25u16];
record.gap = 0;
assert!((record.utilization() - 50.0).abs() < 0.1);
}
#[test]
fn layout_record_utilization_with_gap() {
let mut record = LayoutRecord::new("test");
record.available_size = 100;
record.computed_sizes = smallvec::smallvec![20u16, 20u16];
record.gap = 10;
assert!((record.utilization() - 50.0).abs() < 0.1);
}
#[test]
fn layout_record_utilization_clamped() {
let mut record = LayoutRecord::new("test");
record.available_size = 100;
record.computed_sizes = smallvec::smallvec![150u16];
assert!((record.utilization() - 100.0).abs() < 0.1);
}
#[test]
fn layout_record_zero_available() {
let mut record = LayoutRecord::new("test");
record.available_size = 0;
record.computed_sizes = crate::Sizes::new();
record.gap = 0;
assert!(!record.has_overflow());
assert!(!record.has_underflow());
assert!((record.utilization() - 0.0).abs() < 0.1);
}
#[test]
fn layout_record_summary() {
let mut record = LayoutRecord::new("main_layout");
record.constraints = vec![Constraint::Fixed(30), Constraint::Min(10)];
record.available_size = 100;
record.computed_sizes = smallvec::smallvec![30u16, 70u16];
record.direction = Direction::Horizontal;
record.input_area = Rect::new(0, 0, 100, 50);
record.result_rects =
smallvec::smallvec![Rect::new(0, 0, 30, 50), Rect::new(30, 0, 70, 50)];
let summary = record.summary();
assert!(summary.contains("main_layout"));
assert!(summary.contains("Horizontal"));
assert!(summary.contains("Fixed(30)"));
assert!(summary.contains("Min(10)"));
}
#[test]
fn debugger_disabled_by_default() {
let debugger = LayoutDebugger::new();
assert!(!debugger.enabled());
}
#[test]
fn debugger_enable_disable() {
let debugger = LayoutDebugger::new();
debugger.set_enabled(true);
assert!(debugger.enabled());
debugger.record(LayoutRecord::new("stale"));
assert_eq!(debugger.snapshot().len(), 1);
debugger.set_enabled(false);
assert!(!debugger.enabled());
assert!(debugger.snapshot().is_empty());
assert!(debugger.snapshot_grids().is_empty());
}
#[test]
fn debugger_toggle() {
let debugger = LayoutDebugger::new();
assert!(!debugger.enabled());
let result = debugger.toggle();
assert!(result);
assert!(debugger.enabled());
debugger.record(LayoutRecord::new("stale"));
assert_eq!(debugger.snapshot().len(), 1);
let result = debugger.toggle();
assert!(!result);
assert!(!debugger.enabled());
assert!(debugger.snapshot().is_empty());
}
#[test]
fn debugger_disable_clears_grid_records() {
let debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.record_grid(GridLayoutRecord::new("grid"));
assert_eq!(debugger.snapshot_grids().len(), 1);
debugger.set_enabled(false);
assert!(debugger.snapshot_grids().is_empty());
}
#[test]
fn debugger_record_when_disabled() {
let debugger = LayoutDebugger::new();
debugger.record(LayoutRecord::new("test"));
assert!(debugger.snapshot().is_empty());
}
#[test]
fn debugger_record_when_enabled() {
let debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.record(LayoutRecord::new("test"));
let records = debugger.snapshot();
assert_eq!(records.len(), 1);
assert_eq!(records[0].name, "test");
}
#[test]
fn debugger_clear() {
let debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.record(LayoutRecord::new("test1"));
debugger.record(LayoutRecord::new("test2"));
assert_eq!(debugger.snapshot().len(), 2);
debugger.clear();
assert!(debugger.snapshot().is_empty());
}
#[test]
fn debugger_overflows() {
let debugger = LayoutDebugger::new();
debugger.set_enabled(true);
let mut overflow_record = LayoutRecord::new("overflow");
overflow_record.available_size = 100;
overflow_record.computed_sizes = smallvec::smallvec![60u16, 60u16];
debugger.record(overflow_record);
let mut normal_record = LayoutRecord::new("normal");
normal_record.available_size = 100;
normal_record.computed_sizes = smallvec::smallvec![30u16, 30u16];
debugger.record(normal_record);
let overflows = debugger.overflows();
assert_eq!(overflows.len(), 1);
assert_eq!(overflows[0].name, "overflow");
}
#[test]
fn debugger_underflows() {
let debugger = LayoutDebugger::new();
debugger.set_enabled(true);
let mut underflow_record = LayoutRecord::new("underflow");
underflow_record.available_size = 100;
underflow_record.computed_sizes = smallvec::smallvec![10u16, 10u16]; debugger.record(underflow_record);
let mut normal_record = LayoutRecord::new("normal");
normal_record.available_size = 100;
normal_record.computed_sizes = smallvec::smallvec![45u16, 45u16]; debugger.record(normal_record);
let underflows = debugger.underflows();
assert_eq!(underflows.len(), 1);
assert_eq!(underflows[0].name, "underflow");
}
#[test]
fn debugger_report() {
let debugger = LayoutDebugger::new();
debugger.set_enabled(true);
let mut record = LayoutRecord::new("test_layout");
record.available_size = 100;
record.computed_sizes = smallvec::smallvec![50u16, 50u16];
record.direction = Direction::Horizontal;
debugger.record(record);
let report = debugger.report();
assert!(report.contains("Layout Debug Report"));
assert!(report.contains("test_layout"));
}
#[test]
fn debugger_export_dot() {
let debugger = LayoutDebugger::new();
debugger.set_enabled(true);
let mut record = LayoutRecord::new("root");
record.available_size = 100;
record.computed_sizes = smallvec::smallvec![50u16, 50u16];
record.direction = Direction::Vertical;
debugger.record(record);
let mut child = LayoutRecord::new("child");
child.available_size = 50;
child.computed_sizes = smallvec::smallvec![25u16, 25u16];
child.parent_index = Some(0);
debugger.record(child);
let dot = debugger.export_dot();
assert!(dot.contains("digraph LayoutDebug"));
assert!(dot.contains("root"));
assert!(dot.contains("child"));
assert!(dot.contains("n0 -> n1")); }
#[test]
fn debugger_export_dot_colors() {
let debugger = LayoutDebugger::new();
debugger.set_enabled(true);
let mut overflow = LayoutRecord::new("overflow");
overflow.available_size = 100;
overflow.computed_sizes = smallvec::smallvec![120u16];
debugger.record(overflow);
let mut underflow = LayoutRecord::new("underflow");
underflow.available_size = 100;
underflow.computed_sizes = smallvec::smallvec![10u16];
debugger.record(underflow);
let mut normal = LayoutRecord::new("normal");
normal.available_size = 100;
normal.computed_sizes = smallvec::smallvec![90u16];
debugger.record(normal);
let dot = debugger.export_dot();
assert!(dot.contains("color=\"red\"")); assert!(dot.contains("color=\"yellow\"")); assert!(dot.contains("color=\"green\"")); }
#[test]
fn grid_record_overflow() {
let mut record = GridLayoutRecord::new("grid");
record.available_width = 100;
record.available_height = 100;
record.row_heights = smallvec::smallvec![60u16, 60u16];
record.col_widths = smallvec::smallvec![50u16, 50u16];
assert!(record.has_row_overflow());
assert!(!record.has_col_overflow());
}
#[test]
fn debugger_record_grid() {
let debugger = LayoutDebugger::new();
debugger.set_enabled(true);
let mut record = GridLayoutRecord::new("grid");
record.available_width = 100;
record.available_height = 100;
record.row_heights = smallvec::smallvec![50u16, 50u16];
record.col_widths = smallvec::smallvec![50u16, 50u16];
debugger.record_grid(record);
let records = debugger.snapshot_grids();
assert_eq!(records.len(), 1);
assert_eq!(records[0].name, "grid");
}
#[test]
fn format_constraint_all_types() {
assert_eq!(
LayoutRecord::format_constraint(&Constraint::Fixed(10)),
"Fixed(10)"
);
assert_eq!(
LayoutRecord::format_constraint(&Constraint::Percentage(50.0)),
"Pct(50%)"
);
assert_eq!(
LayoutRecord::format_constraint(&Constraint::Min(5)),
"Min(5)"
);
assert_eq!(
LayoutRecord::format_constraint(&Constraint::Max(20)),
"Max(20)"
);
assert_eq!(
LayoutRecord::format_constraint(&Constraint::Ratio(1, 3)),
"Ratio(1/3)"
);
}
#[test]
fn layout_record_to_jsonl() {
let mut record = LayoutRecord::new("test_layout");
record.constraints = vec![Constraint::Fixed(30), Constraint::Min(10)];
record.available_size = 100;
record.computed_sizes = smallvec::smallvec![30u16, 70u16];
record.direction = Direction::Horizontal;
record.gap = 2;
let jsonl = record.to_jsonl();
assert!(jsonl.contains("\"event\":\"layout_solve\""));
assert!(jsonl.contains("\"name\":\"test_layout\""));
assert!(jsonl.contains("\"direction\":\"Horizontal\""));
assert!(jsonl.contains("\"available_size\":100"));
assert!(jsonl.contains("\"gap\":2"));
assert!(jsonl.contains("\"Fixed(30)\""));
assert!(jsonl.contains("\"Min(10)\""));
assert!(jsonl.contains("\"computed_sizes\":[30,70]"));
assert!(!jsonl.contains('\n'));
}
#[test]
fn grid_record_to_jsonl() {
let mut record = GridLayoutRecord::new("test_grid");
record.available_width = 100;
record.available_height = 50;
record.row_heights = smallvec::smallvec![10u16, 20u16, 20u16];
record.col_widths = smallvec::smallvec![30u16, 30u16, 40u16];
let jsonl = record.to_jsonl();
assert!(jsonl.contains("\"event\":\"grid_layout_solve\""));
assert!(jsonl.contains("\"name\":\"test_grid\""));
assert!(jsonl.contains("\"available_width\":100"));
assert!(jsonl.contains("\"available_height\":50"));
assert!(jsonl.contains("\"row_heights\":[10,20,20]"));
assert!(jsonl.contains("\"col_widths\":[30,30,40]"));
assert!(jsonl.contains("\"has_row_overflow\":false"));
assert!(jsonl.contains("\"has_col_overflow\":false"));
assert!(!jsonl.contains('\n'));
}
#[test]
fn telemetry_hooks_fire_on_layout_solve() {
use std::sync::atomic::{AtomicU32, Ordering};
let counter = Arc::new(AtomicU32::new(0));
let counter_clone = counter.clone();
let hooks = LayoutTelemetryHooks::new().on_layout_solve(move |_record| {
counter_clone.fetch_add(1, Ordering::SeqCst);
});
let debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.set_telemetry_hooks(hooks);
let mut record = LayoutRecord::new("test");
record.available_size = 100;
record.computed_sizes = smallvec::smallvec![50u16, 50u16];
debugger.record(record);
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
#[test]
fn telemetry_hooks_fire_on_overflow() {
use std::sync::atomic::{AtomicU32, Ordering};
let overflow_counter = Arc::new(AtomicU32::new(0));
let overflow_clone = overflow_counter.clone();
let hooks = LayoutTelemetryHooks::new().on_overflow(move |_record| {
overflow_clone.fetch_add(1, Ordering::SeqCst);
});
let debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.set_telemetry_hooks(hooks);
let mut overflow_record = LayoutRecord::new("overflow");
overflow_record.available_size = 100;
overflow_record.computed_sizes = smallvec::smallvec![60u16, 60u16]; debugger.record(overflow_record);
let mut normal_record = LayoutRecord::new("normal");
normal_record.available_size = 100;
normal_record.computed_sizes = smallvec::smallvec![30u16, 30u16];
debugger.record(normal_record);
assert_eq!(overflow_counter.load(Ordering::SeqCst), 1);
}
#[test]
fn telemetry_hooks_fire_on_underflow() {
use std::sync::atomic::{AtomicU32, Ordering};
let underflow_counter = Arc::new(AtomicU32::new(0));
let underflow_clone = underflow_counter.clone();
let hooks = LayoutTelemetryHooks::new().on_underflow(move |_record| {
underflow_clone.fetch_add(1, Ordering::SeqCst);
});
let debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.set_telemetry_hooks(hooks);
let mut underflow_record = LayoutRecord::new("underflow");
underflow_record.available_size = 100;
underflow_record.computed_sizes = smallvec::smallvec![10u16, 10u16]; debugger.record(underflow_record);
assert_eq!(underflow_counter.load(Ordering::SeqCst), 1);
}
#[test]
fn telemetry_hooks_fire_on_grid_solve() {
use std::sync::atomic::{AtomicU32, Ordering};
let counter = Arc::new(AtomicU32::new(0));
let counter_clone = counter.clone();
let hooks = LayoutTelemetryHooks::new().on_grid_solve(move |_record| {
counter_clone.fetch_add(1, Ordering::SeqCst);
});
let debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.set_telemetry_hooks(hooks);
let mut record = GridLayoutRecord::new("grid");
record.available_width = 100;
record.available_height = 50;
record.row_heights = smallvec::smallvec![25u16, 25u16];
record.col_widths = smallvec::smallvec![50u16, 50u16];
debugger.record_grid(record);
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
#[test]
fn telemetry_hooks_not_fired_when_disabled() {
use std::sync::atomic::{AtomicU32, Ordering};
let counter = Arc::new(AtomicU32::new(0));
let counter_clone = counter.clone();
let hooks = LayoutTelemetryHooks::new().on_layout_solve(move |_record| {
counter_clone.fetch_add(1, Ordering::SeqCst);
});
let debugger = LayoutDebugger::new();
debugger.set_telemetry_hooks(hooks);
let mut record = LayoutRecord::new("test");
record.available_size = 100;
record.computed_sizes = smallvec::smallvec![50u16, 50u16];
debugger.record(record);
assert_eq!(counter.load(Ordering::SeqCst), 0);
}
#[test]
fn clear_telemetry_hooks() {
use std::sync::atomic::{AtomicU32, Ordering};
let counter = Arc::new(AtomicU32::new(0));
let counter_clone = counter.clone();
let hooks = LayoutTelemetryHooks::new().on_layout_solve(move |_record| {
counter_clone.fetch_add(1, Ordering::SeqCst);
});
let debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.set_telemetry_hooks(hooks);
let mut record1 = LayoutRecord::new("test1");
record1.available_size = 100;
record1.computed_sizes = smallvec::smallvec![50u16, 50u16];
debugger.record(record1);
assert_eq!(counter.load(Ordering::SeqCst), 1);
debugger.clear_telemetry_hooks();
let mut record2 = LayoutRecord::new("test2");
record2.available_size = 100;
record2.computed_sizes = smallvec::smallvec![50u16, 50u16];
debugger.record(record2);
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
#[test]
fn telemetry_hook_can_clear_hooks_reentrantly_without_deadlocking() {
use std::sync::mpsc;
use std::time::Duration;
let debugger = LayoutDebugger::new();
debugger.set_enabled(true);
let (done_tx, done_rx) = mpsc::channel();
let debugger_for_hook = Arc::clone(&debugger);
let hooks = LayoutTelemetryHooks::new().on_layout_solve(move |_record| {
debugger_for_hook.clear_telemetry_hooks();
done_tx.send(()).expect("completion signal");
});
debugger.set_telemetry_hooks(hooks);
let debugger_for_thread = Arc::clone(&debugger);
let handle = std::thread::spawn(move || {
let mut record = LayoutRecord::new("reentrant_clear");
record.available_size = 100;
record.computed_sizes = smallvec::smallvec![50u16, 50u16];
debugger_for_thread.record(record);
});
done_rx
.recv_timeout(Duration::from_secs(1))
.expect("reentrant hook should complete without deadlocking");
handle.join().expect("layout debug thread");
}
#[test]
fn layout_record_jsonl_overflow_flags() {
let mut record = LayoutRecord::new("overflow_test");
record.available_size = 100;
record.computed_sizes = smallvec::smallvec![60u16, 60u16];
let jsonl = record.to_jsonl();
assert!(jsonl.contains("\"has_overflow\":true"));
}
#[test]
fn layout_record_jsonl_underflow_flags() {
let mut record = LayoutRecord::new("underflow_test");
record.available_size = 100;
record.computed_sizes = smallvec::smallvec![10u16, 10u16];
let jsonl = record.to_jsonl();
assert!(jsonl.contains("\"has_underflow\":true"));
}
}