1#![forbid(unsafe_code)]
2
3use crate::Widget;
4use crate::borders::{BorderSet, BorderType, Borders};
5use crate::measurable::{MeasurableWidget, SizeConstraints};
6use crate::{apply_style, draw_text_span, set_style_area};
7use ftui_core::geometry::{Rect, Size};
8use ftui_render::buffer::Buffer;
9use ftui_render::cell::Cell;
10use ftui_render::frame::Frame;
11use ftui_style::Style;
12use ftui_text::{grapheme_width, graphemes};
13
14#[derive(Debug, Clone, PartialEq, Eq, Default)]
16pub struct Block<'a> {
17 borders: Borders,
18 border_style: Style,
19 border_type: BorderType,
20 title: Option<&'a str>,
21 title_alignment: Alignment,
22 style: Style,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum Alignment {
28 #[default]
29 Left,
31 Center,
33 Right,
35}
36
37impl<'a> Block<'a> {
38 pub fn new() -> Self {
40 Self::default()
41 }
42
43 pub fn bordered() -> Self {
45 Self::default().borders(Borders::ALL)
46 }
47
48 pub fn borders(mut self, borders: Borders) -> Self {
50 self.borders = borders;
51 self
52 }
53
54 pub fn border_style(mut self, style: Style) -> Self {
56 self.border_style = style;
57 self
58 }
59
60 pub fn border_type(mut self, border_type: BorderType) -> Self {
62 self.border_type = border_type;
63 self
64 }
65
66 pub(crate) fn border_set(&self) -> BorderSet {
68 self.border_type.to_border_set()
69 }
70
71 pub fn title(mut self, title: &'a str) -> Self {
73 self.title = Some(title);
74 self
75 }
76
77 pub fn title_alignment(mut self, alignment: Alignment) -> Self {
79 self.title_alignment = alignment;
80 self
81 }
82
83 pub fn style(mut self, style: Style) -> Self {
85 self.style = style;
86 self
87 }
88
89 pub fn inner(&self, area: Rect) -> Rect {
91 let mut inner = area;
92
93 if self.borders.contains(Borders::LEFT) {
94 inner.x = inner.x.saturating_add(1);
95 inner.width = inner.width.saturating_sub(1);
96 }
97 if self.borders.contains(Borders::TOP) {
98 inner.y = inner.y.saturating_add(1);
99 inner.height = inner.height.saturating_sub(1);
100 }
101 if self.borders.contains(Borders::RIGHT) {
102 inner.width = inner.width.saturating_sub(1);
103 }
104 if self.borders.contains(Borders::BOTTOM) {
105 inner.height = inner.height.saturating_sub(1);
106 }
107
108 inner
109 }
110
111 pub fn chrome_size(&self) -> (u16, u16) {
116 let horizontal = self.borders.contains(Borders::LEFT) as u16
117 + self.borders.contains(Borders::RIGHT) as u16;
118 let vertical = self.borders.contains(Borders::TOP) as u16
119 + self.borders.contains(Borders::BOTTOM) as u16;
120 (horizontal, vertical)
121 }
122
123 fn border_cell(&self, c: char) -> Cell {
125 let mut cell = Cell::from_char(c);
126 apply_style(&mut cell, self.border_style);
127 cell
128 }
129
130 fn render_borders(&self, area: Rect, buf: &mut Buffer) {
131 if area.is_empty() {
132 return;
133 }
134
135 let set = self.border_set();
136
137 if self.borders.contains(Borders::LEFT) {
139 for y in area.y..area.bottom() {
140 buf.set(area.x, y, self.border_cell(set.vertical));
141 }
142 }
143 if self.borders.contains(Borders::RIGHT) {
144 let x = area.right() - 1;
145 for y in area.y..area.bottom() {
146 buf.set(x, y, self.border_cell(set.vertical));
147 }
148 }
149 if self.borders.contains(Borders::TOP) {
150 for x in area.x..area.right() {
151 buf.set(x, area.y, self.border_cell(set.horizontal));
152 }
153 }
154 if self.borders.contains(Borders::BOTTOM) {
155 let y = area.bottom() - 1;
156 for x in area.x..area.right() {
157 buf.set(x, y, self.border_cell(set.horizontal));
158 }
159 }
160
161 if self.borders.contains(Borders::LEFT | Borders::TOP) {
163 buf.set(area.x, area.y, self.border_cell(set.top_left));
164 }
165 if self.borders.contains(Borders::RIGHT | Borders::TOP) {
166 buf.set(area.right() - 1, area.y, self.border_cell(set.top_right));
167 }
168 if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
169 buf.set(area.x, area.bottom() - 1, self.border_cell(set.bottom_left));
170 }
171 if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
172 buf.set(
173 area.right() - 1,
174 area.bottom() - 1,
175 self.border_cell(set.bottom_right),
176 );
177 }
178 }
179
180 fn render_borders_ascii(&self, area: Rect, buf: &mut Buffer) {
182 if area.is_empty() {
183 return;
184 }
185
186 let set = crate::borders::BorderSet::ASCII;
187
188 if self.borders.contains(Borders::LEFT) {
189 for y in area.y..area.bottom() {
190 buf.set(area.x, y, self.border_cell(set.vertical));
191 }
192 }
193 if self.borders.contains(Borders::RIGHT) {
194 let x = area.right() - 1;
195 for y in area.y..area.bottom() {
196 buf.set(x, y, self.border_cell(set.vertical));
197 }
198 }
199 if self.borders.contains(Borders::TOP) {
200 for x in area.x..area.right() {
201 buf.set(x, area.y, self.border_cell(set.horizontal));
202 }
203 }
204 if self.borders.contains(Borders::BOTTOM) {
205 let y = area.bottom() - 1;
206 for x in area.x..area.right() {
207 buf.set(x, y, self.border_cell(set.horizontal));
208 }
209 }
210
211 if self.borders.contains(Borders::LEFT | Borders::TOP) {
212 buf.set(area.x, area.y, self.border_cell(set.top_left));
213 }
214 if self.borders.contains(Borders::RIGHT | Borders::TOP) {
215 buf.set(area.right() - 1, area.y, self.border_cell(set.top_right));
216 }
217 if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
218 buf.set(area.x, area.bottom() - 1, self.border_cell(set.bottom_left));
219 }
220 if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
221 buf.set(
222 area.right() - 1,
223 area.bottom() - 1,
224 self.border_cell(set.bottom_right),
225 );
226 }
227 }
228
229 fn render_title(&self, area: Rect, frame: &mut Frame) {
230 if let Some(title) = self.title {
231 if !self.borders.contains(Borders::TOP) || area.width < 3 {
232 return;
233 }
234
235 let available_width = area.width.saturating_sub(2) as usize;
236 if available_width == 0 {
237 return;
238 }
239
240 let title_width = text_width(title);
241 let display_width = title_width.min(available_width);
242
243 let x = match self.title_alignment {
244 Alignment::Left => area.x.saturating_add(1),
245 Alignment::Center => area
246 .x
247 .saturating_add(1)
248 .saturating_add(((available_width.saturating_sub(display_width)) / 2) as u16),
249 Alignment::Right => area
250 .right()
251 .saturating_sub(1)
252 .saturating_sub(display_width as u16),
253 };
254
255 let max_x = area.right().saturating_sub(1);
256 draw_text_span(frame, x, area.y, title, self.border_style, max_x);
257 }
258 }
259}
260
261impl Widget for Block<'_> {
262 fn render(&self, area: Rect, frame: &mut Frame) {
263 #[cfg(feature = "tracing")]
264 let _span = tracing::debug_span!(
265 "widget_render",
266 widget = "Block",
267 x = area.x,
268 y = area.y,
269 w = area.width,
270 h = area.height
271 )
272 .entered();
273
274 if area.is_empty() {
275 return;
276 }
277
278 let deg = frame.degradation;
279
280 if !deg.render_content() {
282 frame.buffer.fill(area, Cell::default());
283 return;
284 }
285
286 if !deg.render_decorative() {
288 if deg.apply_styling() {
289 set_style_area(&mut frame.buffer, area, self.style);
290 }
291 return;
292 }
293
294 if deg.apply_styling() {
296 set_style_area(&mut frame.buffer, area, self.style);
297 }
298
299 if deg.use_unicode_borders() {
301 self.render_borders(area, &mut frame.buffer);
302 } else {
303 self.render_borders_ascii(area, &mut frame.buffer);
305 }
306
307 if deg.apply_styling() {
309 self.render_title(area, frame);
310 } else if deg.render_decorative() {
311 if let Some(title) = self.title
314 && self.borders.contains(Borders::TOP)
315 && area.width >= 3
316 {
317 let available_width = area.width.saturating_sub(2) as usize;
318 if available_width > 0 {
319 let title_width = text_width(title);
320 let display_width = title_width.min(available_width);
321 let x = match self.title_alignment {
322 Alignment::Left => area.x.saturating_add(1),
323 Alignment::Center => area.x.saturating_add(1).saturating_add(
324 ((available_width.saturating_sub(display_width)) / 2) as u16,
325 ),
326 Alignment::Right => area
327 .right()
328 .saturating_sub(1)
329 .saturating_sub(display_width as u16),
330 };
331 let max_x = area.right().saturating_sub(1);
332 draw_text_span(frame, x, area.y, title, Style::default(), max_x);
333 }
334 }
335 }
336 }
337}
338
339impl MeasurableWidget for Block<'_> {
340 fn measure(&self, _available: Size) -> SizeConstraints {
341 let (chrome_width, chrome_height) = self.chrome_size();
342 let chrome = Size::new(chrome_width, chrome_height);
343
344 SizeConstraints::at_least(chrome, chrome)
349 }
350
351 fn has_intrinsic_size(&self) -> bool {
352 self.borders != Borders::empty()
354 }
355}
356
357fn text_width(text: &str) -> usize {
358 if text.is_ascii() {
359 return text.len();
360 }
361 graphemes(text).map(grapheme_width).sum()
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367 use ftui_render::cell::PackedRgba;
368 use ftui_render::grapheme_pool::GraphemePool;
369
370 #[test]
371 fn inner_with_all_borders() {
372 let block = Block::new().borders(Borders::ALL);
373 let area = Rect::new(0, 0, 10, 10);
374 let inner = block.inner(area);
375 assert_eq!(inner, Rect::new(1, 1, 8, 8));
376 }
377
378 #[test]
379 fn inner_with_no_borders() {
380 let block = Block::new();
381 let area = Rect::new(0, 0, 10, 10);
382 let inner = block.inner(area);
383 assert_eq!(inner, area);
384 }
385
386 #[test]
387 fn inner_with_partial_borders() {
388 let block = Block::new().borders(Borders::TOP | Borders::LEFT);
389 let area = Rect::new(0, 0, 10, 10);
390 let inner = block.inner(area);
391 assert_eq!(inner, Rect::new(1, 1, 9, 9));
392 }
393
394 #[test]
395 fn render_empty_area() {
396 let block = Block::new().borders(Borders::ALL);
397 let area = Rect::new(0, 0, 0, 0);
398 let mut pool = GraphemePool::new();
399 let mut frame = Frame::new(1, 1, &mut pool);
400 block.render(area, &mut frame);
401 }
402
403 #[test]
404 fn render_block_with_square_borders() {
405 let block = Block::new()
406 .borders(Borders::ALL)
407 .border_type(BorderType::Square);
408 let area = Rect::new(0, 0, 5, 3);
409 let mut pool = GraphemePool::new();
410 let mut frame = Frame::new(5, 3, &mut pool);
411 block.render(area, &mut frame);
412
413 let buf = &frame.buffer;
414 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('┌'));
415 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('┐'));
416 assert_eq!(buf.get(0, 2).unwrap().content.as_char(), Some('└'));
417 assert_eq!(buf.get(4, 2).unwrap().content.as_char(), Some('┘'));
418 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('─'));
419 assert_eq!(buf.get(0, 1).unwrap().content.as_char(), Some('│'));
420 }
421
422 #[test]
423 fn render_block_with_title() {
424 let block = Block::new()
425 .borders(Borders::ALL)
426 .border_type(BorderType::Square)
427 .title("Hi");
428 let area = Rect::new(0, 0, 10, 3);
429 let mut pool = GraphemePool::new();
430 let mut frame = Frame::new(10, 3, &mut pool);
431 block.render(area, &mut frame);
432
433 let buf = &frame.buffer;
434 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('H'));
435 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('i'));
436 }
437
438 #[test]
439 fn render_title_overrides_on_multiple_calls() {
440 let block = Block::new()
441 .borders(Borders::ALL)
442 .border_type(BorderType::Square)
443 .title("First")
444 .title("Second");
445 let area = Rect::new(0, 0, 12, 3);
446 let mut pool = GraphemePool::new();
447 let mut frame = Frame::new(12, 3, &mut pool);
448 block.render(area, &mut frame);
449
450 let buf = &frame.buffer;
451 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('S'));
452 }
453
454 #[test]
455 fn render_block_with_background() {
456 let block = Block::new().style(Style::new().bg(PackedRgba::rgb(10, 20, 30)));
457 let area = Rect::new(0, 0, 3, 2);
458 let mut pool = GraphemePool::new();
459 let mut frame = Frame::new(3, 2, &mut pool);
460 block.render(area, &mut frame);
461
462 let buf = &frame.buffer;
463 assert_eq!(buf.get(0, 0).unwrap().bg, PackedRgba::rgb(10, 20, 30));
464 assert_eq!(buf.get(2, 1).unwrap().bg, PackedRgba::rgb(10, 20, 30));
465 }
466
467 #[test]
468 fn inner_with_only_bottom() {
469 let block = Block::new().borders(Borders::BOTTOM);
470 let area = Rect::new(0, 0, 10, 10);
471 let inner = block.inner(area);
472 assert_eq!(inner, Rect::new(0, 0, 10, 9));
473 }
474
475 #[test]
476 fn inner_with_only_right() {
477 let block = Block::new().borders(Borders::RIGHT);
478 let area = Rect::new(0, 0, 10, 10);
479 let inner = block.inner(area);
480 assert_eq!(inner, Rect::new(0, 0, 9, 10));
481 }
482
483 #[test]
484 fn inner_saturates_on_tiny_area() {
485 let block = Block::new().borders(Borders::ALL);
486 let area = Rect::new(0, 0, 1, 1);
487 let inner = block.inner(area);
488 assert_eq!(inner.width, 0);
490 }
491
492 #[test]
493 fn bordered_constructor() {
494 let block = Block::bordered();
495 assert_eq!(block.borders, Borders::ALL);
496 }
497
498 #[test]
499 fn default_has_no_borders() {
500 let block = Block::new();
501 assert_eq!(block.borders, Borders::empty());
502 assert!(block.title.is_none());
503 }
504
505 #[test]
506 fn render_rounded_borders() {
507 let block = Block::new()
508 .borders(Borders::ALL)
509 .border_type(BorderType::Rounded);
510 let area = Rect::new(0, 0, 5, 3);
511 let mut pool = GraphemePool::new();
512 let mut frame = Frame::new(5, 3, &mut pool);
513 block.render(area, &mut frame);
514
515 let buf = &frame.buffer;
516 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('╭'));
517 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('╮'));
518 assert_eq!(buf.get(0, 2).unwrap().content.as_char(), Some('╰'));
519 assert_eq!(buf.get(4, 2).unwrap().content.as_char(), Some('╯'));
520 }
521
522 #[test]
523 fn render_double_borders() {
524 let block = Block::new()
525 .borders(Borders::ALL)
526 .border_type(BorderType::Double);
527 let area = Rect::new(0, 0, 5, 3);
528 let mut pool = GraphemePool::new();
529 let mut frame = Frame::new(5, 3, &mut pool);
530 block.render(area, &mut frame);
531
532 let buf = &frame.buffer;
533 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('╔'));
534 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('╗'));
535 }
536
537 #[test]
538 fn render_partial_borders_corners_only_when_edges_enabled() {
539 let block = Block::new()
540 .borders(Borders::TOP | Borders::LEFT | Borders::BOTTOM)
541 .border_type(BorderType::Square);
542 let area = Rect::new(0, 0, 4, 3);
543 let mut pool = GraphemePool::new();
544 let mut frame = Frame::new(4, 3, &mut pool);
545 block.render(area, &mut frame);
546
547 let buf = &frame.buffer;
548 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('┌'));
549 assert_eq!(buf.get(0, 2).unwrap().content.as_char(), Some('└'));
550 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('─'));
551 assert_eq!(buf.get(3, 2).unwrap().content.as_char(), Some('─'));
552 assert!(
553 buf.get(3, 1).unwrap().is_empty()
554 || buf.get(3, 1).unwrap().content.as_char() == Some(' ')
555 );
556 }
557
558 #[test]
559 fn render_vertical_only_borders_use_vertical_glyphs() {
560 let block = Block::new()
561 .borders(Borders::LEFT | Borders::RIGHT)
562 .border_type(BorderType::Double);
563 let area = Rect::new(0, 0, 4, 3);
564 let mut pool = GraphemePool::new();
565 let mut frame = Frame::new(4, 3, &mut pool);
566 block.render(area, &mut frame);
567
568 let buf = &frame.buffer;
569 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('║'));
570 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('║'));
571 assert!(
572 buf.get(1, 0).unwrap().is_empty()
573 || buf.get(1, 0).unwrap().content.as_char() == Some(' ')
574 );
575 }
576
577 #[test]
578 fn render_missing_left_keeps_horizontal_corner_logic() {
579 let block = Block::new()
580 .borders(Borders::TOP | Borders::RIGHT | Borders::BOTTOM)
581 .border_type(BorderType::Square);
582 let area = Rect::new(0, 0, 4, 3);
583 let mut pool = GraphemePool::new();
584 let mut frame = Frame::new(4, 3, &mut pool);
585 block.render(area, &mut frame);
586
587 let buf = &frame.buffer;
588 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('─'));
589 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('┐'));
590 assert_eq!(buf.get(0, 2).unwrap().content.as_char(), Some('─'));
591 assert_eq!(buf.get(3, 2).unwrap().content.as_char(), Some('┘'));
592 assert_eq!(buf.get(3, 1).unwrap().content.as_char(), Some('│'));
593 }
594
595 #[test]
596 fn render_title_left_aligned() {
597 let block = Block::new()
598 .borders(Borders::ALL)
599 .title("Test")
600 .title_alignment(Alignment::Left);
601 let area = Rect::new(0, 0, 10, 3);
602 let mut pool = GraphemePool::new();
603 let mut frame = Frame::new(10, 3, &mut pool);
604 block.render(area, &mut frame);
605
606 let buf = &frame.buffer;
607 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('T'));
608 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('e'));
609 }
610
611 #[test]
612 fn render_title_center_aligned() {
613 let block = Block::new()
614 .borders(Borders::ALL)
615 .title("Hi")
616 .title_alignment(Alignment::Center);
617 let area = Rect::new(0, 0, 10, 3);
618 let mut pool = GraphemePool::new();
619 let mut frame = Frame::new(10, 3, &mut pool);
620 block.render(area, &mut frame);
621
622 let buf = &frame.buffer;
624 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('H'));
625 assert_eq!(buf.get(5, 0).unwrap().content.as_char(), Some('i'));
626 }
627
628 #[test]
629 fn render_title_center_aligned_with_wide_grapheme() {
630 let block = Block::new()
631 .borders(Borders::ALL)
632 .title("界")
633 .title_alignment(Alignment::Center);
634 let area = Rect::new(0, 0, 8, 3);
635 let mut pool = GraphemePool::new();
636 let mut frame = Frame::new(8, 3, &mut pool);
637 block.render(area, &mut frame);
638
639 let buf = &frame.buffer;
641 let cell = buf.get(3, 0).unwrap();
642 assert!(
643 cell.content.as_char() == Some('界') || cell.content.is_grapheme(),
644 "expected title grapheme at x=3"
645 );
646 assert!(buf.get(4, 0).unwrap().is_continuation());
647 }
648
649 #[test]
650 fn render_title_right_aligned() {
651 let block = Block::new()
652 .borders(Borders::ALL)
653 .title("Hi")
654 .title_alignment(Alignment::Right);
655 let area = Rect::new(0, 0, 10, 3);
656 let mut pool = GraphemePool::new();
657 let mut frame = Frame::new(10, 3, &mut pool);
658 block.render(area, &mut frame);
659
660 let buf = &frame.buffer;
661 assert_eq!(buf.get(7, 0).unwrap().content.as_char(), Some('H'));
663 assert_eq!(buf.get(8, 0).unwrap().content.as_char(), Some('i'));
664 }
665
666 #[test]
667 fn render_multi_title_alignment_uses_last_title_and_alignment() {
668 let block = Block::new()
669 .borders(Borders::ALL)
670 .title("Left")
671 .title_alignment(Alignment::Left)
672 .title("Right")
673 .title_alignment(Alignment::Right);
674 let area = Rect::new(0, 0, 12, 3);
675 let mut pool = GraphemePool::new();
676 let mut frame = Frame::new(12, 3, &mut pool);
677 block.render(area, &mut frame);
678
679 let buf = &frame.buffer;
680 assert_eq!(buf.get(6, 0).unwrap().content.as_char(), Some('R'));
681 assert_ne!(buf.get(1, 0).unwrap().content.as_char(), Some('L'));
682 }
683
684 #[test]
685 fn title_not_rendered_without_top_border() {
686 let block = Block::new()
687 .borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM)
688 .title("Hi");
689 let area = Rect::new(0, 0, 10, 3);
690 let mut pool = GraphemePool::new();
691 let mut frame = Frame::new(10, 3, &mut pool);
692 block.render(area, &mut frame);
693
694 let buf = &frame.buffer;
695 assert_ne!(buf.get(1, 0).unwrap().content.as_char(), Some('H'));
697 }
698
699 #[test]
700 fn border_style_applied() {
701 let block = Block::new()
702 .borders(Borders::ALL)
703 .border_style(Style::new().fg(PackedRgba::rgb(255, 0, 0)));
704 let area = Rect::new(0, 0, 5, 3);
705 let mut pool = GraphemePool::new();
706 let mut frame = Frame::new(5, 3, &mut pool);
707 block.render(area, &mut frame);
708
709 let buf = &frame.buffer;
710 assert_eq!(buf.get(0, 0).unwrap().fg, PackedRgba::rgb(255, 0, 0));
711 }
712
713 #[test]
714 fn only_horizontal_borders() {
715 let block = Block::new()
716 .borders(Borders::TOP | Borders::BOTTOM)
717 .border_type(BorderType::Square);
718 let area = Rect::new(0, 0, 5, 3);
719 let mut pool = GraphemePool::new();
720 let mut frame = Frame::new(5, 3, &mut pool);
721 block.render(area, &mut frame);
722
723 let buf = &frame.buffer;
724 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('─'));
726 assert_eq!(buf.get(2, 2).unwrap().content.as_char(), Some('─'));
727 assert!(
729 buf.get(0, 1).unwrap().is_empty()
730 || buf.get(0, 1).unwrap().content.as_char() == Some(' ')
731 );
732 }
733
734 #[test]
735 fn degradation_simple_borders_forces_ascii() {
736 use ftui_render::budget::DegradationLevel;
737
738 let block = Block::new()
739 .borders(Borders::ALL)
740 .border_type(BorderType::Rounded);
741 let area = Rect::new(0, 0, 5, 3);
742 let mut pool = GraphemePool::new();
743 let mut frame = Frame::new(5, 3, &mut pool);
744 frame.set_degradation(DegradationLevel::SimpleBorders);
745 block.render(area, &mut frame);
746
747 let buf = &frame.buffer;
748 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('+'));
749 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('+'));
750 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('-'));
751 assert_eq!(buf.get(0, 1).unwrap().content.as_char(), Some('|'));
752 }
753
754 #[test]
755 fn degradation_simple_borders_partial_edges_use_ascii_corners() {
756 use ftui_render::budget::DegradationLevel;
757
758 let block = Block::new()
759 .borders(Borders::TOP | Borders::RIGHT | Borders::BOTTOM)
760 .border_type(BorderType::Double);
761 let area = Rect::new(0, 0, 4, 3);
762 let mut pool = GraphemePool::new();
763 let mut frame = Frame::new(4, 3, &mut pool);
764 frame.set_degradation(DegradationLevel::SimpleBorders);
765 block.render(area, &mut frame);
766
767 let buf = &frame.buffer;
768 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('-'));
769 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('+'));
770 assert_eq!(buf.get(0, 2).unwrap().content.as_char(), Some('-'));
771 assert_eq!(buf.get(3, 2).unwrap().content.as_char(), Some('+'));
772 assert_eq!(buf.get(3, 1).unwrap().content.as_char(), Some('|'));
773 }
774
775 #[test]
776 fn degradation_no_styling_renders_title_without_styles() {
777 use ftui_render::budget::DegradationLevel;
778
779 let block = Block::new()
780 .borders(Borders::ALL)
781 .border_style(Style::new().fg(PackedRgba::rgb(200, 0, 0)))
782 .title("Hi");
783 let area = Rect::new(0, 0, 6, 3);
784 let mut pool = GraphemePool::new();
785 let mut frame = Frame::new(6, 3, &mut pool);
786 frame.set_degradation(DegradationLevel::NoStyling);
787 block.render(area, &mut frame);
788
789 let buf = &frame.buffer;
790 let default_fg = Cell::default().fg;
791 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('H'));
792 assert_eq!(buf.get(1, 0).unwrap().fg, default_fg);
793 }
794
795 #[test]
796 fn degradation_essential_only_skips_borders() {
797 use ftui_render::budget::DegradationLevel;
798
799 let block = Block::bordered().border_type(BorderType::Square);
800 let area = Rect::new(0, 0, 4, 3);
801 let mut pool = GraphemePool::new();
802 let mut frame = Frame::new(4, 3, &mut pool);
803 frame.set_degradation(DegradationLevel::EssentialOnly);
804 frame.buffer.set(0, 0, Cell::from_char('X'));
805 block.render(area, &mut frame);
806
807 let buf = &frame.buffer;
808 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('X'));
809 }
810
811 #[test]
812 fn degradation_skeleton_clears_area() {
813 use ftui_render::budget::DegradationLevel;
814
815 let block = Block::bordered();
816 let area = Rect::new(0, 0, 3, 2);
817 let mut pool = GraphemePool::new();
818 let mut frame = Frame::new(3, 2, &mut pool);
819 frame.buffer.fill(area, Cell::from_char('X'));
820 frame.set_degradation(DegradationLevel::Skeleton);
821 block.render(area, &mut frame);
822
823 let buf = &frame.buffer;
824 assert!(buf.get(0, 0).unwrap().is_empty());
825 }
826
827 #[test]
828 fn block_equality() {
829 let a = Block::new().borders(Borders::ALL).title("Test");
830 let b = Block::new().borders(Borders::ALL).title("Test");
831 assert_eq!(a, b);
832 }
833
834 #[test]
835 fn render_1x1_no_panic() {
836 let block = Block::bordered();
837 let area = Rect::new(0, 0, 1, 1);
838 let mut pool = GraphemePool::new();
839 let mut frame = Frame::new(1, 1, &mut pool);
840 block.render(area, &mut frame);
841 }
842
843 #[test]
844 fn render_2x2_with_borders() {
845 let block = Block::bordered().border_type(BorderType::Square);
846 let area = Rect::new(0, 0, 2, 2);
847 let mut pool = GraphemePool::new();
848 let mut frame = Frame::new(2, 2, &mut pool);
849 block.render(area, &mut frame);
850
851 let buf = &frame.buffer;
852 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('┌'));
853 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('┐'));
854 assert_eq!(buf.get(0, 1).unwrap().content.as_char(), Some('└'));
855 assert_eq!(buf.get(1, 1).unwrap().content.as_char(), Some('┘'));
856 }
857
858 #[test]
859 fn title_too_narrow() {
860 let block = Block::bordered().title("LongTitle");
862 let area = Rect::new(0, 0, 4, 3);
863 let mut pool = GraphemePool::new();
864 let mut frame = Frame::new(4, 3, &mut pool);
865 block.render(area, &mut frame);
866 }
868
869 #[test]
870 fn alignment_default_is_left() {
871 assert_eq!(Alignment::default(), Alignment::Left);
872 }
873
874 use crate::MeasurableWidget;
877 use ftui_core::geometry::Size;
878
879 #[test]
880 fn chrome_size_no_borders() {
881 let block = Block::new();
882 assert_eq!(block.chrome_size(), (0, 0));
883 }
884
885 #[test]
886 fn chrome_size_all_borders() {
887 let block = Block::bordered();
888 assert_eq!(block.chrome_size(), (2, 2));
889 }
890
891 #[test]
892 fn chrome_size_partial_borders() {
893 let block = Block::new().borders(Borders::TOP | Borders::LEFT);
894 assert_eq!(block.chrome_size(), (1, 1));
895 }
896
897 #[test]
898 fn chrome_size_horizontal_only() {
899 let block = Block::new().borders(Borders::LEFT | Borders::RIGHT);
900 assert_eq!(block.chrome_size(), (2, 0));
901 }
902
903 #[test]
904 fn chrome_size_vertical_only() {
905 let block = Block::new().borders(Borders::TOP | Borders::BOTTOM);
906 assert_eq!(block.chrome_size(), (0, 2));
907 }
908
909 #[test]
910 fn measure_no_borders() {
911 let block = Block::new();
912 let constraints = block.measure(Size::MAX);
913 assert_eq!(constraints.min, Size::ZERO);
914 assert_eq!(constraints.preferred, Size::ZERO);
915 }
916
917 #[test]
918 fn measure_all_borders() {
919 let block = Block::bordered();
920 let constraints = block.measure(Size::MAX);
921 assert_eq!(constraints.min, Size::new(2, 2));
922 assert_eq!(constraints.preferred, Size::new(2, 2));
923 assert_eq!(constraints.max, None); }
925
926 #[test]
927 fn measure_partial_borders() {
928 let block = Block::new().borders(Borders::TOP | Borders::RIGHT);
929 let constraints = block.measure(Size::MAX);
930 assert_eq!(constraints.min, Size::new(1, 1));
931 assert_eq!(constraints.preferred, Size::new(1, 1));
932 }
933
934 #[test]
935 fn has_intrinsic_size_with_borders() {
936 let block = Block::bordered();
937 assert!(block.has_intrinsic_size());
938 }
939
940 #[test]
941 fn has_no_intrinsic_size_without_borders() {
942 let block = Block::new();
943 assert!(!block.has_intrinsic_size());
944 }
945
946 #[test]
947 fn measure_is_pure() {
948 let block = Block::bordered();
949 let a = block.measure(Size::new(100, 50));
950 let b = block.measure(Size::new(100, 50));
951 assert_eq!(a, b);
952 }
953}