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