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