1#![forbid(unsafe_code)]
2
3use crate::Widget;
24use crate::layout_debugger::{LayoutDebugger, LayoutRecord};
25use ftui_core::geometry::Rect;
26use ftui_render::buffer::Buffer;
27use ftui_render::cell::{Cell, PackedRgba};
28use ftui_render::drawing::{BorderChars, Draw};
29use ftui_render::frame::Frame;
30
31#[derive(Debug, Clone)]
33pub struct ConstraintOverlayStyle {
34 pub normal_color: PackedRgba,
36 pub overflow_color: PackedRgba,
38 pub underflow_color: PackedRgba,
40 pub requested_color: PackedRgba,
42 pub label_fg: PackedRgba,
44 pub label_bg: PackedRgba,
46 pub show_size_diff: bool,
48 pub show_constraint_bounds: bool,
50 pub show_borders: bool,
52 pub show_labels: bool,
54 pub border_chars: BorderChars,
56}
57
58impl Default for ConstraintOverlayStyle {
59 fn default() -> Self {
60 Self {
61 normal_color: PackedRgba::rgb(100, 200, 100),
62 overflow_color: PackedRgba::rgb(240, 80, 80),
63 underflow_color: PackedRgba::rgb(240, 200, 80),
64 requested_color: PackedRgba::rgb(80, 150, 240),
65 label_fg: PackedRgba::rgb(255, 255, 255),
66 label_bg: PackedRgba::rgb(0, 0, 0),
67 show_size_diff: true,
68 show_constraint_bounds: true,
69 show_borders: true,
70 show_labels: true,
71 border_chars: BorderChars::ASCII,
72 }
73 }
74}
75
76pub struct ConstraintOverlay<'a> {
85 debugger: &'a LayoutDebugger,
86 style: ConstraintOverlayStyle,
87}
88
89impl<'a> ConstraintOverlay<'a> {
90 pub fn new(debugger: &'a LayoutDebugger) -> Self {
92 Self {
93 debugger,
94 style: ConstraintOverlayStyle::default(),
95 }
96 }
97
98 #[must_use]
100 pub fn style(mut self, style: ConstraintOverlayStyle) -> Self {
101 self.style = style;
102 self
103 }
104
105 fn render_record(&self, record: &LayoutRecord, area: Rect, buf: &mut Buffer) {
106 let Some(clipped) = record.area_received.intersection_opt(&area) else {
108 return;
109 };
110 if clipped.is_empty() {
111 return;
112 }
113
114 let constraints = &record.constraints;
116 let received = &record.area_received;
117
118 let is_overflow = (constraints.max_width != 0 && received.width > constraints.max_width)
119 || (constraints.max_height != 0 && received.height > constraints.max_height);
120 let is_underflow =
121 received.width < constraints.min_width || received.height < constraints.min_height;
122
123 let border_color = if is_overflow {
124 self.style.overflow_color
125 } else if is_underflow {
126 self.style.underflow_color
127 } else {
128 self.style.normal_color
129 };
130
131 if self.style.show_borders {
133 let border_cell = Cell::from_char('+').with_fg(border_color);
134 buf.draw_border(clipped, self.style.border_chars, border_cell);
135 }
136
137 if self.style.show_size_diff {
139 let requested = &record.area_requested;
140 if requested != received
141 && let Some(req_clipped) = requested.intersection_opt(&area)
142 && !req_clipped.is_empty()
143 {
144 let req_cell = Cell::from_char('.').with_fg(self.style.requested_color);
146 self.draw_requested_outline(req_clipped, buf, req_cell);
147 }
148 }
149
150 if self.style.show_labels {
152 let label = self.format_label(record, is_overflow, is_underflow);
153 let label_x = clipped.x.saturating_add(1);
154 let label_y = clipped.y;
155 let max_x = clipped.right();
156
157 if label_x < max_x {
158 let label_cell = Cell::from_char(' ')
159 .with_fg(self.style.label_fg)
160 .with_bg(self.style.label_bg);
161 let _ = buf.print_text_clipped(label_x, label_y, &label, label_cell, max_x);
162 }
163 }
164
165 for child in &record.children {
167 self.render_record(child, area, buf);
168 }
169 }
170
171 fn draw_requested_outline(&self, area: Rect, buf: &mut Buffer, cell: Cell) {
172 if area.width >= 1 && area.height >= 1 {
174 buf.set_fast(area.x, area.y, cell);
175 }
176 if area.width >= 2 && area.height >= 1 {
177 buf.set_fast(area.right().saturating_sub(1), area.y, cell);
178 }
179 if area.width >= 1 && area.height >= 2 {
180 buf.set_fast(area.x, area.bottom().saturating_sub(1), cell);
181 }
182 if area.width >= 2 && area.height >= 2 {
183 buf.set_fast(
184 area.right().saturating_sub(1),
185 area.bottom().saturating_sub(1),
186 cell,
187 );
188 }
189 }
190
191 fn format_label(&self, record: &LayoutRecord, is_overflow: bool, is_underflow: bool) -> String {
192 let status = if is_overflow {
193 "!"
194 } else if is_underflow {
195 "?"
196 } else {
197 ""
198 };
199
200 let mut label = format!("{}{}", record.widget_name, status);
201
202 let req = &record.area_requested;
204 let got = &record.area_received;
205 if self.style.show_size_diff && (req.width != got.width || req.height != got.height) {
206 label.push_str(&format!(
207 " {}x{}\u{2192}{}x{}",
208 req.width, req.height, got.width, got.height
209 ));
210 } else {
211 label.push_str(&format!(" {}x{}", got.width, got.height));
212 }
213
214 if self.style.show_constraint_bounds {
216 let c = &record.constraints;
217 if c.min_width != 0 || c.min_height != 0 || c.max_width != 0 || c.max_height != 0 {
218 label.push_str(&format!(
219 " [{}..{} x {}..{}]",
220 c.min_width,
221 if c.max_width == 0 {
222 "\u{221E}".to_string()
223 } else {
224 c.max_width.to_string()
225 },
226 c.min_height,
227 if c.max_height == 0 {
228 "\u{221E}".to_string()
229 } else {
230 c.max_height.to_string()
231 }
232 ));
233 }
234 }
235
236 label
237 }
238}
239
240impl Widget for ConstraintOverlay<'_> {
241 fn render(&self, area: Rect, frame: &mut Frame) {
242 if !self.debugger.enabled() {
243 return;
244 }
245
246 for record in self.debugger.records() {
247 self.render_record(record, area, &mut frame.buffer);
248 }
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::layout_debugger::LayoutConstraints;
256 use ftui_render::grapheme_pool::GraphemePool;
257
258 #[test]
259 fn overlay_renders_nothing_when_disabled() {
260 let mut debugger = LayoutDebugger::new();
261 debugger.record(LayoutRecord::new(
263 "Root",
264 Rect::new(0, 0, 10, 4),
265 Rect::new(0, 0, 10, 4),
266 LayoutConstraints::unconstrained(),
267 ));
268
269 let overlay = ConstraintOverlay::new(&debugger);
270 let mut pool = GraphemePool::new();
271 let mut frame = Frame::new(20, 10, &mut pool);
272 overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
273
274 assert!(frame.buffer.get(0, 0).unwrap().is_empty());
276 }
277
278 #[test]
279 fn overlay_renders_border_for_valid_constraint() {
280 let mut debugger = LayoutDebugger::new();
281 debugger.set_enabled(true);
282 debugger.record(LayoutRecord::new(
283 "Root",
284 Rect::new(1, 1, 6, 4),
285 Rect::new(1, 1, 6, 4),
286 LayoutConstraints::new(4, 10, 2, 6),
287 ));
288
289 let overlay = ConstraintOverlay::new(&debugger);
290 let mut pool = GraphemePool::new();
291 let mut frame = Frame::new(20, 10, &mut pool);
292 overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
293
294 let cell = frame.buffer.get(1, 1).unwrap();
296 assert_eq!(cell.content.as_char(), Some('+'));
297 }
298
299 #[test]
300 fn overlay_uses_overflow_color_when_exceeds_max() {
301 let mut debugger = LayoutDebugger::new();
302 debugger.set_enabled(true);
303 debugger.record(LayoutRecord::new(
305 "Overflow",
306 Rect::new(0, 0, 10, 4),
307 Rect::new(0, 0, 10, 4),
308 LayoutConstraints::new(0, 8, 0, 3),
309 ));
310
311 let style = ConstraintOverlayStyle {
312 overflow_color: PackedRgba::rgb(255, 0, 0),
313 ..Default::default()
314 };
315
316 let overlay = ConstraintOverlay::new(&debugger).style(style);
317 let mut pool = GraphemePool::new();
318 let mut frame = Frame::new(20, 10, &mut pool);
319 overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
320
321 let cell = frame.buffer.get(0, 0).unwrap();
322 assert_eq!(cell.fg, PackedRgba::rgb(255, 0, 0));
323 }
324
325 #[test]
326 fn overlay_uses_underflow_color_when_below_min() {
327 let mut debugger = LayoutDebugger::new();
328 debugger.set_enabled(true);
329 debugger.record(LayoutRecord::new(
331 "Underflow",
332 Rect::new(0, 0, 4, 2),
333 Rect::new(0, 0, 4, 2),
334 LayoutConstraints::new(6, 0, 3, 0),
335 ));
336
337 let style = ConstraintOverlayStyle {
338 underflow_color: PackedRgba::rgb(255, 255, 0),
339 ..Default::default()
340 };
341
342 let overlay = ConstraintOverlay::new(&debugger).style(style);
343 let mut pool = GraphemePool::new();
344 let mut frame = Frame::new(20, 10, &mut pool);
345 overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
346
347 let cell = frame.buffer.get(0, 0).unwrap();
348 assert_eq!(cell.fg, PackedRgba::rgb(255, 255, 0));
349 }
350
351 #[test]
352 fn overlay_shows_requested_vs_received_diff() {
353 let mut debugger = LayoutDebugger::new();
354 debugger.set_enabled(true);
355 debugger.record(LayoutRecord::new(
357 "Diff",
358 Rect::new(0, 0, 10, 5),
359 Rect::new(0, 0, 8, 4),
360 LayoutConstraints::unconstrained(),
361 ));
362
363 let style = ConstraintOverlayStyle {
364 requested_color: PackedRgba::rgb(0, 0, 255),
365 ..Default::default()
366 };
367
368 let overlay = ConstraintOverlay::new(&debugger).style(style);
369 let mut pool = GraphemePool::new();
370 let mut frame = Frame::new(20, 10, &mut pool);
371 overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
372
373 let cell = frame.buffer.get(9, 0).unwrap();
375 assert_eq!(cell.content.as_char(), Some('.'));
376 assert_eq!(cell.fg, PackedRgba::rgb(0, 0, 255));
377 }
378
379 #[test]
380 fn overlay_renders_children() {
381 let mut debugger = LayoutDebugger::new();
382 debugger.set_enabled(true);
383
384 let child = LayoutRecord::new(
385 "Child",
386 Rect::new(2, 2, 4, 2),
387 Rect::new(2, 2, 4, 2),
388 LayoutConstraints::unconstrained(),
389 );
390 let parent = LayoutRecord::new(
391 "Parent",
392 Rect::new(0, 0, 10, 6),
393 Rect::new(0, 0, 10, 6),
394 LayoutConstraints::unconstrained(),
395 )
396 .with_child(child);
397 debugger.record(parent);
398
399 let overlay = ConstraintOverlay::new(&debugger);
400 let mut pool = GraphemePool::new();
401 let mut frame = Frame::new(20, 10, &mut pool);
402 overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
403
404 let parent_cell = frame.buffer.get(0, 0).unwrap();
406 assert_eq!(parent_cell.content.as_char(), Some('+'));
407
408 let child_cell = frame.buffer.get(2, 2).unwrap();
409 assert_eq!(child_cell.content.as_char(), Some('+'));
410 }
411
412 #[test]
413 fn overlay_clips_to_render_area() {
414 let mut debugger = LayoutDebugger::new();
415 debugger.set_enabled(true);
416 debugger.record(LayoutRecord::new(
417 "PartiallyVisible",
418 Rect::new(5, 5, 10, 10),
419 Rect::new(5, 5, 10, 10),
420 LayoutConstraints::unconstrained(),
421 ));
422
423 let overlay = ConstraintOverlay::new(&debugger);
424 let mut pool = GraphemePool::new();
425 let mut frame = Frame::new(10, 10, &mut pool);
426 overlay.render(Rect::new(0, 0, 10, 10), &mut frame);
428
429 let cell = frame.buffer.get(5, 5).unwrap();
431 assert_eq!(cell.content.as_char(), Some('+'));
432
433 let outside = frame.buffer.get(0, 0).unwrap();
435 assert!(outside.is_empty());
436 }
437
438 #[test]
439 fn format_label_includes_status_marker() {
440 let debugger = LayoutDebugger::new();
441 let overlay = ConstraintOverlay::new(&debugger);
442
443 let record = LayoutRecord::new(
445 "Widget",
446 Rect::new(0, 0, 10, 4),
447 Rect::new(0, 0, 10, 4),
448 LayoutConstraints::new(0, 8, 0, 0),
449 );
450 let label = overlay.format_label(&record, true, false);
451 assert!(label.starts_with("Widget!"));
452
453 let label = overlay.format_label(&record, false, true);
455 assert!(label.starts_with("Widget?"));
456
457 let label = overlay.format_label(&record, false, false);
459 assert!(label.starts_with("Widget "));
460 }
461
462 #[test]
463 fn style_can_be_customized() {
464 let debugger = LayoutDebugger::new();
465 let style = ConstraintOverlayStyle {
466 show_borders: false,
467 show_labels: false,
468 show_size_diff: false,
469 ..Default::default()
470 };
471
472 let overlay = ConstraintOverlay::new(&debugger).style(style);
473 assert!(!overlay.style.show_borders);
474 assert!(!overlay.style.show_labels);
475 }
476
477 #[test]
478 fn default_style_values() {
479 let s = ConstraintOverlayStyle::default();
480 assert_eq!(s.normal_color, PackedRgba::rgb(100, 200, 100));
481 assert_eq!(s.overflow_color, PackedRgba::rgb(240, 80, 80));
482 assert_eq!(s.underflow_color, PackedRgba::rgb(240, 200, 80));
483 assert_eq!(s.requested_color, PackedRgba::rgb(80, 150, 240));
484 assert!(s.show_size_diff);
485 assert!(s.show_constraint_bounds);
486 assert!(s.show_borders);
487 assert!(s.show_labels);
488 }
489
490 #[test]
491 fn format_label_same_requested_and_received() {
492 let debugger = LayoutDebugger::new();
493 let overlay = ConstraintOverlay::new(&debugger);
494 let record = LayoutRecord::new(
495 "Box",
496 Rect::new(0, 0, 8, 4),
497 Rect::new(0, 0, 8, 4),
498 LayoutConstraints::unconstrained(),
499 );
500 let label = overlay.format_label(&record, false, false);
501 assert!(label.contains("8x4"));
502 assert!(!label.contains('\u{2192}'));
504 }
505
506 #[test]
507 fn format_label_different_sizes_shows_arrow() {
508 let debugger = LayoutDebugger::new();
509 let overlay = ConstraintOverlay::new(&debugger);
510 let record = LayoutRecord::new(
511 "Box",
512 Rect::new(0, 0, 10, 5),
513 Rect::new(0, 0, 8, 4),
514 LayoutConstraints::unconstrained(),
515 );
516 let label = overlay.format_label(&record, false, false);
517 assert!(label.contains("10x5"));
519 assert!(label.contains('\u{2192}'));
520 assert!(label.contains("8x4"));
521 }
522
523 #[test]
524 fn format_label_hides_size_diff_when_disabled() {
525 let debugger = LayoutDebugger::new();
526 let style = ConstraintOverlayStyle {
527 show_size_diff: false,
528 ..Default::default()
529 };
530 let overlay = ConstraintOverlay::new(&debugger).style(style);
531 let record = LayoutRecord::new(
532 "Box",
533 Rect::new(0, 0, 10, 5),
534 Rect::new(0, 0, 8, 4),
535 LayoutConstraints::unconstrained(),
536 );
537 let label = overlay.format_label(&record, false, false);
538
539 assert!(!label.contains('\u{2192}'));
540 assert!(label.contains("8x4"));
541 assert!(!label.contains("10x5"));
542 }
543
544 #[test]
545 fn format_label_constraint_bounds_infinity() {
546 let debugger = LayoutDebugger::new();
547 let overlay = ConstraintOverlay::new(&debugger);
548 let record = LayoutRecord::new(
550 "W",
551 Rect::new(0, 0, 8, 4),
552 Rect::new(0, 0, 8, 4),
553 LayoutConstraints::new(5, 0, 0, 10),
554 );
555 let label = overlay.format_label(&record, false, false);
556 assert!(label.contains('\u{221E}'));
558 assert!(label.contains("5.."));
559 }
560
561 #[test]
562 fn format_label_no_bounds_when_all_zero() {
563 let debugger = LayoutDebugger::new();
564 let overlay = ConstraintOverlay::new(&debugger);
565 let record = LayoutRecord::new(
566 "W",
567 Rect::new(0, 0, 8, 4),
568 Rect::new(0, 0, 8, 4),
569 LayoutConstraints::new(0, 0, 0, 0),
570 );
571 let label = overlay.format_label(&record, false, false);
572 assert!(!label.contains('['));
574 }
575
576 #[test]
577 fn format_label_no_bounds_when_disabled() {
578 let debugger = LayoutDebugger::new();
579 let style = ConstraintOverlayStyle {
580 show_constraint_bounds: false,
581 ..Default::default()
582 };
583 let overlay = ConstraintOverlay::new(&debugger).style(style);
584 let record = LayoutRecord::new(
585 "W",
586 Rect::new(0, 0, 8, 4),
587 Rect::new(0, 0, 8, 4),
588 LayoutConstraints::new(5, 10, 3, 8),
589 );
590 let label = overlay.format_label(&record, false, false);
591 assert!(!label.contains('['));
592 }
593
594 #[test]
595 fn enabled_debugger_with_no_records_renders_nothing() {
596 let mut debugger = LayoutDebugger::new();
597 debugger.set_enabled(true);
598 let overlay = ConstraintOverlay::new(&debugger);
600 let mut pool = GraphemePool::new();
601 let mut frame = Frame::new(20, 10, &mut pool);
602 overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
603 assert!(frame.buffer.get(0, 0).unwrap().is_empty());
604 }
605
606 #[test]
607 fn record_fully_outside_render_area_is_skipped() {
608 let mut debugger = LayoutDebugger::new();
609 debugger.set_enabled(true);
610 debugger.record(LayoutRecord::new(
611 "Offscreen",
612 Rect::new(50, 50, 10, 10),
613 Rect::new(50, 50, 10, 10),
614 LayoutConstraints::unconstrained(),
615 ));
616
617 let overlay = ConstraintOverlay::new(&debugger);
618 let mut pool = GraphemePool::new();
619 let mut frame = Frame::new(20, 10, &mut pool);
620 overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
621
622 assert!(frame.buffer.get(0, 0).unwrap().is_empty());
624 }
625
626 #[test]
629 fn zero_size_record_is_skipped() {
630 let mut debugger = LayoutDebugger::new();
631 debugger.set_enabled(true);
632 debugger.record(LayoutRecord::new(
633 "Empty",
634 Rect::new(0, 0, 0, 0),
635 Rect::new(0, 0, 0, 0),
636 LayoutConstraints::unconstrained(),
637 ));
638
639 let overlay = ConstraintOverlay::new(&debugger);
640 let mut pool = GraphemePool::new();
641 let mut frame = Frame::new(20, 10, &mut pool);
642 overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
643 assert!(frame.buffer.get(0, 0).unwrap().is_empty());
644 }
645
646 #[test]
647 fn one_by_one_record_renders_border() {
648 let mut debugger = LayoutDebugger::new();
649 debugger.set_enabled(true);
650 debugger.record(LayoutRecord::new(
651 "Tiny",
652 Rect::new(2, 2, 1, 1),
653 Rect::new(2, 2, 1, 1),
654 LayoutConstraints::unconstrained(),
655 ));
656
657 let overlay = ConstraintOverlay::new(&debugger);
658 let mut pool = GraphemePool::new();
659 let mut frame = Frame::new(20, 10, &mut pool);
660 overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
661 let cell = frame.buffer.get(2, 2).unwrap();
663 assert!(!cell.is_empty());
664 }
665
666 #[test]
667 fn overflow_only_height() {
668 let mut debugger = LayoutDebugger::new();
669 debugger.set_enabled(true);
670 debugger.record(LayoutRecord::new(
672 "HOverflow",
673 Rect::new(0, 0, 5, 8),
674 Rect::new(0, 0, 5, 8),
675 LayoutConstraints::new(0, 10, 0, 6),
676 ));
677
678 let style = ConstraintOverlayStyle {
679 overflow_color: PackedRgba::rgb(255, 0, 0),
680 ..Default::default()
681 };
682 let overlay = ConstraintOverlay::new(&debugger).style(style);
683 let mut pool = GraphemePool::new();
684 let mut frame = Frame::new(20, 10, &mut pool);
685 overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
686
687 let cell = frame.buffer.get(0, 0).unwrap();
688 assert_eq!(cell.fg, PackedRgba::rgb(255, 0, 0), "height overflow color");
689 }
690
691 #[test]
692 fn underflow_only_height() {
693 let mut debugger = LayoutDebugger::new();
694 debugger.set_enabled(true);
695 debugger.record(LayoutRecord::new(
697 "HUnderflow",
698 Rect::new(0, 0, 6, 2),
699 Rect::new(0, 0, 6, 2),
700 LayoutConstraints::new(4, 0, 3, 0),
701 ));
702
703 let style = ConstraintOverlayStyle {
704 underflow_color: PackedRgba::rgb(255, 255, 0),
705 ..Default::default()
706 };
707 let overlay = ConstraintOverlay::new(&debugger).style(style);
708 let mut pool = GraphemePool::new();
709 let mut frame = Frame::new(20, 10, &mut pool);
710 overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
711
712 let cell = frame.buffer.get(0, 0).unwrap();
713 assert_eq!(
714 cell.fg,
715 PackedRgba::rgb(255, 255, 0),
716 "height underflow color"
717 );
718 }
719
720 #[test]
721 fn overflow_takes_priority_over_underflow() {
722 let mut debugger = LayoutDebugger::new();
723 debugger.set_enabled(true);
724 debugger.record(LayoutRecord::new(
726 "Both",
727 Rect::new(0, 0, 10, 2),
728 Rect::new(0, 0, 10, 2),
729 LayoutConstraints::new(0, 8, 3, 0),
730 ));
731
732 let style = ConstraintOverlayStyle {
733 overflow_color: PackedRgba::rgb(255, 0, 0),
734 underflow_color: PackedRgba::rgb(255, 255, 0),
735 ..Default::default()
736 };
737 let overlay = ConstraintOverlay::new(&debugger).style(style);
738 let mut pool = GraphemePool::new();
739 let mut frame = Frame::new(20, 10, &mut pool);
740 overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
741
742 let cell = frame.buffer.get(0, 0).unwrap();
743 assert_eq!(
744 cell.fg,
745 PackedRgba::rgb(255, 0, 0),
746 "overflow wins over underflow"
747 );
748 }
749
750 #[test]
751 fn multiple_records_all_render() {
752 let mut debugger = LayoutDebugger::new();
753 debugger.set_enabled(true);
754 debugger.record(LayoutRecord::new(
755 "A",
756 Rect::new(0, 0, 5, 3),
757 Rect::new(0, 0, 5, 3),
758 LayoutConstraints::unconstrained(),
759 ));
760 debugger.record(LayoutRecord::new(
761 "B",
762 Rect::new(6, 0, 5, 3),
763 Rect::new(6, 0, 5, 3),
764 LayoutConstraints::unconstrained(),
765 ));
766
767 let overlay = ConstraintOverlay::new(&debugger);
768 let mut pool = GraphemePool::new();
769 let mut frame = Frame::new(20, 10, &mut pool);
770 overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
771
772 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('+'));
773 assert_eq!(frame.buffer.get(6, 0).unwrap().content.as_char(), Some('+'));
774 }
775
776 #[test]
777 fn deeply_nested_children_render() {
778 let mut debugger = LayoutDebugger::new();
779 debugger.set_enabled(true);
780
781 let grandchild = LayoutRecord::new(
782 "GC",
783 Rect::new(4, 4, 3, 2),
784 Rect::new(4, 4, 3, 2),
785 LayoutConstraints::unconstrained(),
786 );
787 let child = LayoutRecord::new(
788 "Child",
789 Rect::new(2, 2, 8, 6),
790 Rect::new(2, 2, 8, 6),
791 LayoutConstraints::unconstrained(),
792 )
793 .with_child(grandchild);
794 let parent = LayoutRecord::new(
795 "Parent",
796 Rect::new(0, 0, 12, 10),
797 Rect::new(0, 0, 12, 10),
798 LayoutConstraints::unconstrained(),
799 )
800 .with_child(child);
801 debugger.record(parent);
802
803 let overlay = ConstraintOverlay::new(&debugger);
804 let mut pool = GraphemePool::new();
805 let mut frame = Frame::new(20, 12, &mut pool);
806 overlay.render(Rect::new(0, 0, 20, 12), &mut frame);
807
808 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('+'));
810 assert_eq!(frame.buffer.get(2, 2).unwrap().content.as_char(), Some('+'));
811 assert_eq!(frame.buffer.get(4, 4).unwrap().content.as_char(), Some('+'));
812 }
813
814 #[test]
815 fn format_label_empty_widget_name() {
816 let debugger = LayoutDebugger::new();
817 let overlay = ConstraintOverlay::new(&debugger);
818 let record = LayoutRecord::new(
819 "",
820 Rect::new(0, 0, 5, 3),
821 Rect::new(0, 0, 5, 3),
822 LayoutConstraints::unconstrained(),
823 );
824 let label = overlay.format_label(&record, false, false);
825 assert!(label.contains("5x3"), "size should still appear: {label}");
826 }
827
828 #[test]
829 fn format_label_both_bounds_finite() {
830 let debugger = LayoutDebugger::new();
831 let overlay = ConstraintOverlay::new(&debugger);
832 let record = LayoutRecord::new(
833 "W",
834 Rect::new(0, 0, 8, 4),
835 Rect::new(0, 0, 8, 4),
836 LayoutConstraints::new(4, 12, 2, 8),
837 );
838 let label = overlay.format_label(&record, false, false);
839 assert!(label.contains("[4..12 x 2..8]"), "label={label}");
841 }
842
843 #[test]
844 fn requested_outline_not_drawn_when_same_as_received() {
845 let mut debugger = LayoutDebugger::new();
846 debugger.set_enabled(true);
847 debugger.record(LayoutRecord::new(
848 "Same",
849 Rect::new(0, 0, 6, 4),
850 Rect::new(0, 0, 6, 4),
851 LayoutConstraints::unconstrained(),
852 ));
853
854 let style = ConstraintOverlayStyle {
855 requested_color: PackedRgba::rgb(0, 0, 255),
856 ..Default::default()
857 };
858 let overlay = ConstraintOverlay::new(&debugger).style(style);
859 let mut pool = GraphemePool::new();
860 let mut frame = Frame::new(20, 10, &mut pool);
861 overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
862
863 let cell = frame.buffer.get(5, 3).unwrap(); assert_ne!(
867 cell.content.as_char(),
868 Some('.'),
869 "dot should not appear when same size"
870 );
871 }
872
873 #[test]
874 fn style_clone_and_debug() {
875 let style = ConstraintOverlayStyle::default();
876 let cloned = style.clone();
877 let _ = format!("{cloned:?}");
878 assert_eq!(cloned.normal_color, style.normal_color);
879 }
880
881 #[test]
882 fn max_width_zero_means_unconstrained_no_overflow() {
883 let debugger = LayoutDebugger::new();
884 let overlay = ConstraintOverlay::new(&debugger);
885 let record = LayoutRecord::new(
887 "W",
888 Rect::new(0, 0, 100, 4),
889 Rect::new(0, 0, 100, 4),
890 LayoutConstraints::new(0, 0, 0, 0),
891 );
892 let label = overlay.format_label(&record, false, false);
895 assert!(!label.contains('!'), "should not be overflow: {label}");
896 }
897
898 #[test]
901 fn no_borders_when_show_borders_disabled() {
902 let mut debugger = LayoutDebugger::new();
903 debugger.set_enabled(true);
904 debugger.record(LayoutRecord::new(
905 "NoBorder",
906 Rect::new(0, 0, 6, 4),
907 Rect::new(0, 0, 6, 4),
908 LayoutConstraints::unconstrained(),
909 ));
910
911 let style = ConstraintOverlayStyle {
912 show_borders: false,
913 show_labels: false,
914 show_size_diff: false,
915 ..Default::default()
916 };
917 let overlay = ConstraintOverlay::new(&debugger).style(style);
918 let mut pool = GraphemePool::new();
919 let mut frame = Frame::new(20, 10, &mut pool);
920 overlay.render(Rect::new(0, 0, 20, 10), &mut frame);
921
922 assert!(frame.buffer.get(0, 0).unwrap().is_empty());
924 }
925}