1#![forbid(unsafe_code)]
2
3use 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#[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 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 #[must_use]
54 pub fn borders(mut self, borders: Borders) -> Self {
55 self.borders = borders;
56 self
57 }
58
59 #[must_use]
61 pub fn border_style(mut self, style: Style) -> Self {
62 self.border_style = style;
63 self
64 }
65
66 #[must_use]
68 pub fn border_type(mut self, border_type: BorderType) -> Self {
69 self.border_type = border_type;
70 self
71 }
72
73 #[must_use]
75 pub fn title(mut self, title: &'a str) -> Self {
76 self.title = Some(title);
77 self
78 }
79
80 #[must_use]
82 pub fn title_alignment(mut self, alignment: Alignment) -> Self {
83 self.title_alignment = alignment;
84 self
85 }
86
87 #[must_use]
89 pub fn title_style(mut self, style: Style) -> Self {
90 self.title_style = style;
91 self
92 }
93
94 #[must_use]
96 pub fn subtitle(mut self, subtitle: &'a str) -> Self {
97 self.subtitle = Some(subtitle);
98 self
99 }
100
101 #[must_use]
103 pub fn subtitle_alignment(mut self, alignment: Alignment) -> Self {
104 self.subtitle_alignment = alignment;
105 self
106 }
107
108 #[must_use]
110 pub fn subtitle_style(mut self, style: Style) -> Self {
111 self.subtitle_style = style;
112 self
113 }
114
115 #[must_use]
117 pub fn style(mut self, style: Style) -> Self {
118 self.style = style;
119 self
120 }
121
122 #[must_use]
124 pub fn padding(mut self, padding: impl Into<Sides>) -> Self {
125 self.padding = padding.into();
126 self
127 }
128
129 pub fn inner(&self, area: Rect) -> Rect {
131 let mut inner = area;
132
133 if self.borders.contains(Borders::LEFT) {
134 inner.x = inner.x.saturating_add(1);
135 inner.width = inner.width.saturating_sub(1);
136 }
137 if self.borders.contains(Borders::TOP) {
138 inner.y = inner.y.saturating_add(1);
139 inner.height = inner.height.saturating_sub(1);
140 }
141 if self.borders.contains(Borders::RIGHT) {
142 inner.width = inner.width.saturating_sub(1);
143 }
144 if self.borders.contains(Borders::BOTTOM) {
145 inner.height = inner.height.saturating_sub(1);
146 }
147
148 inner
149 }
150
151 fn border_cell(&self, c: char, style: Style) -> Cell {
152 let mut cell = Cell::from_char(c);
153 apply_style(&mut cell, style);
154 cell
155 }
156
157 fn pick_border_set(&self, buf: &Buffer) -> BorderSet {
158 let deg = buf.degradation;
159 if !deg.use_unicode_borders() {
160 return BorderSet::ASCII;
161 }
162 self.border_type.to_border_set()
163 }
164
165 fn render_borders(&self, area: Rect, buf: &mut Buffer, set: BorderSet, style: Style) {
166 if area.is_empty() {
167 return;
168 }
169
170 if self.borders.contains(Borders::LEFT) {
172 for y in area.y..area.bottom() {
173 buf.set_fast(area.x, y, self.border_cell(set.vertical, style));
174 }
175 }
176 if self.borders.contains(Borders::RIGHT) {
177 let x = area.right() - 1;
178 for y in area.y..area.bottom() {
179 buf.set_fast(x, y, self.border_cell(set.vertical, style));
180 }
181 }
182 if self.borders.contains(Borders::TOP) {
183 for x in area.x..area.right() {
184 buf.set_fast(x, area.y, self.border_cell(set.horizontal, style));
185 }
186 }
187 if self.borders.contains(Borders::BOTTOM) {
188 let y = area.bottom().saturating_sub(1);
189 for x in area.x..area.right() {
190 buf.set_fast(x, y, self.border_cell(set.horizontal, style));
191 }
192 }
193
194 if self.borders.contains(Borders::LEFT | Borders::TOP) {
196 buf.set_fast(area.x, area.y, self.border_cell(set.top_left, style));
197 }
198 if self.borders.contains(Borders::RIGHT | Borders::TOP) {
199 buf.set_fast(
200 area.right() - 1,
201 area.y,
202 self.border_cell(set.top_right, style),
203 );
204 }
205 if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
206 buf.set_fast(
207 area.x,
208 area.bottom() - 1,
209 self.border_cell(set.bottom_left, style),
210 );
211 }
212 if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
213 buf.set_fast(
214 area.right() - 1,
215 area.bottom() - 1,
216 self.border_cell(set.bottom_right, style),
217 );
218 }
219 }
220
221 fn ellipsize<'s>(&self, s: &'s str, max_width: usize) -> std::borrow::Cow<'s, str> {
222 let total = display_width(s);
223 if total <= max_width {
224 return std::borrow::Cow::Borrowed(s);
225 }
226 if max_width == 0 {
227 return std::borrow::Cow::Borrowed("");
228 }
229
230 if max_width == 1 {
232 return std::borrow::Cow::Borrowed("…");
233 }
234
235 let mut out = String::new();
236 let mut used = 0usize;
237 let target = max_width - 1;
238
239 for g in s.graphemes(true) {
240 let w = grapheme_width(g);
241 if w == 0 {
242 continue;
243 }
244 if used + w > target {
245 break;
246 }
247 out.push_str(g);
248 used += w;
249 }
250
251 out.push('…');
252 std::borrow::Cow::Owned(out)
253 }
254
255 fn render_top_text(
256 &self,
257 area: Rect,
258 frame: &mut Frame,
259 text: &str,
260 alignment: Alignment,
261 style: Style,
262 clear_existing_style: bool,
263 ) {
264 if area.width < 2 {
265 return;
266 }
267
268 let available_width = area.width.saturating_sub(2) as usize;
269 let text = self.ellipsize(text, available_width);
270 let text_width = display_width(text.as_ref()).min(available_width);
271
272 let x = match alignment {
273 Alignment::Left => area.x.saturating_add(1),
274 Alignment::Center => area
275 .x
276 .saturating_add(1)
277 .saturating_add(((available_width.saturating_sub(text_width)) / 2) as u16),
278 Alignment::Right => area
279 .right()
280 .saturating_sub(1)
281 .saturating_sub(text_width as u16),
282 };
283
284 let max_x = area.right().saturating_sub(1);
285 if clear_existing_style && text_width > 0 {
286 frame
287 .buffer
288 .fill(Rect::new(x, area.y, text_width as u16, 1), Cell::default());
289 }
290 draw_text_span(frame, x, area.y, text.as_ref(), style, max_x);
291 }
292
293 fn render_bottom_text(
294 &self,
295 area: Rect,
296 frame: &mut Frame,
297 text: &str,
298 alignment: Alignment,
299 style: Style,
300 clear_existing_style: bool,
301 ) {
302 if area.height < 1 || area.width < 2 {
303 return;
304 }
305
306 let available_width = area.width.saturating_sub(2) as usize;
307 let text = self.ellipsize(text, available_width);
308 let text_width = display_width(text.as_ref()).min(available_width);
309
310 let x = match alignment {
311 Alignment::Left => area.x.saturating_add(1),
312 Alignment::Center => area
313 .x
314 .saturating_add(1)
315 .saturating_add(((available_width.saturating_sub(text_width)) / 2) as u16),
316 Alignment::Right => area
317 .right()
318 .saturating_sub(1)
319 .saturating_sub(text_width as u16),
320 };
321
322 let y = area.bottom().saturating_sub(1);
323 let max_x = area.right().saturating_sub(1);
324 if clear_existing_style && text_width > 0 {
325 frame
326 .buffer
327 .fill(Rect::new(x, y, text_width as u16, 1), Cell::default());
328 }
329 draw_text_span(frame, x, y, text.as_ref(), style, max_x);
330 }
331}
332
333struct ScissorGuard<'a, 'pool> {
334 frame: &'a mut Frame<'pool>,
335}
336
337impl<'a, 'pool> ScissorGuard<'a, 'pool> {
338 fn new(frame: &'a mut Frame<'pool>, rect: Rect) -> Self {
339 frame.buffer.push_scissor(rect);
340 Self { frame }
341 }
342}
343
344impl Drop for ScissorGuard<'_, '_> {
345 fn drop(&mut self) {
346 self.frame.buffer.pop_scissor();
347 }
348}
349
350impl<W: Widget> Widget for Panel<'_, W> {
351 fn render(&self, area: Rect, frame: &mut Frame) {
352 #[cfg(feature = "tracing")]
353 let _span = tracing::debug_span!(
354 "widget_render",
355 widget = "Panel",
356 x = area.x,
357 y = area.y,
358 w = area.width,
359 h = area.height
360 )
361 .entered();
362
363 if area.is_empty() {
364 return;
365 }
366
367 let deg = frame.buffer.degradation;
368 let clear_existing_text_style = !deg.apply_styling();
369 let border_style = if deg.apply_styling() {
370 self.border_style
371 } else {
372 Style::default()
373 };
374
375 if !deg.render_content() {
378 frame.buffer.fill(area, Cell::default());
379 } else if deg.apply_styling() {
380 set_style_area(&mut frame.buffer, area, self.style);
382 }
383
384 if deg.render_decorative() {
386 let set = self.pick_border_set(&frame.buffer);
387 self.render_borders(area, &mut frame.buffer, set, border_style);
388
389 if self.borders.contains(Borders::TOP)
390 && let Some(title) = self.title
391 {
392 let title_style = if deg.apply_styling() {
393 self.title_style.merge(&self.border_style)
394 } else {
395 Style::default()
396 };
397 self.render_top_text(
398 area,
399 frame,
400 title,
401 self.title_alignment,
402 title_style,
403 clear_existing_text_style,
404 );
405 }
406
407 if self.borders.contains(Borders::BOTTOM)
408 && let Some(subtitle) = self.subtitle
409 {
410 let subtitle_style = if deg.apply_styling() {
411 self.subtitle_style.merge(&self.border_style)
412 } else {
413 Style::default()
414 };
415 self.render_bottom_text(
416 area,
417 frame,
418 subtitle,
419 self.subtitle_alignment,
420 subtitle_style,
421 clear_existing_text_style,
422 );
423 }
424 }
425
426 let content_bounds = if deg.render_decorative() {
428 self.inner(area)
429 } else {
430 area
431 };
432 let mut content_area = content_bounds;
433 content_area = content_area.inner(self.padding);
434 if content_area.is_empty() {
435 return;
436 }
437
438 let guard = ScissorGuard::new(frame, content_area);
439 self.child.render(content_area, guard.frame);
440 }
441
442 fn is_essential(&self) -> bool {
443 self.child.is_essential()
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450 use ftui_render::frame::Frame;
451 use ftui_render::grapheme_pool::GraphemePool;
452
453 fn panel_stub() -> Panel<'static, crate::block::Block<'static>> {
454 Panel::new(crate::block::Block::default())
455 }
456
457 fn cell_char(frame: &Frame, x: u16, y: u16) -> Option<char> {
458 frame.buffer.get(x, y).and_then(|c| c.content.as_char())
459 }
460
461 #[test]
464 fn ellipsize_short_is_borrowed() {
465 let panel = panel_stub();
466 let out = panel.ellipsize("abc", 3);
467 assert!(matches!(out, std::borrow::Cow::Borrowed(_)));
468 assert_eq!(out, "abc");
469 }
470
471 #[test]
472 fn ellipsize_truncates_with_ellipsis() {
473 let panel = panel_stub();
474 let out = panel.ellipsize("abcdef", 4);
475 assert_eq!(out, "abc…");
476 }
477
478 #[test]
479 fn ellipsize_zero_width_returns_empty() {
480 let panel = panel_stub();
481 let out = panel.ellipsize("abc", 0);
482 assert_eq!(out, "");
483 }
484
485 #[test]
486 fn ellipsize_width_one_returns_ellipsis() {
487 let panel = panel_stub();
488 let out = panel.ellipsize("abc", 1);
489 assert_eq!(out, "…");
490 }
491
492 #[test]
493 fn ellipsize_exact_fit_is_borrowed() {
494 let panel = panel_stub();
495 let out = panel.ellipsize("hello", 5);
496 assert!(matches!(out, std::borrow::Cow::Borrowed(_)));
497 assert_eq!(out, "hello");
498 }
499
500 #[test]
501 fn ellipsize_one_over_truncates() {
502 let panel = panel_stub();
503 let out = panel.ellipsize("hello", 4);
504 assert_eq!(out, "hel…");
505 }
506
507 #[test]
510 fn inner_all_borders() {
511 let panel = panel_stub().borders(Borders::ALL);
512 let area = Rect::new(0, 0, 10, 10);
513 assert_eq!(panel.inner(area), Rect::new(1, 1, 8, 8));
514 }
515
516 #[test]
517 fn inner_no_borders() {
518 let panel = panel_stub().borders(Borders::NONE);
519 let area = Rect::new(0, 0, 10, 10);
520 assert_eq!(panel.inner(area), area);
521 }
522
523 #[test]
524 fn inner_top_and_left_only() {
525 let panel = panel_stub().borders(Borders::TOP | Borders::LEFT);
526 let area = Rect::new(0, 0, 10, 10);
527 assert_eq!(panel.inner(area), Rect::new(1, 1, 9, 9));
528 }
529
530 #[test]
531 fn inner_right_and_bottom_only() {
532 let panel = panel_stub().borders(Borders::RIGHT | Borders::BOTTOM);
533 let area = Rect::new(0, 0, 10, 10);
534 assert_eq!(panel.inner(area), Rect::new(0, 0, 9, 9));
535 }
536
537 #[test]
538 fn inner_with_offset_area() {
539 let panel = panel_stub().borders(Borders::ALL);
540 let area = Rect::new(5, 3, 10, 8);
541 assert_eq!(panel.inner(area), Rect::new(6, 4, 8, 6));
542 }
543
544 #[test]
545 fn inner_zero_size_saturates() {
546 let panel = panel_stub().borders(Borders::ALL);
547 let area = Rect::new(0, 0, 1, 1);
548 let inner = panel.inner(area);
549 assert_eq!(inner.width, 0);
550 assert_eq!(inner.height, 0);
551 }
552
553 #[test]
556 fn render_borders_square() {
557 let child = crate::block::Block::default();
558 let panel = Panel::new(child)
559 .borders(Borders::ALL)
560 .border_type(BorderType::Square);
561 let area = Rect::new(0, 0, 5, 3);
562 let mut pool = GraphemePool::new();
563 let mut frame = Frame::new(5, 3, &mut pool);
564
565 panel.render(area, &mut frame);
566
567 assert_eq!(cell_char(&frame, 0, 0), Some('┌'));
568 assert_eq!(cell_char(&frame, 4, 0), Some('┐'));
569 assert_eq!(cell_char(&frame, 0, 2), Some('└'));
570 assert_eq!(cell_char(&frame, 4, 2), Some('┘'));
571 assert_eq!(cell_char(&frame, 2, 0), Some('─'));
572 assert_eq!(cell_char(&frame, 0, 1), Some('│'));
573 }
574
575 #[test]
576 fn render_borders_rounded() {
577 let child = crate::block::Block::default();
578 let panel = Panel::new(child)
579 .borders(Borders::ALL)
580 .border_type(BorderType::Rounded);
581 let area = Rect::new(0, 0, 5, 3);
582 let mut pool = GraphemePool::new();
583 let mut frame = Frame::new(5, 3, &mut pool);
584
585 panel.render(area, &mut frame);
586
587 assert_eq!(cell_char(&frame, 0, 0), Some('╭'));
588 assert_eq!(cell_char(&frame, 4, 0), Some('╮'));
589 assert_eq!(cell_char(&frame, 0, 2), Some('╰'));
590 assert_eq!(cell_char(&frame, 4, 2), Some('╯'));
591 }
592
593 #[test]
594 fn render_empty_area_does_not_panic() {
595 let panel = panel_stub().borders(Borders::ALL);
596 let area = Rect::new(0, 0, 0, 0);
597 let mut pool = GraphemePool::new();
598 let mut frame = Frame::new(1, 1, &mut pool);
599 panel.render(area, &mut frame);
600 }
601
602 #[test]
605 fn render_title_left_aligned() {
606 let child = crate::block::Block::default();
607 let panel = Panel::new(child)
608 .borders(Borders::ALL)
609 .border_type(BorderType::Square)
610 .title("Hi")
611 .title_alignment(Alignment::Left);
612 let area = Rect::new(0, 0, 10, 3);
613 let mut pool = GraphemePool::new();
614 let mut frame = Frame::new(10, 3, &mut pool);
615
616 panel.render(area, &mut frame);
617
618 assert_eq!(cell_char(&frame, 1, 0), Some('H'));
620 assert_eq!(cell_char(&frame, 2, 0), Some('i'));
621 }
622
623 #[test]
624 fn render_title_right_aligned() {
625 let child = crate::block::Block::default();
626 let panel = Panel::new(child)
627 .borders(Borders::ALL)
628 .border_type(BorderType::Square)
629 .title("Hi")
630 .title_alignment(Alignment::Right);
631 let area = Rect::new(0, 0, 10, 3);
632 let mut pool = GraphemePool::new();
633 let mut frame = Frame::new(10, 3, &mut pool);
634
635 panel.render(area, &mut frame);
636
637 assert_eq!(cell_char(&frame, 7, 0), Some('H'));
640 assert_eq!(cell_char(&frame, 8, 0), Some('i'));
641 }
642
643 #[test]
644 fn render_title_center_aligned() {
645 let child = crate::block::Block::default();
646 let panel = Panel::new(child)
647 .borders(Borders::ALL)
648 .border_type(BorderType::Square)
649 .title("AB")
650 .title_alignment(Alignment::Center);
651 let area = Rect::new(0, 0, 10, 3);
652 let mut pool = GraphemePool::new();
653 let mut frame = Frame::new(10, 3, &mut pool);
654
655 panel.render(area, &mut frame);
656
657 assert_eq!(cell_char(&frame, 4, 0), Some('A'));
660 assert_eq!(cell_char(&frame, 5, 0), Some('B'));
661 }
662
663 #[test]
664 fn render_title_no_top_border_skips_title() {
665 let child = crate::block::Block::default();
666 let panel = Panel::new(child)
667 .borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM)
668 .title("Hi");
669 let area = Rect::new(0, 0, 10, 3);
670 let mut pool = GraphemePool::new();
671 let mut frame = Frame::new(10, 3, &mut pool);
672
673 panel.render(area, &mut frame);
674
675 assert_ne!(cell_char(&frame, 1, 0), Some('H'));
677 }
678
679 #[test]
680 fn render_title_truncated_with_ellipsis() {
681 let child = crate::block::Block::default();
682 let panel = Panel::new(child)
683 .borders(Borders::ALL)
684 .border_type(BorderType::Square)
685 .title("LongTitle")
686 .title_alignment(Alignment::Left);
687 let area = Rect::new(0, 0, 6, 3);
689 let mut pool = GraphemePool::new();
690 let mut frame = Frame::new(6, 3, &mut pool);
691
692 panel.render(area, &mut frame);
693
694 assert_eq!(cell_char(&frame, 1, 0), Some('L'));
695 assert_eq!(cell_char(&frame, 2, 0), Some('o'));
696 assert_eq!(cell_char(&frame, 3, 0), Some('n'));
697 assert_eq!(cell_char(&frame, 4, 0), Some('…'));
698 }
699
700 #[test]
703 fn render_subtitle_left_aligned() {
704 let child = crate::block::Block::default();
705 let panel = Panel::new(child)
706 .borders(Borders::ALL)
707 .border_type(BorderType::Square)
708 .subtitle("Lo")
709 .subtitle_alignment(Alignment::Left);
710 let area = Rect::new(0, 0, 10, 3);
711 let mut pool = GraphemePool::new();
712 let mut frame = Frame::new(10, 3, &mut pool);
713
714 panel.render(area, &mut frame);
715
716 assert_eq!(cell_char(&frame, 1, 2), Some('L'));
718 assert_eq!(cell_char(&frame, 2, 2), Some('o'));
719 }
720
721 #[test]
722 fn render_subtitle_no_bottom_border_skips() {
723 let child = crate::block::Block::default();
724 let panel = Panel::new(child)
725 .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)
726 .subtitle("Lo");
727 let area = Rect::new(0, 0, 10, 3);
728 let mut pool = GraphemePool::new();
729 let mut frame = Frame::new(10, 3, &mut pool);
730
731 panel.render(area, &mut frame);
732
733 assert_ne!(cell_char(&frame, 1, 2), Some('L'));
735 }
736
737 #[test]
740 fn inner_with_padding_reduces_area() {
741 let panel = panel_stub().borders(Borders::ALL).padding(Sides::all(1));
742 let area = Rect::new(0, 0, 10, 10);
743 let inner_from_borders = panel.inner(area);
745 let padded = inner_from_borders.inner(Sides::all(1));
746 assert_eq!(padded, Rect::new(2, 2, 6, 6));
747 }
748
749 struct MarkerWidget;
753
754 impl Widget for MarkerWidget {
755 fn render(&self, area: Rect, frame: &mut Frame) {
756 if !area.is_empty() {
757 let mut cell = Cell::from_char('X');
758 apply_style(&mut cell, Style::default());
759 frame.buffer.set(area.x, area.y, cell);
760 }
761 }
762 }
763
764 #[test]
765 fn child_rendered_inside_borders() {
766 let panel = Panel::new(MarkerWidget).borders(Borders::ALL);
767 let area = Rect::new(0, 0, 5, 5);
768 let mut pool = GraphemePool::new();
769 let mut frame = Frame::new(5, 5, &mut pool);
770
771 panel.render(area, &mut frame);
772
773 assert_eq!(cell_char(&frame, 1, 1), Some('X'));
775 }
776
777 #[test]
778 fn child_rendered_with_padding_offset() {
779 let panel = Panel::new(MarkerWidget)
780 .borders(Borders::ALL)
781 .padding(Sides::new(1, 1, 0, 1));
782 let area = Rect::new(0, 0, 10, 10);
783 let mut pool = GraphemePool::new();
784 let mut frame = Frame::new(10, 10, &mut pool);
785
786 panel.render(area, &mut frame);
787
788 assert_eq!(cell_char(&frame, 2, 2), Some('X'));
790 }
791
792 #[test]
793 fn child_not_rendered_when_padding_consumes_all_space() {
794 let panel = Panel::new(MarkerWidget)
795 .borders(Borders::ALL)
796 .padding(Sides::all(10));
797 let area = Rect::new(0, 0, 5, 5);
798 let mut pool = GraphemePool::new();
799 let mut frame = Frame::new(5, 5, &mut pool);
800
801 panel.render(area, &mut frame);
803 }
804
805 #[test]
808 fn builder_chain_compiles() {
809 let _panel = Panel::new(crate::block::Block::default())
810 .borders(Borders::ALL)
811 .border_type(BorderType::Double)
812 .border_style(Style::new().bold())
813 .title("Title")
814 .title_alignment(Alignment::Center)
815 .title_style(Style::new().italic())
816 .subtitle("Sub")
817 .subtitle_alignment(Alignment::Right)
818 .subtitle_style(Style::new())
819 .style(Style::new())
820 .padding(Sides::all(1));
821 }
822
823 #[test]
824 fn degradation_no_styling_drops_border_style() {
825 use ftui_render::budget::DegradationLevel;
826 use ftui_render::cell::PackedRgba;
827
828 let panel = Panel::new(crate::block::Block::default())
829 .borders(Borders::ALL)
830 .border_style(Style::new().fg(PackedRgba::rgb(255, 0, 0)).bold());
831 let area = Rect::new(0, 0, 5, 3);
832 let mut pool = GraphemePool::new();
833 let mut frame = Frame::new(5, 3, &mut pool);
834 frame.buffer.degradation = DegradationLevel::NoStyling;
835 panel.render(area, &mut frame);
836
837 let border = frame.buffer.get(0, 0).unwrap();
838 let default_cell = Cell::from_char(border.content.as_char().unwrap());
839 assert_eq!(border.fg, default_cell.fg);
840 assert_eq!(border.bg, default_cell.bg);
841 assert_eq!(border.attrs, default_cell.attrs);
842 }
843
844 #[test]
845 fn degradation_no_styling_title_does_not_inherit_border_style() {
846 use ftui_render::budget::DegradationLevel;
847 use ftui_render::cell::PackedRgba;
848
849 let panel = Panel::new(crate::block::Block::default())
850 .borders(Borders::ALL)
851 .border_style(Style::new().fg(PackedRgba::rgb(255, 0, 0)).bold())
852 .title("Hi");
853 let area = Rect::new(0, 0, 6, 3);
854 let mut pool = GraphemePool::new();
855 let mut frame = Frame::new(6, 3, &mut pool);
856 frame.buffer.degradation = DegradationLevel::NoStyling;
857 panel.render(area, &mut frame);
858
859 let title = frame.buffer.get(1, 0).unwrap();
860 let default_cell = Cell::from_char('H');
861 assert_eq!(title.content.as_char(), Some('H'));
862 assert_eq!(title.fg, default_cell.fg);
863 assert_eq!(title.bg, default_cell.bg);
864 assert_eq!(title.attrs, default_cell.attrs);
865 }
866
867 #[test]
868 fn degradation_no_styling_subtitle_does_not_inherit_border_style() {
869 use ftui_render::budget::DegradationLevel;
870 use ftui_render::cell::PackedRgba;
871
872 let panel = Panel::new(crate::block::Block::default())
873 .borders(Borders::ALL)
874 .border_style(Style::new().fg(PackedRgba::rgb(255, 0, 0)).bold())
875 .subtitle("Lo");
876 let area = Rect::new(0, 0, 6, 3);
877 let mut pool = GraphemePool::new();
878 let mut frame = Frame::new(6, 3, &mut pool);
879 frame.buffer.degradation = DegradationLevel::NoStyling;
880 panel.render(area, &mut frame);
881
882 let subtitle = frame.buffer.get(1, 2).unwrap();
883 let default_cell = Cell::from_char('L');
884 assert_eq!(subtitle.content.as_char(), Some('L'));
885 assert_eq!(subtitle.fg, default_cell.fg);
886 assert_eq!(subtitle.bg, default_cell.bg);
887 assert_eq!(subtitle.attrs, default_cell.attrs);
888 }
889
890 #[test]
891 fn skeleton_still_renders_essential_child() {
892 use ftui_render::budget::DegradationLevel;
893
894 struct EssentialMarker;
895
896 impl Widget for EssentialMarker {
897 fn render(&self, area: Rect, frame: &mut Frame) {
898 frame.buffer.set(area.x, area.y, Cell::from_char('E'));
899 }
900
901 fn is_essential(&self) -> bool {
902 true
903 }
904 }
905
906 let panel = Panel::new(EssentialMarker).borders(Borders::ALL);
907 let area = Rect::new(0, 0, 5, 3);
908 let mut pool = GraphemePool::new();
909 let mut frame = Frame::new(5, 3, &mut pool);
910 frame.buffer.degradation = DegradationLevel::Skeleton;
911
912 panel.render(area, &mut frame);
913
914 assert_eq!(cell_char(&frame, 0, 0), Some('E'));
915 assert!(frame.buffer.get(4, 0).unwrap().is_empty());
916 }
917
918 #[test]
919 fn essential_only_does_not_reserve_hidden_border_space() {
920 use ftui_render::budget::DegradationLevel;
921
922 let panel = Panel::new(MarkerWidget).borders(Borders::ALL);
923 let area = Rect::new(0, 0, 5, 3);
924 let mut pool = GraphemePool::new();
925 let mut frame = Frame::new(5, 3, &mut pool);
926 frame.buffer.degradation = DegradationLevel::EssentialOnly;
927
928 panel.render(area, &mut frame);
929
930 assert_eq!(cell_char(&frame, 0, 0), Some('X'));
931 assert!(frame.buffer.get(1, 0).unwrap().is_empty());
932 }
933}