#![forbid(unsafe_code)]
use crate::Widget;
use crate::layout_debugger::{LayoutDebugger, LayoutRecord};
use ftui_core::geometry::Rect;
use ftui_render::buffer::Buffer;
use ftui_render::cell::{Cell, PackedRgba};
use ftui_render::drawing::{BorderChars, Draw};
use ftui_render::frame::Frame;
#[derive(Debug, Clone)]
pub struct ConstraintOverlayStyle {
pub normal_color: PackedRgba,
pub overflow_color: PackedRgba,
pub underflow_color: PackedRgba,
pub requested_color: PackedRgba,
pub label_fg: PackedRgba,
pub label_bg: PackedRgba,
pub show_size_diff: bool,
pub show_constraint_bounds: bool,
pub show_borders: bool,
pub show_labels: bool,
pub border_chars: BorderChars,
}
impl Default for ConstraintOverlayStyle {
fn default() -> Self {
Self {
normal_color: PackedRgba::rgb(100, 200, 100),
overflow_color: PackedRgba::rgb(240, 80, 80),
underflow_color: PackedRgba::rgb(240, 200, 80),
requested_color: PackedRgba::rgb(80, 150, 240),
label_fg: PackedRgba::rgb(255, 255, 255),
label_bg: PackedRgba::rgb(0, 0, 0),
show_size_diff: true,
show_constraint_bounds: true,
show_borders: true,
show_labels: true,
border_chars: BorderChars::ASCII,
}
}
}
pub struct ConstraintOverlay<'a> {
debugger: &'a LayoutDebugger,
style: ConstraintOverlayStyle,
}
impl<'a> ConstraintOverlay<'a> {
pub fn new(debugger: &'a LayoutDebugger) -> Self {
Self {
debugger,
style: ConstraintOverlayStyle::default(),
}
}
#[must_use]
pub fn style(mut self, style: ConstraintOverlayStyle) -> Self {
self.style = style;
self
}
fn render_record(&self, record: &LayoutRecord, area: Rect, buf: &mut Buffer) {
let Some(clipped) = record.area_received.intersection_opt(&area) else {
return;
};
if clipped.is_empty() {
return;
}
let constraints = &record.constraints;
let received = &record.area_received;
let is_overflow = (constraints.max_width != 0 && received.width > constraints.max_width)
|| (constraints.max_height != 0 && received.height > constraints.max_height);
let is_underflow =
received.width < constraints.min_width || received.height < constraints.min_height;
let border_color = if is_overflow {
self.style.overflow_color
} else if is_underflow {
self.style.underflow_color
} else {
self.style.normal_color
};
if self.style.show_borders {
let border_cell = Cell::from_char('+').with_fg(border_color);
buf.draw_border(clipped, self.style.border_chars, border_cell);
}
if self.style.show_size_diff {
let requested = &record.area_requested;
if requested != received
&& let Some(req_clipped) = requested.intersection_opt(&area)
&& !req_clipped.is_empty()
{
let req_cell = Cell::from_char('.').with_fg(self.style.requested_color);
self.draw_requested_outline(req_clipped, buf, req_cell);
}
}
if self.style.show_labels {
let label = self.format_label(record, is_overflow, is_underflow);
let label_x = clipped.x.saturating_add(1);
let label_y = clipped.y;
let max_x = clipped.right();
if label_x < max_x {
let label_cell = Cell::from_char(' ')
.with_fg(self.style.label_fg)
.with_bg(self.style.label_bg);
let _ = buf.print_text_clipped(label_x, label_y, &label, label_cell, max_x);
}
}
for child in &record.children {
self.render_record(child, area, buf);
}
}
fn draw_requested_outline(&self, area: Rect, buf: &mut Buffer, cell: Cell) {
if area.width >= 1 && area.height >= 1 {
buf.set_fast(area.x, area.y, cell);
}
if area.width >= 2 && area.height >= 1 {
buf.set_fast(area.right().saturating_sub(1), area.y, cell);
}
if area.width >= 1 && area.height >= 2 {
buf.set_fast(area.x, area.bottom().saturating_sub(1), cell);
}
if area.width >= 2 && area.height >= 2 {
buf.set_fast(
area.right().saturating_sub(1),
area.bottom().saturating_sub(1),
cell,
);
}
}
fn format_label(&self, record: &LayoutRecord, is_overflow: bool, is_underflow: bool) -> String {
let status = if is_overflow {
"!"
} else if is_underflow {
"?"
} else {
""
};
let mut label = format!("{}{}", record.widget_name, status);
let req = &record.area_requested;
let got = &record.area_received;
if self.style.show_size_diff && (req.width != got.width || req.height != got.height) {
label.push_str(&format!(
" {}x{}\u{2192}{}x{}",
req.width, req.height, got.width, got.height
));
} else {
label.push_str(&format!(" {}x{}", got.width, got.height));
}
if self.style.show_constraint_bounds {
let c = &record.constraints;
if c.min_width != 0 || c.min_height != 0 || c.max_width != 0 || c.max_height != 0 {
label.push_str(&format!(
" [{}..{} x {}..{}]",
c.min_width,
if c.max_width == 0 {
"\u{221E}".to_string()
} else {
c.max_width.to_string()
},
c.min_height,
if c.max_height == 0 {
"\u{221E}".to_string()
} else {
c.max_height.to_string()
}
));
}
}
label
}
}
impl Widget for ConstraintOverlay<'_> {
fn render(&self, area: Rect, frame: &mut Frame) {
if !self.debugger.enabled() {
return;
}
for record in self.debugger.records() {
self.render_record(record, area, &mut frame.buffer);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout_debugger::LayoutConstraints;
use ftui_render::grapheme_pool::GraphemePool;
#[test]
fn overlay_renders_nothing_when_disabled() {
let mut debugger = LayoutDebugger::new();
debugger.record(LayoutRecord::new(
"Root",
Rect::new(0, 0, 10, 4),
Rect::new(0, 0, 10, 4),
LayoutConstraints::unconstrained(),
));
let overlay = ConstraintOverlay::new(&debugger);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(20, 10, &mut pool);
overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
assert!(frame.buffer.get(0, 0).unwrap().is_empty());
}
#[test]
fn overlay_renders_border_for_valid_constraint() {
let mut debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.record(LayoutRecord::new(
"Root",
Rect::new(1, 1, 6, 4),
Rect::new(1, 1, 6, 4),
LayoutConstraints::new(4, 10, 2, 6),
));
let overlay = ConstraintOverlay::new(&debugger);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(20, 10, &mut pool);
overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
let cell = frame.buffer.get(1, 1).unwrap();
assert_eq!(cell.content.as_char(), Some('+'));
}
#[test]
fn overlay_uses_overflow_color_when_exceeds_max() {
let mut debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.record(LayoutRecord::new(
"Overflow",
Rect::new(0, 0, 10, 4),
Rect::new(0, 0, 10, 4),
LayoutConstraints::new(0, 8, 0, 3),
));
let style = ConstraintOverlayStyle {
overflow_color: PackedRgba::rgb(255, 0, 0),
..Default::default()
};
let overlay = ConstraintOverlay::new(&debugger).style(style);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(20, 10, &mut pool);
overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
let cell = frame.buffer.get(0, 0).unwrap();
assert_eq!(cell.fg, PackedRgba::rgb(255, 0, 0));
}
#[test]
fn overlay_uses_underflow_color_when_below_min() {
let mut debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.record(LayoutRecord::new(
"Underflow",
Rect::new(0, 0, 4, 2),
Rect::new(0, 0, 4, 2),
LayoutConstraints::new(6, 0, 3, 0),
));
let style = ConstraintOverlayStyle {
underflow_color: PackedRgba::rgb(255, 255, 0),
..Default::default()
};
let overlay = ConstraintOverlay::new(&debugger).style(style);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(20, 10, &mut pool);
overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
let cell = frame.buffer.get(0, 0).unwrap();
assert_eq!(cell.fg, PackedRgba::rgb(255, 255, 0));
}
#[test]
fn overlay_shows_requested_vs_received_diff() {
let mut debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.record(LayoutRecord::new(
"Diff",
Rect::new(0, 0, 10, 5),
Rect::new(0, 0, 8, 4),
LayoutConstraints::unconstrained(),
));
let style = ConstraintOverlayStyle {
requested_color: PackedRgba::rgb(0, 0, 255),
..Default::default()
};
let overlay = ConstraintOverlay::new(&debugger).style(style);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(20, 10, &mut pool);
overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
let cell = frame.buffer.get(9, 0).unwrap();
assert_eq!(cell.content.as_char(), Some('.'));
assert_eq!(cell.fg, PackedRgba::rgb(0, 0, 255));
}
#[test]
fn overlay_renders_children() {
let mut debugger = LayoutDebugger::new();
debugger.set_enabled(true);
let child = LayoutRecord::new(
"Child",
Rect::new(2, 2, 4, 2),
Rect::new(2, 2, 4, 2),
LayoutConstraints::unconstrained(),
);
let parent = LayoutRecord::new(
"Parent",
Rect::new(0, 0, 10, 6),
Rect::new(0, 0, 10, 6),
LayoutConstraints::unconstrained(),
)
.with_child(child);
debugger.record(parent);
let overlay = ConstraintOverlay::new(&debugger);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(20, 10, &mut pool);
overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
let parent_cell = frame.buffer.get(0, 0).unwrap();
assert_eq!(parent_cell.content.as_char(), Some('+'));
let child_cell = frame.buffer.get(2, 2).unwrap();
assert_eq!(child_cell.content.as_char(), Some('+'));
}
#[test]
fn overlay_clips_to_render_area() {
let mut debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.record(LayoutRecord::new(
"PartiallyVisible",
Rect::new(5, 5, 10, 10),
Rect::new(5, 5, 10, 10),
LayoutConstraints::unconstrained(),
));
let overlay = ConstraintOverlay::new(&debugger);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 10, &mut pool);
overlay.render(Rect::new(0, 0, 10, 10), &mut frame);
let cell = frame.buffer.get(5, 5).unwrap();
assert_eq!(cell.content.as_char(), Some('+'));
let outside = frame.buffer.get(0, 0).unwrap();
assert!(outside.is_empty());
}
#[test]
fn format_label_includes_status_marker() {
let debugger = LayoutDebugger::new();
let overlay = ConstraintOverlay::new(&debugger);
let record = LayoutRecord::new(
"Widget",
Rect::new(0, 0, 10, 4),
Rect::new(0, 0, 10, 4),
LayoutConstraints::new(0, 8, 0, 0),
);
let label = overlay.format_label(&record, true, false);
assert!(label.starts_with("Widget!"));
let label = overlay.format_label(&record, false, true);
assert!(label.starts_with("Widget?"));
let label = overlay.format_label(&record, false, false);
assert!(label.starts_with("Widget "));
}
#[test]
fn style_can_be_customized() {
let debugger = LayoutDebugger::new();
let style = ConstraintOverlayStyle {
show_borders: false,
show_labels: false,
show_size_diff: false,
..Default::default()
};
let overlay = ConstraintOverlay::new(&debugger).style(style);
assert!(!overlay.style.show_borders);
assert!(!overlay.style.show_labels);
}
#[test]
fn default_style_values() {
let s = ConstraintOverlayStyle::default();
assert_eq!(s.normal_color, PackedRgba::rgb(100, 200, 100));
assert_eq!(s.overflow_color, PackedRgba::rgb(240, 80, 80));
assert_eq!(s.underflow_color, PackedRgba::rgb(240, 200, 80));
assert_eq!(s.requested_color, PackedRgba::rgb(80, 150, 240));
assert!(s.show_size_diff);
assert!(s.show_constraint_bounds);
assert!(s.show_borders);
assert!(s.show_labels);
}
#[test]
fn format_label_same_requested_and_received() {
let debugger = LayoutDebugger::new();
let overlay = ConstraintOverlay::new(&debugger);
let record = LayoutRecord::new(
"Box",
Rect::new(0, 0, 8, 4),
Rect::new(0, 0, 8, 4),
LayoutConstraints::unconstrained(),
);
let label = overlay.format_label(&record, false, false);
assert!(label.contains("8x4"));
assert!(!label.contains('\u{2192}'));
}
#[test]
fn format_label_different_sizes_shows_arrow() {
let debugger = LayoutDebugger::new();
let overlay = ConstraintOverlay::new(&debugger);
let record = LayoutRecord::new(
"Box",
Rect::new(0, 0, 10, 5),
Rect::new(0, 0, 8, 4),
LayoutConstraints::unconstrained(),
);
let label = overlay.format_label(&record, false, false);
assert!(label.contains("10x5"));
assert!(label.contains('\u{2192}'));
assert!(label.contains("8x4"));
}
#[test]
fn format_label_hides_size_diff_when_disabled() {
let debugger = LayoutDebugger::new();
let style = ConstraintOverlayStyle {
show_size_diff: false,
..Default::default()
};
let overlay = ConstraintOverlay::new(&debugger).style(style);
let record = LayoutRecord::new(
"Box",
Rect::new(0, 0, 10, 5),
Rect::new(0, 0, 8, 4),
LayoutConstraints::unconstrained(),
);
let label = overlay.format_label(&record, false, false);
assert!(!label.contains('\u{2192}'));
assert!(label.contains("8x4"));
assert!(!label.contains("10x5"));
}
#[test]
fn format_label_constraint_bounds_infinity() {
let debugger = LayoutDebugger::new();
let overlay = ConstraintOverlay::new(&debugger);
let record = LayoutRecord::new(
"W",
Rect::new(0, 0, 8, 4),
Rect::new(0, 0, 8, 4),
LayoutConstraints::new(5, 0, 0, 10),
);
let label = overlay.format_label(&record, false, false);
assert!(label.contains('\u{221E}'));
assert!(label.contains("5.."));
}
#[test]
fn format_label_no_bounds_when_all_zero() {
let debugger = LayoutDebugger::new();
let overlay = ConstraintOverlay::new(&debugger);
let record = LayoutRecord::new(
"W",
Rect::new(0, 0, 8, 4),
Rect::new(0, 0, 8, 4),
LayoutConstraints::new(0, 0, 0, 0),
);
let label = overlay.format_label(&record, false, false);
assert!(!label.contains('['));
}
#[test]
fn format_label_no_bounds_when_disabled() {
let debugger = LayoutDebugger::new();
let style = ConstraintOverlayStyle {
show_constraint_bounds: false,
..Default::default()
};
let overlay = ConstraintOverlay::new(&debugger).style(style);
let record = LayoutRecord::new(
"W",
Rect::new(0, 0, 8, 4),
Rect::new(0, 0, 8, 4),
LayoutConstraints::new(5, 10, 3, 8),
);
let label = overlay.format_label(&record, false, false);
assert!(!label.contains('['));
}
#[test]
fn enabled_debugger_with_no_records_renders_nothing() {
let mut debugger = LayoutDebugger::new();
debugger.set_enabled(true);
let overlay = ConstraintOverlay::new(&debugger);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(20, 10, &mut pool);
overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
assert!(frame.buffer.get(0, 0).unwrap().is_empty());
}
#[test]
fn record_fully_outside_render_area_is_skipped() {
let mut debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.record(LayoutRecord::new(
"Offscreen",
Rect::new(50, 50, 10, 10),
Rect::new(50, 50, 10, 10),
LayoutConstraints::unconstrained(),
));
let overlay = ConstraintOverlay::new(&debugger);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(20, 10, &mut pool);
overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
assert!(frame.buffer.get(0, 0).unwrap().is_empty());
}
#[test]
fn zero_size_record_is_skipped() {
let mut debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.record(LayoutRecord::new(
"Empty",
Rect::new(0, 0, 0, 0),
Rect::new(0, 0, 0, 0),
LayoutConstraints::unconstrained(),
));
let overlay = ConstraintOverlay::new(&debugger);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(20, 10, &mut pool);
overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
assert!(frame.buffer.get(0, 0).unwrap().is_empty());
}
#[test]
fn one_by_one_record_renders_border() {
let mut debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.record(LayoutRecord::new(
"Tiny",
Rect::new(2, 2, 1, 1),
Rect::new(2, 2, 1, 1),
LayoutConstraints::unconstrained(),
));
let overlay = ConstraintOverlay::new(&debugger);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(20, 10, &mut pool);
overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
let cell = frame.buffer.get(2, 2).unwrap();
assert!(!cell.is_empty());
}
#[test]
fn overflow_only_height() {
let mut debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.record(LayoutRecord::new(
"HOverflow",
Rect::new(0, 0, 5, 8),
Rect::new(0, 0, 5, 8),
LayoutConstraints::new(0, 10, 0, 6),
));
let style = ConstraintOverlayStyle {
overflow_color: PackedRgba::rgb(255, 0, 0),
..Default::default()
};
let overlay = ConstraintOverlay::new(&debugger).style(style);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(20, 10, &mut pool);
overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
let cell = frame.buffer.get(0, 0).unwrap();
assert_eq!(cell.fg, PackedRgba::rgb(255, 0, 0), "height overflow color");
}
#[test]
fn underflow_only_height() {
let mut debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.record(LayoutRecord::new(
"HUnderflow",
Rect::new(0, 0, 6, 2),
Rect::new(0, 0, 6, 2),
LayoutConstraints::new(4, 0, 3, 0),
));
let style = ConstraintOverlayStyle {
underflow_color: PackedRgba::rgb(255, 255, 0),
..Default::default()
};
let overlay = ConstraintOverlay::new(&debugger).style(style);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(20, 10, &mut pool);
overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
let cell = frame.buffer.get(0, 0).unwrap();
assert_eq!(
cell.fg,
PackedRgba::rgb(255, 255, 0),
"height underflow color"
);
}
#[test]
fn overflow_takes_priority_over_underflow() {
let mut debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.record(LayoutRecord::new(
"Both",
Rect::new(0, 0, 10, 2),
Rect::new(0, 0, 10, 2),
LayoutConstraints::new(0, 8, 3, 0),
));
let style = ConstraintOverlayStyle {
overflow_color: PackedRgba::rgb(255, 0, 0),
underflow_color: PackedRgba::rgb(255, 255, 0),
..Default::default()
};
let overlay = ConstraintOverlay::new(&debugger).style(style);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(20, 10, &mut pool);
overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
let cell = frame.buffer.get(0, 0).unwrap();
assert_eq!(
cell.fg,
PackedRgba::rgb(255, 0, 0),
"overflow wins over underflow"
);
}
#[test]
fn multiple_records_all_render() {
let mut debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.record(LayoutRecord::new(
"A",
Rect::new(0, 0, 5, 3),
Rect::new(0, 0, 5, 3),
LayoutConstraints::unconstrained(),
));
debugger.record(LayoutRecord::new(
"B",
Rect::new(6, 0, 5, 3),
Rect::new(6, 0, 5, 3),
LayoutConstraints::unconstrained(),
));
let overlay = ConstraintOverlay::new(&debugger);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(20, 10, &mut pool);
overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('+'));
assert_eq!(frame.buffer.get(6, 0).unwrap().content.as_char(), Some('+'));
}
#[test]
fn deeply_nested_children_render() {
let mut debugger = LayoutDebugger::new();
debugger.set_enabled(true);
let grandchild = LayoutRecord::new(
"GC",
Rect::new(4, 4, 3, 2),
Rect::new(4, 4, 3, 2),
LayoutConstraints::unconstrained(),
);
let child = LayoutRecord::new(
"Child",
Rect::new(2, 2, 8, 6),
Rect::new(2, 2, 8, 6),
LayoutConstraints::unconstrained(),
)
.with_child(grandchild);
let parent = LayoutRecord::new(
"Parent",
Rect::new(0, 0, 12, 10),
Rect::new(0, 0, 12, 10),
LayoutConstraints::unconstrained(),
)
.with_child(child);
debugger.record(parent);
let overlay = ConstraintOverlay::new(&debugger);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(20, 12, &mut pool);
overlay.render(Rect::new(0, 0, 20, 12), &mut frame);
assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('+'));
assert_eq!(frame.buffer.get(2, 2).unwrap().content.as_char(), Some('+'));
assert_eq!(frame.buffer.get(4, 4).unwrap().content.as_char(), Some('+'));
}
#[test]
fn format_label_empty_widget_name() {
let debugger = LayoutDebugger::new();
let overlay = ConstraintOverlay::new(&debugger);
let record = LayoutRecord::new(
"",
Rect::new(0, 0, 5, 3),
Rect::new(0, 0, 5, 3),
LayoutConstraints::unconstrained(),
);
let label = overlay.format_label(&record, false, false);
assert!(label.contains("5x3"), "size should still appear: {label}");
}
#[test]
fn format_label_both_bounds_finite() {
let debugger = LayoutDebugger::new();
let overlay = ConstraintOverlay::new(&debugger);
let record = LayoutRecord::new(
"W",
Rect::new(0, 0, 8, 4),
Rect::new(0, 0, 8, 4),
LayoutConstraints::new(4, 12, 2, 8),
);
let label = overlay.format_label(&record, false, false);
assert!(label.contains("[4..12 x 2..8]"), "label={label}");
}
#[test]
fn requested_outline_not_drawn_when_same_as_received() {
let mut debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.record(LayoutRecord::new(
"Same",
Rect::new(0, 0, 6, 4),
Rect::new(0, 0, 6, 4),
LayoutConstraints::unconstrained(),
));
let style = ConstraintOverlayStyle {
requested_color: PackedRgba::rgb(0, 0, 255),
..Default::default()
};
let overlay = ConstraintOverlay::new(&debugger).style(style);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(20, 10, &mut pool);
overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
let cell = frame.buffer.get(5, 3).unwrap(); assert_ne!(
cell.content.as_char(),
Some('.'),
"dot should not appear when same size"
);
}
#[test]
fn style_clone_and_debug() {
let style = ConstraintOverlayStyle::default();
let cloned = style.clone();
let _ = format!("{cloned:?}");
assert_eq!(cloned.normal_color, style.normal_color);
}
#[test]
fn max_width_zero_means_unconstrained_no_overflow() {
let debugger = LayoutDebugger::new();
let overlay = ConstraintOverlay::new(&debugger);
let record = LayoutRecord::new(
"W",
Rect::new(0, 0, 100, 4),
Rect::new(0, 0, 100, 4),
LayoutConstraints::new(0, 0, 0, 0),
);
let label = overlay.format_label(&record, false, false);
assert!(!label.contains('!'), "should not be overflow: {label}");
}
#[test]
fn no_borders_when_show_borders_disabled() {
let mut debugger = LayoutDebugger::new();
debugger.set_enabled(true);
debugger.record(LayoutRecord::new(
"NoBorder",
Rect::new(0, 0, 6, 4),
Rect::new(0, 0, 6, 4),
LayoutConstraints::unconstrained(),
));
let style = ConstraintOverlayStyle {
show_borders: false,
show_labels: false,
show_size_diff: false,
..Default::default()
};
let overlay = ConstraintOverlay::new(&debugger).style(style);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(20, 10, &mut pool);
overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
assert!(frame.buffer.get(0, 0).unwrap().is_empty());
}
}