Skip to main content

ftui_widgets/
panel.rs

1#![forbid(unsafe_code)]
2
3//! Panel widget: border + optional title/subtitle + inner padding + child content.
4
5use crate::block::Alignment;
6use crate::borders::{BorderSet, BorderType, Borders};
7use crate::{Widget, apply_style, draw_text_span, set_style_area};
8use ftui_core::geometry::{Rect, Sides};
9use ftui_render::buffer::Buffer;
10use ftui_render::cell::Cell;
11use ftui_render::frame::Frame;
12use ftui_style::Style;
13use ftui_text::{display_width, grapheme_width};
14use unicode_segmentation::UnicodeSegmentation;
15
16/// A bordered container that renders a child widget inside an inner padded area.
17#[derive(Debug, Clone)]
18pub struct Panel<'a, W> {
19    child: W,
20    borders: Borders,
21    border_style: Style,
22    border_type: BorderType,
23    title: Option<&'a str>,
24    title_alignment: Alignment,
25    title_style: Style,
26    subtitle: Option<&'a str>,
27    subtitle_alignment: Alignment,
28    subtitle_style: Style,
29    style: Style,
30    padding: Sides,
31}
32
33impl<'a, W> Panel<'a, W> {
34    /// Create a new panel wrapping the given child widget.
35    pub fn new(child: W) -> Self {
36        Self {
37            child,
38            borders: Borders::ALL,
39            border_style: Style::default(),
40            border_type: BorderType::Square,
41            title: None,
42            title_alignment: Alignment::Left,
43            title_style: Style::default(),
44            subtitle: None,
45            subtitle_alignment: Alignment::Left,
46            subtitle_style: Style::default(),
47            style: Style::default(),
48            padding: Sides::default(),
49        }
50    }
51
52    /// Set which borders to draw.
53    pub fn borders(mut self, borders: Borders) -> Self {
54        self.borders = borders;
55        self
56    }
57
58    /// Set the style applied to the border lines.
59    pub fn border_style(mut self, style: Style) -> Self {
60        self.border_style = style;
61        self
62    }
63
64    /// Set the border line type (e.g. square, rounded, double).
65    pub fn border_type(mut self, border_type: BorderType) -> Self {
66        self.border_type = border_type;
67        self
68    }
69
70    /// Set the title text displayed on the top border.
71    pub fn title(mut self, title: &'a str) -> Self {
72        self.title = Some(title);
73        self
74    }
75
76    /// Set the horizontal alignment of the title.
77    pub fn title_alignment(mut self, alignment: Alignment) -> Self {
78        self.title_alignment = alignment;
79        self
80    }
81
82    /// Set the style applied to the title text.
83    pub fn title_style(mut self, style: Style) -> Self {
84        self.title_style = style;
85        self
86    }
87
88    /// Set the subtitle text displayed on the bottom border.
89    pub fn subtitle(mut self, subtitle: &'a str) -> Self {
90        self.subtitle = Some(subtitle);
91        self
92    }
93
94    /// Set the horizontal alignment of the subtitle.
95    pub fn subtitle_alignment(mut self, alignment: Alignment) -> Self {
96        self.subtitle_alignment = alignment;
97        self
98    }
99
100    /// Set the style applied to the subtitle text.
101    pub fn subtitle_style(mut self, style: Style) -> Self {
102        self.subtitle_style = style;
103        self
104    }
105
106    /// Set the base style for the entire panel area.
107    pub fn style(mut self, style: Style) -> Self {
108        self.style = style;
109        self
110    }
111
112    /// Set inner padding between the border and child content.
113    pub fn padding(mut self, padding: impl Into<Sides>) -> Self {
114        self.padding = padding.into();
115        self
116    }
117
118    /// Compute the inner area inside the panel borders.
119    pub fn inner(&self, area: Rect) -> Rect {
120        let mut inner = area;
121
122        if self.borders.contains(Borders::LEFT) {
123            inner.x = inner.x.saturating_add(1);
124            inner.width = inner.width.saturating_sub(1);
125        }
126        if self.borders.contains(Borders::TOP) {
127            inner.y = inner.y.saturating_add(1);
128            inner.height = inner.height.saturating_sub(1);
129        }
130        if self.borders.contains(Borders::RIGHT) {
131            inner.width = inner.width.saturating_sub(1);
132        }
133        if self.borders.contains(Borders::BOTTOM) {
134            inner.height = inner.height.saturating_sub(1);
135        }
136
137        inner
138    }
139
140    fn border_cell(&self, c: char) -> Cell {
141        let mut cell = Cell::from_char(c);
142        apply_style(&mut cell, self.border_style);
143        cell
144    }
145
146    fn pick_border_set(&self, buf: &Buffer) -> BorderSet {
147        let deg = buf.degradation;
148        if !deg.use_unicode_borders() {
149            return BorderSet::ASCII;
150        }
151        self.border_type.to_border_set()
152    }
153
154    fn render_borders(&self, area: Rect, buf: &mut Buffer, set: BorderSet) {
155        if area.is_empty() {
156            return;
157        }
158
159        // Edges
160        if self.borders.contains(Borders::LEFT) {
161            for y in area.y..area.bottom() {
162                buf.set(area.x, y, self.border_cell(set.vertical));
163            }
164        }
165        if self.borders.contains(Borders::RIGHT) {
166            let x = area.right() - 1;
167            for y in area.y..area.bottom() {
168                buf.set(x, y, self.border_cell(set.vertical));
169            }
170        }
171        if self.borders.contains(Borders::TOP) {
172            for x in area.x..area.right() {
173                buf.set(x, area.y, self.border_cell(set.horizontal));
174            }
175        }
176        if self.borders.contains(Borders::BOTTOM) {
177            let y = area.bottom().saturating_sub(1);
178            for x in area.x..area.right() {
179                buf.set(x, y, self.border_cell(set.horizontal));
180            }
181        }
182
183        // Corners (drawn after edges)
184        if self.borders.contains(Borders::LEFT | Borders::TOP) {
185            buf.set(area.x, area.y, self.border_cell(set.top_left));
186        }
187        if self.borders.contains(Borders::RIGHT | Borders::TOP) {
188            buf.set(area.right() - 1, area.y, self.border_cell(set.top_right));
189        }
190        if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
191            buf.set(area.x, area.bottom() - 1, self.border_cell(set.bottom_left));
192        }
193        if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
194            buf.set(
195                area.right() - 1,
196                area.bottom() - 1,
197                self.border_cell(set.bottom_right),
198            );
199        }
200    }
201
202    fn ellipsize<'s>(&self, s: &'s str, max_width: usize) -> std::borrow::Cow<'s, str> {
203        let total = display_width(s);
204        if total <= max_width {
205            return std::borrow::Cow::Borrowed(s);
206        }
207        if max_width == 0 {
208            return std::borrow::Cow::Borrowed("");
209        }
210
211        // Use a single-cell ellipsis.
212        if max_width == 1 {
213            return std::borrow::Cow::Borrowed("…");
214        }
215
216        let mut out = String::new();
217        let mut used = 0usize;
218        let target = max_width - 1;
219
220        for g in s.graphemes(true) {
221            let w = grapheme_width(g);
222            if w == 0 {
223                continue;
224            }
225            if used + w > target {
226                break;
227            }
228            out.push_str(g);
229            used += w;
230        }
231
232        out.push('…');
233        std::borrow::Cow::Owned(out)
234    }
235
236    fn render_top_text(
237        &self,
238        area: Rect,
239        frame: &mut Frame,
240        text: &str,
241        alignment: Alignment,
242        style: Style,
243    ) {
244        if area.width < 2 {
245            return;
246        }
247
248        let available_width = area.width.saturating_sub(2) as usize;
249        let text = self.ellipsize(text, available_width);
250        let text_width = display_width(text.as_ref()).min(available_width);
251
252        let x = match alignment {
253            Alignment::Left => area.x.saturating_add(1),
254            Alignment::Center => area
255                .x
256                .saturating_add(1)
257                .saturating_add(((available_width.saturating_sub(text_width)) / 2) as u16),
258            Alignment::Right => area
259                .right()
260                .saturating_sub(1)
261                .saturating_sub(text_width as u16),
262        };
263
264        let max_x = area.right().saturating_sub(1);
265        draw_text_span(frame, x, area.y, text.as_ref(), style, max_x);
266    }
267
268    fn render_bottom_text(
269        &self,
270        area: Rect,
271        frame: &mut Frame,
272        text: &str,
273        alignment: Alignment,
274        style: Style,
275    ) {
276        if area.height < 1 || area.width < 2 {
277            return;
278        }
279
280        let available_width = area.width.saturating_sub(2) as usize;
281        let text = self.ellipsize(text, available_width);
282        let text_width = display_width(text.as_ref()).min(available_width);
283
284        let x = match alignment {
285            Alignment::Left => area.x.saturating_add(1),
286            Alignment::Center => area
287                .x
288                .saturating_add(1)
289                .saturating_add(((available_width.saturating_sub(text_width)) / 2) as u16),
290            Alignment::Right => area
291                .right()
292                .saturating_sub(1)
293                .saturating_sub(text_width as u16),
294        };
295
296        let y = area.bottom().saturating_sub(1);
297        let max_x = area.right().saturating_sub(1);
298        draw_text_span(frame, x, y, text.as_ref(), style, max_x);
299    }
300}
301
302struct ScissorGuard<'a, 'pool> {
303    frame: &'a mut Frame<'pool>,
304}
305
306impl<'a, 'pool> ScissorGuard<'a, 'pool> {
307    fn new(frame: &'a mut Frame<'pool>, rect: Rect) -> Self {
308        frame.buffer.push_scissor(rect);
309        Self { frame }
310    }
311}
312
313impl Drop for ScissorGuard<'_, '_> {
314    fn drop(&mut self) {
315        self.frame.buffer.pop_scissor();
316    }
317}
318
319impl<W: Widget> Widget for Panel<'_, W> {
320    fn render(&self, area: Rect, frame: &mut Frame) {
321        #[cfg(feature = "tracing")]
322        let _span = tracing::debug_span!(
323            "widget_render",
324            widget = "Panel",
325            x = area.x,
326            y = area.y,
327            w = area.width,
328            h = area.height
329        )
330        .entered();
331
332        if area.is_empty() {
333            return;
334        }
335
336        let deg = frame.buffer.degradation;
337
338        // Skeleton+: skip everything, just clear area
339        if !deg.render_content() {
340            frame.buffer.fill(area, Cell::default());
341            return;
342        }
343
344        // Background/style
345        if deg.apply_styling() {
346            set_style_area(&mut frame.buffer, area, self.style);
347        }
348
349        // Decorative layer: borders + title/subtitle
350        if deg.render_decorative() {
351            let set = self.pick_border_set(&frame.buffer);
352            self.render_borders(area, &mut frame.buffer, set);
353
354            if self.borders.contains(Borders::TOP)
355                && let Some(title) = self.title
356            {
357                let title_style = if deg.apply_styling() {
358                    self.title_style.merge(&self.border_style)
359                } else {
360                    Style::default()
361                };
362                self.render_top_text(area, frame, title, self.title_alignment, title_style);
363            }
364
365            if self.borders.contains(Borders::BOTTOM)
366                && let Some(subtitle) = self.subtitle
367            {
368                let subtitle_style = if deg.apply_styling() {
369                    self.subtitle_style.merge(&self.border_style)
370                } else {
371                    Style::default()
372                };
373                self.render_bottom_text(
374                    area,
375                    frame,
376                    subtitle,
377                    self.subtitle_alignment,
378                    subtitle_style,
379                );
380            }
381        }
382
383        // Content
384        let mut content_area = self.inner(area);
385        content_area = content_area.inner(self.padding);
386        if content_area.is_empty() {
387            return;
388        }
389
390        let guard = ScissorGuard::new(frame, content_area);
391        self.child.render(content_area, guard.frame);
392    }
393
394    fn is_essential(&self) -> bool {
395        self.child.is_essential()
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402    use ftui_render::frame::Frame;
403    use ftui_render::grapheme_pool::GraphemePool;
404
405    fn panel_stub() -> Panel<'static, crate::block::Block<'static>> {
406        Panel::new(crate::block::Block::default())
407    }
408
409    fn cell_char(frame: &Frame, x: u16, y: u16) -> Option<char> {
410        frame.buffer.get(x, y).and_then(|c| c.content.as_char())
411    }
412
413    // --- ellipsize tests ---
414
415    #[test]
416    fn ellipsize_short_is_borrowed() {
417        let panel = panel_stub();
418        let out = panel.ellipsize("abc", 3);
419        assert!(matches!(out, std::borrow::Cow::Borrowed(_)));
420        assert_eq!(out, "abc");
421    }
422
423    #[test]
424    fn ellipsize_truncates_with_ellipsis() {
425        let panel = panel_stub();
426        let out = panel.ellipsize("abcdef", 4);
427        assert_eq!(out, "abc…");
428    }
429
430    #[test]
431    fn ellipsize_zero_width_returns_empty() {
432        let panel = panel_stub();
433        let out = panel.ellipsize("abc", 0);
434        assert_eq!(out, "");
435    }
436
437    #[test]
438    fn ellipsize_width_one_returns_ellipsis() {
439        let panel = panel_stub();
440        let out = panel.ellipsize("abc", 1);
441        assert_eq!(out, "…");
442    }
443
444    #[test]
445    fn ellipsize_exact_fit_is_borrowed() {
446        let panel = panel_stub();
447        let out = panel.ellipsize("hello", 5);
448        assert!(matches!(out, std::borrow::Cow::Borrowed(_)));
449        assert_eq!(out, "hello");
450    }
451
452    #[test]
453    fn ellipsize_one_over_truncates() {
454        let panel = panel_stub();
455        let out = panel.ellipsize("hello", 4);
456        assert_eq!(out, "hel…");
457    }
458
459    // --- inner() calculation tests ---
460
461    #[test]
462    fn inner_all_borders() {
463        let panel = panel_stub().borders(Borders::ALL);
464        let area = Rect::new(0, 0, 10, 10);
465        assert_eq!(panel.inner(area), Rect::new(1, 1, 8, 8));
466    }
467
468    #[test]
469    fn inner_no_borders() {
470        let panel = panel_stub().borders(Borders::NONE);
471        let area = Rect::new(0, 0, 10, 10);
472        assert_eq!(panel.inner(area), area);
473    }
474
475    #[test]
476    fn inner_top_and_left_only() {
477        let panel = panel_stub().borders(Borders::TOP | Borders::LEFT);
478        let area = Rect::new(0, 0, 10, 10);
479        assert_eq!(panel.inner(area), Rect::new(1, 1, 9, 9));
480    }
481
482    #[test]
483    fn inner_right_and_bottom_only() {
484        let panel = panel_stub().borders(Borders::RIGHT | Borders::BOTTOM);
485        let area = Rect::new(0, 0, 10, 10);
486        assert_eq!(panel.inner(area), Rect::new(0, 0, 9, 9));
487    }
488
489    #[test]
490    fn inner_with_offset_area() {
491        let panel = panel_stub().borders(Borders::ALL);
492        let area = Rect::new(5, 3, 10, 8);
493        assert_eq!(panel.inner(area), Rect::new(6, 4, 8, 6));
494    }
495
496    #[test]
497    fn inner_zero_size_saturates() {
498        let panel = panel_stub().borders(Borders::ALL);
499        let area = Rect::new(0, 0, 1, 1);
500        let inner = panel.inner(area);
501        assert_eq!(inner.width, 0);
502        assert_eq!(inner.height, 0);
503    }
504
505    // --- render border tests ---
506
507    #[test]
508    fn render_borders_square() {
509        let child = crate::block::Block::default();
510        let panel = Panel::new(child)
511            .borders(Borders::ALL)
512            .border_type(BorderType::Square);
513        let area = Rect::new(0, 0, 5, 3);
514        let mut pool = GraphemePool::new();
515        let mut frame = Frame::new(5, 3, &mut pool);
516
517        panel.render(area, &mut frame);
518
519        assert_eq!(cell_char(&frame, 0, 0), Some('┌'));
520        assert_eq!(cell_char(&frame, 4, 0), Some('┐'));
521        assert_eq!(cell_char(&frame, 0, 2), Some('└'));
522        assert_eq!(cell_char(&frame, 4, 2), Some('┘'));
523        assert_eq!(cell_char(&frame, 2, 0), Some('─'));
524        assert_eq!(cell_char(&frame, 0, 1), Some('│'));
525    }
526
527    #[test]
528    fn render_borders_rounded() {
529        let child = crate::block::Block::default();
530        let panel = Panel::new(child)
531            .borders(Borders::ALL)
532            .border_type(BorderType::Rounded);
533        let area = Rect::new(0, 0, 5, 3);
534        let mut pool = GraphemePool::new();
535        let mut frame = Frame::new(5, 3, &mut pool);
536
537        panel.render(area, &mut frame);
538
539        assert_eq!(cell_char(&frame, 0, 0), Some('╭'));
540        assert_eq!(cell_char(&frame, 4, 0), Some('╮'));
541        assert_eq!(cell_char(&frame, 0, 2), Some('╰'));
542        assert_eq!(cell_char(&frame, 4, 2), Some('╯'));
543    }
544
545    #[test]
546    fn render_empty_area_does_not_panic() {
547        let panel = panel_stub().borders(Borders::ALL);
548        let area = Rect::new(0, 0, 0, 0);
549        let mut pool = GraphemePool::new();
550        let mut frame = Frame::new(1, 1, &mut pool);
551        panel.render(area, &mut frame);
552    }
553
554    // --- title rendering tests ---
555
556    #[test]
557    fn render_title_left_aligned() {
558        let child = crate::block::Block::default();
559        let panel = Panel::new(child)
560            .borders(Borders::ALL)
561            .border_type(BorderType::Square)
562            .title("Hi")
563            .title_alignment(Alignment::Left);
564        let area = Rect::new(0, 0, 10, 3);
565        let mut pool = GraphemePool::new();
566        let mut frame = Frame::new(10, 3, &mut pool);
567
568        panel.render(area, &mut frame);
569
570        // Title starts at x=1 (after left border)
571        assert_eq!(cell_char(&frame, 1, 0), Some('H'));
572        assert_eq!(cell_char(&frame, 2, 0), Some('i'));
573    }
574
575    #[test]
576    fn render_title_right_aligned() {
577        let child = crate::block::Block::default();
578        let panel = Panel::new(child)
579            .borders(Borders::ALL)
580            .border_type(BorderType::Square)
581            .title("Hi")
582            .title_alignment(Alignment::Right);
583        let area = Rect::new(0, 0, 10, 3);
584        let mut pool = GraphemePool::new();
585        let mut frame = Frame::new(10, 3, &mut pool);
586
587        panel.render(area, &mut frame);
588
589        // "Hi" is 2 chars, right edge is 9, so title at 9-1-2=6..8
590        // right() = 10, sub 1 = 9, sub 2 = 7
591        assert_eq!(cell_char(&frame, 7, 0), Some('H'));
592        assert_eq!(cell_char(&frame, 8, 0), Some('i'));
593    }
594
595    #[test]
596    fn render_title_center_aligned() {
597        let child = crate::block::Block::default();
598        let panel = Panel::new(child)
599            .borders(Borders::ALL)
600            .border_type(BorderType::Square)
601            .title("AB")
602            .title_alignment(Alignment::Center);
603        let area = Rect::new(0, 0, 10, 3);
604        let mut pool = GraphemePool::new();
605        let mut frame = Frame::new(10, 3, &mut pool);
606
607        panel.render(area, &mut frame);
608
609        // available_width = 10-2 = 8, display_width = 2
610        // x = 0 + 1 + (8-2)/2 = 1 + 3 = 4
611        assert_eq!(cell_char(&frame, 4, 0), Some('A'));
612        assert_eq!(cell_char(&frame, 5, 0), Some('B'));
613    }
614
615    #[test]
616    fn render_title_no_top_border_skips_title() {
617        let child = crate::block::Block::default();
618        let panel = Panel::new(child)
619            .borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM)
620            .title("Hi");
621        let area = Rect::new(0, 0, 10, 3);
622        let mut pool = GraphemePool::new();
623        let mut frame = Frame::new(10, 3, &mut pool);
624
625        panel.render(area, &mut frame);
626
627        // Title should NOT appear on row 0 since no top border
628        assert_ne!(cell_char(&frame, 1, 0), Some('H'));
629    }
630
631    #[test]
632    fn render_title_truncated_with_ellipsis() {
633        let child = crate::block::Block::default();
634        let panel = Panel::new(child)
635            .borders(Borders::ALL)
636            .border_type(BorderType::Square)
637            .title("LongTitle")
638            .title_alignment(Alignment::Left);
639        // Width 6: available = 6-2 = 4, "LongTitle" (9 chars) -> "Lon…"
640        let area = Rect::new(0, 0, 6, 3);
641        let mut pool = GraphemePool::new();
642        let mut frame = Frame::new(6, 3, &mut pool);
643
644        panel.render(area, &mut frame);
645
646        assert_eq!(cell_char(&frame, 1, 0), Some('L'));
647        assert_eq!(cell_char(&frame, 2, 0), Some('o'));
648        assert_eq!(cell_char(&frame, 3, 0), Some('n'));
649        assert_eq!(cell_char(&frame, 4, 0), Some('…'));
650    }
651
652    // --- subtitle rendering tests ---
653
654    #[test]
655    fn render_subtitle_left_aligned() {
656        let child = crate::block::Block::default();
657        let panel = Panel::new(child)
658            .borders(Borders::ALL)
659            .border_type(BorderType::Square)
660            .subtitle("Lo")
661            .subtitle_alignment(Alignment::Left);
662        let area = Rect::new(0, 0, 10, 3);
663        let mut pool = GraphemePool::new();
664        let mut frame = Frame::new(10, 3, &mut pool);
665
666        panel.render(area, &mut frame);
667
668        // Subtitle on bottom row (y=2), starting at x=1
669        assert_eq!(cell_char(&frame, 1, 2), Some('L'));
670        assert_eq!(cell_char(&frame, 2, 2), Some('o'));
671    }
672
673    #[test]
674    fn render_subtitle_no_bottom_border_skips() {
675        let child = crate::block::Block::default();
676        let panel = Panel::new(child)
677            .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)
678            .subtitle("Lo");
679        let area = Rect::new(0, 0, 10, 3);
680        let mut pool = GraphemePool::new();
681        let mut frame = Frame::new(10, 3, &mut pool);
682
683        panel.render(area, &mut frame);
684
685        // Subtitle should not appear since no bottom border
686        assert_ne!(cell_char(&frame, 1, 2), Some('L'));
687    }
688
689    // --- padding tests ---
690
691    #[test]
692    fn inner_with_padding_reduces_area() {
693        let panel = panel_stub().borders(Borders::ALL).padding(Sides::all(1));
694        let area = Rect::new(0, 0, 10, 10);
695        // inner from borders = (1,1,8,8), then padding of 1 on each side = (2,2,6,6)
696        let inner_from_borders = panel.inner(area);
697        let padded = inner_from_borders.inner(Sides::all(1));
698        assert_eq!(padded, Rect::new(2, 2, 6, 6));
699    }
700
701    // --- child rendering tests ---
702
703    /// A simple test widget that writes 'X' at (0,0) relative to its area.
704    struct MarkerWidget;
705
706    impl Widget for MarkerWidget {
707        fn render(&self, area: Rect, frame: &mut Frame) {
708            if !area.is_empty() {
709                let mut cell = Cell::from_char('X');
710                apply_style(&mut cell, Style::default());
711                frame.buffer.set(area.x, area.y, cell);
712            }
713        }
714    }
715
716    #[test]
717    fn child_rendered_inside_borders() {
718        let panel = Panel::new(MarkerWidget).borders(Borders::ALL);
719        let area = Rect::new(0, 0, 5, 5);
720        let mut pool = GraphemePool::new();
721        let mut frame = Frame::new(5, 5, &mut pool);
722
723        panel.render(area, &mut frame);
724
725        // Child area starts at (1,1) for ALL borders
726        assert_eq!(cell_char(&frame, 1, 1), Some('X'));
727    }
728
729    #[test]
730    fn child_rendered_with_padding_offset() {
731        let panel = Panel::new(MarkerWidget)
732            .borders(Borders::ALL)
733            .padding(Sides::new(1, 1, 0, 1));
734        let area = Rect::new(0, 0, 10, 10);
735        let mut pool = GraphemePool::new();
736        let mut frame = Frame::new(10, 10, &mut pool);
737
738        panel.render(area, &mut frame);
739
740        // borders inner = (1,1,8,8), padding top=1 left=1 -> child at (2,2)
741        assert_eq!(cell_char(&frame, 2, 2), Some('X'));
742    }
743
744    #[test]
745    fn child_not_rendered_when_padding_consumes_all_space() {
746        let panel = Panel::new(MarkerWidget)
747            .borders(Borders::ALL)
748            .padding(Sides::all(10));
749        let area = Rect::new(0, 0, 5, 5);
750        let mut pool = GraphemePool::new();
751        let mut frame = Frame::new(5, 5, &mut pool);
752
753        // Should not panic even though padding exceeds available space
754        panel.render(area, &mut frame);
755    }
756
757    // --- builder chain test ---
758
759    #[test]
760    fn builder_chain_compiles() {
761        let _panel = Panel::new(crate::block::Block::default())
762            .borders(Borders::ALL)
763            .border_type(BorderType::Double)
764            .border_style(Style::new().bold())
765            .title("Title")
766            .title_alignment(Alignment::Center)
767            .title_style(Style::new().italic())
768            .subtitle("Sub")
769            .subtitle_alignment(Alignment::Right)
770            .subtitle_style(Style::new())
771            .style(Style::new())
772            .padding(Sides::all(1));
773    }
774}