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_text(&self) -> Option<&str> {
94 self.title
95 }
96
97 #[must_use]
99 pub fn title_alignment(mut self, alignment: Alignment) -> Self {
100 self.title_alignment = alignment;
101 self
102 }
103
104 #[must_use]
106 pub fn style(mut self, style: Style) -> Self {
107 self.style = style;
108 self
109 }
110
111 #[must_use]
113 pub fn inner(&self, area: Rect) -> Rect {
114 let mut inner = area;
115
116 if self.borders.contains(Borders::LEFT) {
117 inner.x = inner.x.saturating_add(1);
118 inner.width = inner.width.saturating_sub(1);
119 }
120 if self.borders.contains(Borders::TOP) {
121 inner.y = inner.y.saturating_add(1);
122 inner.height = inner.height.saturating_sub(1);
123 }
124 if self.borders.contains(Borders::RIGHT) {
125 inner.width = inner.width.saturating_sub(1);
126 }
127 if self.borders.contains(Borders::BOTTOM) {
128 inner.height = inner.height.saturating_sub(1);
129 }
130
131 inner.inner(self.padding)
132 }
133
134 #[must_use]
139 pub fn chrome_size(&self) -> (u16, u16) {
140 let border_h = self.borders.contains(Borders::LEFT) as u16
141 + self.borders.contains(Borders::RIGHT) as u16;
142 let border_v = self.borders.contains(Borders::TOP) as u16
143 + self.borders.contains(Borders::BOTTOM) as u16;
144
145 let padding_h = self.padding.left + self.padding.right;
146 let padding_v = self.padding.top + self.padding.bottom;
147
148 (
149 border_h.saturating_add(padding_h),
150 border_v.saturating_add(padding_v),
151 )
152 }
153
154 fn border_cell(&self, c: char, style: Style) -> Cell {
156 let mut cell = Cell::from_char(c);
157 apply_style(&mut cell, style);
158 cell
159 }
160
161 fn render_borders(&self, area: Rect, buf: &mut Buffer, style: Style) {
162 if area.is_empty() {
163 return;
164 }
165
166 let set = self.border_set();
167
168 if self.borders.contains(Borders::LEFT) {
170 for y in area.y..area.bottom() {
171 buf.set_fast(area.x, y, self.border_cell(set.vertical, style));
172 }
173 }
174 if self.borders.contains(Borders::RIGHT) {
175 let x = area.right() - 1;
176 for y in area.y..area.bottom() {
177 buf.set_fast(x, y, self.border_cell(set.vertical, style));
178 }
179 }
180 if self.borders.contains(Borders::TOP) {
181 for x in area.x..area.right() {
182 buf.set_fast(x, area.y, self.border_cell(set.horizontal, style));
183 }
184 }
185 if self.borders.contains(Borders::BOTTOM) {
186 let y = area.bottom() - 1;
187 for x in area.x..area.right() {
188 buf.set_fast(x, y, self.border_cell(set.horizontal, style));
189 }
190 }
191
192 if self.borders.contains(Borders::LEFT | Borders::TOP) {
194 buf.set_fast(area.x, area.y, self.border_cell(set.top_left, style));
195 }
196 if self.borders.contains(Borders::RIGHT | Borders::TOP) {
197 buf.set_fast(
198 area.right() - 1,
199 area.y,
200 self.border_cell(set.top_right, style),
201 );
202 }
203 if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
204 buf.set_fast(
205 area.x,
206 area.bottom() - 1,
207 self.border_cell(set.bottom_left, style),
208 );
209 }
210 if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
211 buf.set_fast(
212 area.right() - 1,
213 area.bottom() - 1,
214 self.border_cell(set.bottom_right, style),
215 );
216 }
217 }
218
219 fn render_borders_ascii(&self, area: Rect, buf: &mut Buffer, style: Style) {
221 if area.is_empty() {
222 return;
223 }
224
225 let set = crate::borders::BorderSet::ASCII;
226
227 if self.borders.contains(Borders::LEFT) {
228 for y in area.y..area.bottom() {
229 buf.set_fast(area.x, y, self.border_cell(set.vertical, style));
230 }
231 }
232 if self.borders.contains(Borders::RIGHT) {
233 let x = area.right() - 1;
234 for y in area.y..area.bottom() {
235 buf.set_fast(x, y, self.border_cell(set.vertical, style));
236 }
237 }
238 if self.borders.contains(Borders::TOP) {
239 for x in area.x..area.right() {
240 buf.set_fast(x, area.y, self.border_cell(set.horizontal, style));
241 }
242 }
243 if self.borders.contains(Borders::BOTTOM) {
244 let y = area.bottom() - 1;
245 for x in area.x..area.right() {
246 buf.set_fast(x, y, self.border_cell(set.horizontal, style));
247 }
248 }
249
250 if self.borders.contains(Borders::LEFT | Borders::TOP) {
251 buf.set_fast(area.x, area.y, self.border_cell(set.top_left, style));
252 }
253 if self.borders.contains(Borders::RIGHT | Borders::TOP) {
254 buf.set_fast(
255 area.right() - 1,
256 area.y,
257 self.border_cell(set.top_right, style),
258 );
259 }
260 if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
261 buf.set_fast(
262 area.x,
263 area.bottom() - 1,
264 self.border_cell(set.bottom_left, style),
265 );
266 }
267 if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
268 buf.set_fast(
269 area.right() - 1,
270 area.bottom() - 1,
271 self.border_cell(set.bottom_right, style),
272 );
273 }
274 }
275
276 fn render_title(&self, area: Rect, frame: &mut Frame) {
277 if let Some(title) = self.title {
278 if !self.borders.contains(Borders::TOP) || area.width < 3 {
279 return;
280 }
281
282 let available_width = area.width.saturating_sub(2) as usize;
283 if available_width == 0 {
284 return;
285 }
286
287 let display_width = fitted_text_width(title, available_width);
288 if display_width == 0 {
289 return;
290 }
291
292 let x = match self.title_alignment {
293 Alignment::Left => area.x.saturating_add(1),
294 Alignment::Center => area
295 .x
296 .saturating_add(1)
297 .saturating_add(((available_width.saturating_sub(display_width)) / 2) as u16),
298 Alignment::Right => area
299 .right()
300 .saturating_sub(1)
301 .saturating_sub(display_width as u16),
302 };
303
304 let max_x = area.right().saturating_sub(1);
305 draw_text_span(frame, x, area.y, title, self.border_style, max_x);
306 }
307 }
308}
309
310impl Widget for Block<'_> {
311 fn render(&self, area: Rect, frame: &mut Frame) {
312 #[cfg(feature = "tracing")]
313 let _span = tracing::debug_span!(
314 "widget_render",
315 widget = "Block",
316 x = area.x,
317 y = area.y,
318 w = area.width,
319 h = area.height
320 )
321 .entered();
322
323 if area.is_empty() {
324 return;
325 }
326
327 let deg = frame.degradation;
328 let border_style = if deg.apply_styling() {
329 self.border_style
330 } else {
331 Style::default()
332 };
333
334 if !deg.render_content() {
336 frame.buffer.fill(area, Cell::default());
337 return;
338 }
339
340 if !deg.render_decorative() {
343 frame.buffer.fill(area, Cell::default());
344 if deg.apply_styling() {
345 set_style_area(&mut frame.buffer, area, self.style);
346 }
347 return;
348 }
349
350 if deg.apply_styling() {
352 set_style_area(&mut frame.buffer, area, self.style);
353 }
354
355 if deg.use_unicode_borders() {
357 self.render_borders(area, &mut frame.buffer, border_style);
358 } else {
359 self.render_borders_ascii(area, &mut frame.buffer, border_style);
361 }
362
363 if deg.apply_styling() {
365 self.render_title(area, frame);
366 } else if deg.render_decorative() {
367 if let Some(title) = self.title
370 && self.borders.contains(Borders::TOP)
371 && area.width >= 3
372 {
373 let available_width = area.width.saturating_sub(2) as usize;
374 if available_width > 0 {
375 let display_width = fitted_text_width(title, available_width);
376 if display_width == 0 {
377 return;
378 }
379 let x = match self.title_alignment {
380 Alignment::Left => area.x.saturating_add(1),
381 Alignment::Center => area.x.saturating_add(1).saturating_add(
382 ((available_width.saturating_sub(display_width)) / 2) as u16,
383 ),
384 Alignment::Right => area
385 .right()
386 .saturating_sub(1)
387 .saturating_sub(display_width as u16),
388 };
389 let max_x = area.right().saturating_sub(1);
390 frame.buffer.fill(
393 Rect::new(x, area.y, display_width as u16, 1),
394 Cell::default(),
395 );
396 draw_text_span(frame, x, area.y, title, Style::default(), max_x);
397 }
398 }
399 }
400 }
401}
402
403impl MeasurableWidget for Block<'_> {
404 fn measure(&self, _available: Size) -> SizeConstraints {
405 let (chrome_width, chrome_height) = self.chrome_size();
406 let chrome = Size::new(chrome_width, chrome_height);
407
408 SizeConstraints::at_least(chrome, chrome)
413 }
414
415 fn has_intrinsic_size(&self) -> bool {
416 self.borders != Borders::empty()
418 }
419}
420
421fn fitted_text_width(text: &str, max_width: usize) -> usize {
422 let mut width = 0usize;
423 for grapheme in graphemes(text) {
424 let w = grapheme_width(grapheme);
425 if w == 0 {
426 continue;
427 }
428 if width.saturating_add(w) > max_width {
429 break;
430 }
431 width += w;
432 }
433 width
434}
435
436impl ftui_a11y::Accessible for Block<'_> {
441 fn accessibility_nodes(&self, area: Rect) -> Vec<ftui_a11y::node::A11yNodeInfo> {
442 use ftui_a11y::node::{A11yNodeInfo, A11yRole};
443
444 let id = crate::a11y_node_id(area);
445 let mut node = A11yNodeInfo::new(id, A11yRole::Group, area);
446 if let Some(title) = self.title_text() {
447 node = node.with_name(title);
448 }
449 vec![node]
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456 use ftui_render::cell::PackedRgba;
457 use ftui_render::grapheme_pool::GraphemePool;
458
459 #[test]
460 fn inner_with_all_borders() {
461 let block = Block::new().borders(Borders::ALL);
462 let area = Rect::new(0, 0, 10, 10);
463 let inner = block.inner(area);
464 assert_eq!(inner, Rect::new(1, 1, 8, 8));
465 }
466
467 #[test]
468 fn inner_with_no_borders() {
469 let block = Block::new();
470 let area = Rect::new(0, 0, 10, 10);
471 let inner = block.inner(area);
472 assert_eq!(inner, area);
473 }
474
475 #[test]
476 fn inner_with_partial_borders() {
477 let block = Block::new().borders(Borders::TOP | Borders::LEFT);
478 let area = Rect::new(0, 0, 10, 10);
479 let inner = block.inner(area);
480 assert_eq!(inner, Rect::new(1, 1, 9, 9));
481 }
482
483 #[test]
484 fn render_empty_area() {
485 let block = Block::new().borders(Borders::ALL);
486 let area = Rect::new(0, 0, 0, 0);
487 let mut pool = GraphemePool::new();
488 let mut frame = Frame::new(1, 1, &mut pool);
489 block.render(area, &mut frame);
490 }
491
492 #[test]
493 fn render_block_with_square_borders() {
494 let block = Block::new()
495 .borders(Borders::ALL)
496 .border_type(BorderType::Square);
497 let area = Rect::new(0, 0, 5, 3);
498 let mut pool = GraphemePool::new();
499 let mut frame = Frame::new(5, 3, &mut pool);
500 block.render(area, &mut frame);
501
502 let buf = &frame.buffer;
503 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('┌'));
504 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('┐'));
505 assert_eq!(buf.get(0, 2).unwrap().content.as_char(), Some('└'));
506 assert_eq!(buf.get(4, 2).unwrap().content.as_char(), Some('┘'));
507 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('─'));
508 assert_eq!(buf.get(0, 1).unwrap().content.as_char(), Some('│'));
509 }
510
511 #[test]
512 fn render_block_with_title() {
513 let block = Block::new()
514 .borders(Borders::ALL)
515 .border_type(BorderType::Square)
516 .title("Hi");
517 let area = Rect::new(0, 0, 10, 3);
518 let mut pool = GraphemePool::new();
519 let mut frame = Frame::new(10, 3, &mut pool);
520 block.render(area, &mut frame);
521
522 let buf = &frame.buffer;
523 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('H'));
524 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('i'));
525 }
526
527 #[test]
528 fn render_title_overrides_on_multiple_calls() {
529 let block = Block::new()
530 .borders(Borders::ALL)
531 .border_type(BorderType::Square)
532 .title("First")
533 .title("Second");
534 let area = Rect::new(0, 0, 12, 3);
535 let mut pool = GraphemePool::new();
536 let mut frame = Frame::new(12, 3, &mut pool);
537 block.render(area, &mut frame);
538
539 let buf = &frame.buffer;
540 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('S'));
541 }
542
543 #[test]
544 fn render_block_with_background() {
545 let block = Block::new().style(Style::new().bg(PackedRgba::rgb(10, 20, 30)));
546 let area = Rect::new(0, 0, 3, 2);
547 let mut pool = GraphemePool::new();
548 let mut frame = Frame::new(3, 2, &mut pool);
549 block.render(area, &mut frame);
550
551 let buf = &frame.buffer;
552 assert_eq!(buf.get(0, 0).unwrap().bg, PackedRgba::rgb(10, 20, 30));
553 assert_eq!(buf.get(2, 1).unwrap().bg, PackedRgba::rgb(10, 20, 30));
554 }
555
556 #[test]
557 fn inner_with_only_bottom() {
558 let block = Block::new().borders(Borders::BOTTOM);
559 let area = Rect::new(0, 0, 10, 10);
560 let inner = block.inner(area);
561 assert_eq!(inner, Rect::new(0, 0, 10, 9));
562 }
563
564 #[test]
565 fn inner_with_only_right() {
566 let block = Block::new().borders(Borders::RIGHT);
567 let area = Rect::new(0, 0, 10, 10);
568 let inner = block.inner(area);
569 assert_eq!(inner, Rect::new(0, 0, 9, 10));
570 }
571
572 #[test]
573 fn inner_saturates_on_tiny_area() {
574 let block = Block::new().borders(Borders::ALL);
575 let area = Rect::new(0, 0, 1, 1);
576 let inner = block.inner(area);
577 assert_eq!(inner.width, 0);
579 }
580
581 #[test]
582 fn bordered_constructor() {
583 let block = Block::bordered();
584 assert_eq!(block.borders, Borders::ALL);
585 }
586
587 #[test]
588 fn default_has_no_borders() {
589 let block = Block::new();
590 assert_eq!(block.borders, Borders::empty());
591 assert!(block.title.is_none());
592 }
593
594 #[test]
595 fn render_rounded_borders() {
596 let block = Block::new()
597 .borders(Borders::ALL)
598 .border_type(BorderType::Rounded);
599 let area = Rect::new(0, 0, 5, 3);
600 let mut pool = GraphemePool::new();
601 let mut frame = Frame::new(5, 3, &mut pool);
602 block.render(area, &mut frame);
603
604 let buf = &frame.buffer;
605 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('╭'));
606 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('╮'));
607 assert_eq!(buf.get(0, 2).unwrap().content.as_char(), Some('╰'));
608 assert_eq!(buf.get(4, 2).unwrap().content.as_char(), Some('╯'));
609 }
610
611 #[test]
612 fn render_double_borders() {
613 let block = Block::new()
614 .borders(Borders::ALL)
615 .border_type(BorderType::Double);
616 let area = Rect::new(0, 0, 5, 3);
617 let mut pool = GraphemePool::new();
618 let mut frame = Frame::new(5, 3, &mut pool);
619 block.render(area, &mut frame);
620
621 let buf = &frame.buffer;
622 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('╔'));
623 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('╗'));
624 }
625
626 #[test]
627 fn render_partial_borders_corners_only_when_edges_enabled() {
628 let block = Block::new()
629 .borders(Borders::TOP | Borders::LEFT | Borders::BOTTOM)
630 .border_type(BorderType::Square);
631 let area = Rect::new(0, 0, 4, 3);
632 let mut pool = GraphemePool::new();
633 let mut frame = Frame::new(4, 3, &mut pool);
634 block.render(area, &mut frame);
635
636 let buf = &frame.buffer;
637 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('┌'));
638 assert_eq!(buf.get(0, 2).unwrap().content.as_char(), Some('└'));
639 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('─'));
640 assert_eq!(buf.get(3, 2).unwrap().content.as_char(), Some('─'));
641 assert!(
642 buf.get(3, 1).unwrap().is_empty()
643 || buf.get(3, 1).unwrap().content.as_char() == Some(' ')
644 );
645 }
646
647 #[test]
648 fn render_vertical_only_borders_use_vertical_glyphs() {
649 let block = Block::new()
650 .borders(Borders::LEFT | Borders::RIGHT)
651 .border_type(BorderType::Double);
652 let area = Rect::new(0, 0, 4, 3);
653 let mut pool = GraphemePool::new();
654 let mut frame = Frame::new(4, 3, &mut pool);
655 block.render(area, &mut frame);
656
657 let buf = &frame.buffer;
658 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('║'));
659 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('║'));
660 assert!(
661 buf.get(1, 0).unwrap().is_empty()
662 || buf.get(1, 0).unwrap().content.as_char() == Some(' ')
663 );
664 }
665
666 #[test]
667 fn render_missing_left_keeps_horizontal_corner_logic() {
668 let block = Block::new()
669 .borders(Borders::TOP | Borders::RIGHT | Borders::BOTTOM)
670 .border_type(BorderType::Square);
671 let area = Rect::new(0, 0, 4, 3);
672 let mut pool = GraphemePool::new();
673 let mut frame = Frame::new(4, 3, &mut pool);
674 block.render(area, &mut frame);
675
676 let buf = &frame.buffer;
677 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('─'));
678 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('┐'));
679 assert_eq!(buf.get(0, 2).unwrap().content.as_char(), Some('─'));
680 assert_eq!(buf.get(3, 2).unwrap().content.as_char(), Some('┘'));
681 assert_eq!(buf.get(3, 1).unwrap().content.as_char(), Some('│'));
682 }
683
684 #[test]
685 fn render_title_left_aligned() {
686 let block = Block::new()
687 .borders(Borders::ALL)
688 .title("Test")
689 .title_alignment(Alignment::Left);
690 let area = Rect::new(0, 0, 10, 3);
691 let mut pool = GraphemePool::new();
692 let mut frame = Frame::new(10, 3, &mut pool);
693 block.render(area, &mut frame);
694
695 let buf = &frame.buffer;
696 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('T'));
697 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('e'));
698 }
699
700 #[test]
701 fn render_title_center_aligned() {
702 let block = Block::new()
703 .borders(Borders::ALL)
704 .title("Hi")
705 .title_alignment(Alignment::Center);
706 let area = Rect::new(0, 0, 10, 3);
707 let mut pool = GraphemePool::new();
708 let mut frame = Frame::new(10, 3, &mut pool);
709 block.render(area, &mut frame);
710
711 let buf = &frame.buffer;
713 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('H'));
714 assert_eq!(buf.get(5, 0).unwrap().content.as_char(), Some('i'));
715 }
716
717 #[test]
718 fn render_title_center_aligned_with_wide_grapheme() {
719 let block = Block::new()
720 .borders(Borders::ALL)
721 .title("界")
722 .title_alignment(Alignment::Center);
723 let area = Rect::new(0, 0, 8, 3);
724 let mut pool = GraphemePool::new();
725 let mut frame = Frame::new(8, 3, &mut pool);
726 block.render(area, &mut frame);
727
728 let buf = &frame.buffer;
730 let cell = buf.get(3, 0).unwrap();
731 assert!(
732 cell.content.as_char() == Some('界') || cell.content.is_grapheme(),
733 "expected title grapheme at x=3"
734 );
735 assert!(buf.get(4, 0).unwrap().is_continuation());
736 }
737
738 #[test]
739 fn render_title_right_aligned() {
740 let block = Block::new()
741 .borders(Borders::ALL)
742 .title("Hi")
743 .title_alignment(Alignment::Right);
744 let area = Rect::new(0, 0, 10, 3);
745 let mut pool = GraphemePool::new();
746 let mut frame = Frame::new(10, 3, &mut pool);
747 block.render(area, &mut frame);
748
749 let buf = &frame.buffer;
750 assert_eq!(buf.get(7, 0).unwrap().content.as_char(), Some('H'));
752 assert_eq!(buf.get(8, 0).unwrap().content.as_char(), Some('i'));
753 }
754
755 #[test]
756 fn render_title_right_aligned_truncated_wide_grapheme_uses_fitted_width() {
757 let block = Block::new()
758 .borders(Borders::ALL)
759 .title("界界")
760 .title_alignment(Alignment::Right);
761 let area = Rect::new(0, 0, 5, 3);
762 let mut pool = GraphemePool::new();
763 let mut frame = Frame::new(5, 3, &mut pool);
764 block.render(area, &mut frame);
765
766 let buf = &frame.buffer;
767 let cell = buf.get(2, 0).unwrap();
768 assert!(
769 cell.content.as_char() == Some('界') || cell.content.is_grapheme(),
770 "expected fitted wide title to be right aligned"
771 );
772 assert!(buf.get(3, 0).unwrap().is_continuation());
773 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('─'));
774 }
775
776 #[test]
777 fn render_multi_title_alignment_uses_last_title_and_alignment() {
778 let block = Block::new()
779 .borders(Borders::ALL)
780 .title("Left")
781 .title_alignment(Alignment::Left)
782 .title("Right")
783 .title_alignment(Alignment::Right);
784 let area = Rect::new(0, 0, 12, 3);
785 let mut pool = GraphemePool::new();
786 let mut frame = Frame::new(12, 3, &mut pool);
787 block.render(area, &mut frame);
788
789 let buf = &frame.buffer;
790 assert_eq!(buf.get(6, 0).unwrap().content.as_char(), Some('R'));
791 assert_ne!(buf.get(1, 0).unwrap().content.as_char(), Some('L'));
792 }
793
794 #[test]
795 fn title_not_rendered_without_top_border() {
796 let block = Block::new()
797 .borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM)
798 .title("Hi");
799 let area = Rect::new(0, 0, 10, 3);
800 let mut pool = GraphemePool::new();
801 let mut frame = Frame::new(10, 3, &mut pool);
802 block.render(area, &mut frame);
803
804 let buf = &frame.buffer;
805 assert_ne!(buf.get(1, 0).unwrap().content.as_char(), Some('H'));
807 }
808
809 #[test]
810 fn border_style_applied() {
811 let block = Block::new()
812 .borders(Borders::ALL)
813 .border_style(Style::new().fg(PackedRgba::rgb(255, 0, 0)));
814 let area = Rect::new(0, 0, 5, 3);
815 let mut pool = GraphemePool::new();
816 let mut frame = Frame::new(5, 3, &mut pool);
817 block.render(area, &mut frame);
818
819 let buf = &frame.buffer;
820 assert_eq!(buf.get(0, 0).unwrap().fg, PackedRgba::rgb(255, 0, 0));
821 }
822
823 #[test]
824 fn only_horizontal_borders() {
825 let block = Block::new()
826 .borders(Borders::TOP | Borders::BOTTOM)
827 .border_type(BorderType::Square);
828 let area = Rect::new(0, 0, 5, 3);
829 let mut pool = GraphemePool::new();
830 let mut frame = Frame::new(5, 3, &mut pool);
831 block.render(area, &mut frame);
832
833 let buf = &frame.buffer;
834 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('─'));
836 assert_eq!(buf.get(2, 2).unwrap().content.as_char(), Some('─'));
837 assert!(
839 buf.get(0, 1).unwrap().is_empty()
840 || buf.get(0, 1).unwrap().content.as_char() == Some(' ')
841 );
842 }
843
844 #[test]
845 fn degradation_simple_borders_forces_ascii() {
846 use ftui_render::budget::DegradationLevel;
847
848 let block = Block::new()
849 .borders(Borders::ALL)
850 .border_type(BorderType::Rounded);
851 let area = Rect::new(0, 0, 5, 3);
852 let mut pool = GraphemePool::new();
853 let mut frame = Frame::new(5, 3, &mut pool);
854 frame.set_degradation(DegradationLevel::SimpleBorders);
855 block.render(area, &mut frame);
856
857 let buf = &frame.buffer;
858 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('+'));
859 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('+'));
860 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('-'));
861 assert_eq!(buf.get(0, 1).unwrap().content.as_char(), Some('|'));
862 }
863
864 #[test]
865 fn degradation_simple_borders_partial_edges_use_ascii_corners() {
866 use ftui_render::budget::DegradationLevel;
867
868 let block = Block::new()
869 .borders(Borders::TOP | Borders::RIGHT | Borders::BOTTOM)
870 .border_type(BorderType::Double);
871 let area = Rect::new(0, 0, 4, 3);
872 let mut pool = GraphemePool::new();
873 let mut frame = Frame::new(4, 3, &mut pool);
874 frame.set_degradation(DegradationLevel::SimpleBorders);
875 block.render(area, &mut frame);
876
877 let buf = &frame.buffer;
878 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('-'));
879 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('+'));
880 assert_eq!(buf.get(0, 2).unwrap().content.as_char(), Some('-'));
881 assert_eq!(buf.get(3, 2).unwrap().content.as_char(), Some('+'));
882 assert_eq!(buf.get(3, 1).unwrap().content.as_char(), Some('|'));
883 }
884
885 #[test]
886 fn degradation_no_styling_renders_title_without_styles() {
887 use ftui_render::budget::DegradationLevel;
888
889 let block = Block::new()
890 .borders(Borders::ALL)
891 .border_style(Style::new().fg(PackedRgba::rgb(200, 0, 0)))
892 .title("Hi");
893 let area = Rect::new(0, 0, 6, 3);
894 let mut pool = GraphemePool::new();
895 let mut frame = Frame::new(6, 3, &mut pool);
896 frame.set_degradation(DegradationLevel::NoStyling);
897 block.render(area, &mut frame);
898
899 let buf = &frame.buffer;
900 let default_fg = Cell::default().fg;
901 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('H'));
902 assert_eq!(buf.get(1, 0).unwrap().fg, default_fg);
903 }
904
905 #[test]
906 fn degradation_no_styling_keeps_border_when_title_does_not_fit() {
907 use ftui_render::budget::DegradationLevel;
908
909 let titled = Block::new()
910 .borders(Borders::ALL)
911 .border_type(BorderType::Square)
912 .title("界");
913 let plain = Block::new()
914 .borders(Borders::ALL)
915 .border_type(BorderType::Square);
916 let area = Rect::new(0, 0, 3, 3);
917
918 let mut titled_pool = GraphemePool::new();
919 let mut titled_frame = Frame::new(3, 3, &mut titled_pool);
920 titled_frame.set_degradation(DegradationLevel::NoStyling);
921 titled.render(area, &mut titled_frame);
922
923 let mut plain_pool = GraphemePool::new();
924 let mut plain_frame = Frame::new(3, 3, &mut plain_pool);
925 plain_frame.set_degradation(DegradationLevel::NoStyling);
926 plain.render(area, &mut plain_frame);
927
928 assert_eq!(titled_frame.buffer.get(1, 0), plain_frame.buffer.get(1, 0));
929 }
930
931 #[test]
932 fn degradation_no_styling_drops_border_style_everywhere() {
933 use ftui_render::budget::DegradationLevel;
934
935 let block = Block::new()
936 .borders(Borders::ALL)
937 .border_style(Style::new().fg(PackedRgba::rgb(200, 0, 0)).bold());
938 let area = Rect::new(0, 0, 5, 3);
939 let mut pool = GraphemePool::new();
940 let mut frame = Frame::new(5, 3, &mut pool);
941 frame.set_degradation(DegradationLevel::NoStyling);
942 block.render(area, &mut frame);
943
944 let border = frame.buffer.get(0, 0).unwrap();
945 let default_cell = Cell::from_char(border.content.as_char().unwrap());
946 assert_eq!(border.fg, default_cell.fg);
947 assert_eq!(border.bg, default_cell.bg);
948 assert_eq!(border.attrs, default_cell.attrs);
949 }
950
951 #[test]
952 fn degradation_essential_only_clears_stale_borders_and_title() {
953 use ftui_render::budget::DegradationLevel;
954
955 let block = Block::bordered()
956 .border_type(BorderType::Square)
957 .title("Hi");
958 let area = Rect::new(0, 0, 6, 3);
959 let mut pool = GraphemePool::new();
960 let mut frame = Frame::new(6, 3, &mut pool);
961 block.render(area, &mut frame);
962
963 frame.set_degradation(DegradationLevel::EssentialOnly);
964 block.render(area, &mut frame);
965
966 let buf = &frame.buffer;
967 for y in 0..area.height {
968 for x in 0..area.width {
969 assert!(
970 buf.get(x, y).unwrap().is_empty(),
971 "expected cleared cell at ({x}, {y}), got {:?}",
972 buf.get(x, y).unwrap()
973 );
974 }
975 }
976 }
977
978 #[test]
979 fn degradation_skeleton_clears_area() {
980 use ftui_render::budget::DegradationLevel;
981
982 let block = Block::bordered();
983 let area = Rect::new(0, 0, 3, 2);
984 let mut pool = GraphemePool::new();
985 let mut frame = Frame::new(3, 2, &mut pool);
986 frame.buffer.fill(area, Cell::from_char('X'));
987 frame.set_degradation(DegradationLevel::Skeleton);
988 block.render(area, &mut frame);
989
990 let buf = &frame.buffer;
991 assert!(buf.get(0, 0).unwrap().is_empty());
992 }
993
994 #[test]
995 fn block_equality() {
996 let a = Block::new().borders(Borders::ALL).title("Test");
997 let b = Block::new().borders(Borders::ALL).title("Test");
998 assert_eq!(a, b);
999 }
1000
1001 #[test]
1002 fn render_1x1_no_panic() {
1003 let block = Block::bordered();
1004 let area = Rect::new(0, 0, 1, 1);
1005 let mut pool = GraphemePool::new();
1006 let mut frame = Frame::new(1, 1, &mut pool);
1007 block.render(area, &mut frame);
1008 }
1009
1010 #[test]
1011 fn render_2x2_with_borders() {
1012 let block = Block::bordered().border_type(BorderType::Square);
1013 let area = Rect::new(0, 0, 2, 2);
1014 let mut pool = GraphemePool::new();
1015 let mut frame = Frame::new(2, 2, &mut pool);
1016 block.render(area, &mut frame);
1017
1018 let buf = &frame.buffer;
1019 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('┌'));
1020 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('┐'));
1021 assert_eq!(buf.get(0, 1).unwrap().content.as_char(), Some('└'));
1022 assert_eq!(buf.get(1, 1).unwrap().content.as_char(), Some('┘'));
1023 }
1024
1025 #[test]
1026 fn title_too_narrow() {
1027 let block = Block::bordered().title("LongTitle");
1029 let area = Rect::new(0, 0, 4, 3);
1030 let mut pool = GraphemePool::new();
1031 let mut frame = Frame::new(4, 3, &mut pool);
1032 block.render(area, &mut frame);
1033 }
1035
1036 #[test]
1037 fn alignment_default_is_left() {
1038 assert_eq!(Alignment::default(), Alignment::Left);
1039 }
1040
1041 use crate::MeasurableWidget;
1044 use ftui_core::geometry::Size;
1045
1046 #[test]
1047 fn chrome_size_no_borders() {
1048 let block = Block::new();
1049 assert_eq!(block.chrome_size(), (0, 0));
1050 }
1051
1052 #[test]
1053 fn chrome_size_all_borders() {
1054 let block = Block::bordered();
1055 assert_eq!(block.chrome_size(), (4, 4));
1058 }
1059
1060 #[test]
1061 fn chrome_size_partial_borders() {
1062 let block = Block::new().borders(Borders::TOP | Borders::LEFT);
1063 assert_eq!(block.chrome_size(), (1, 1));
1064 }
1065
1066 #[test]
1067 fn chrome_size_horizontal_only() {
1068 let block = Block::new().borders(Borders::LEFT | Borders::RIGHT);
1069 assert_eq!(block.chrome_size(), (2, 0));
1070 }
1071
1072 #[test]
1073 fn chrome_size_vertical_only() {
1074 let block = Block::new().borders(Borders::TOP | Borders::BOTTOM);
1075 assert_eq!(block.chrome_size(), (0, 2));
1076 }
1077
1078 #[test]
1079 fn measure_no_borders() {
1080 let block = Block::new();
1081 let constraints = block.measure(Size::MAX);
1082 assert_eq!(constraints.min, Size::ZERO);
1083 assert_eq!(constraints.preferred, Size::ZERO);
1084 }
1085
1086 #[test]
1087 fn measure_all_borders() {
1088 let block = Block::bordered();
1089 let constraints = block.measure(Size::MAX);
1090 assert_eq!(constraints.min, Size::new(4, 4));
1091 assert_eq!(constraints.preferred, Size::new(4, 4));
1092 assert_eq!(constraints.max, None); }
1094
1095 #[test]
1096 fn measure_partial_borders() {
1097 let block = Block::new().borders(Borders::TOP | Borders::RIGHT);
1098 let constraints = block.measure(Size::MAX);
1099 assert_eq!(constraints.min, Size::new(1, 1));
1100 assert_eq!(constraints.preferred, Size::new(1, 1));
1101 }
1102
1103 #[test]
1104 fn has_intrinsic_size_with_borders() {
1105 let block = Block::bordered();
1106 assert!(block.has_intrinsic_size());
1107 }
1108
1109 #[test]
1110 fn has_no_intrinsic_size_without_borders() {
1111 let block = Block::new();
1112 assert!(!block.has_intrinsic_size());
1113 }
1114
1115 #[test]
1116 fn measure_is_pure() {
1117 let block = Block::bordered();
1118 let a = block.measure(Size::new(100, 50));
1119 let b = block.measure(Size::new(100, 50));
1120 assert_eq!(a, b);
1121 }
1122}