From 220205925911ed4377358d2a28ffca9373f11bda Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Wed, 7 Feb 2024 15:24:14 -0800 Subject: [PATCH] fix(block): fix crash on empty right aligned title (#933) - Simplified implementation of the rendering for block. - Introduces a subtle rendering change where centered titles that are odd in length will now be rendered one character to the left compared to before. This aligns with other places that we render centered text and is a more consistent behavior. See https://github.com/ratatui-org/ratatui/pull/807#discussion_r1455645954 for another example of this. Fixes: https://github.com/ratatui-org/ratatui/pull/929 --- src/widgets/block.rs | 410 +++++++++++++++++++-------------- tests/widgets_block.rs | 504 ++++++++++++++++++++++------------------- 2 files changed, 513 insertions(+), 401 deletions(-) diff --git a/src/widgets/block.rs b/src/widgets/block.rs index 91d3162d..8ccd1c85 100644 --- a/src/widgets/block.rs +++ b/src/widgets/block.rs @@ -5,6 +5,7 @@ //! In its simplest form, a `Block` is a [border](Borders) around another widget. It can have a //! [title](Block::title) and [padding](Block::padding). +use itertools::Itertools; use strum::{Display, EnumString}; use crate::{prelude::*, symbols::border, widgets::Borders}; @@ -147,7 +148,6 @@ pub struct Block<'a> { titles_alignment: Alignment, /// The default position of the titles that don't have one titles_position: Position, - /// Visible borders borders: Borders, /// Border style @@ -525,9 +525,11 @@ impl Widget for Block<'_> { impl WidgetRef for Block<'_> { fn render_ref(&self, area: Rect, buf: &mut Buffer) { + let area = area.intersection(buf.area); if area.is_empty() { return; } + buf.set_style(area, self.style); self.render_borders(area, buf); self.render_titles(area, buf); } @@ -535,185 +537,227 @@ impl WidgetRef for Block<'_> { impl Block<'_> { fn render_borders(&self, area: Rect, buf: &mut Buffer) { - buf.set_style(area, self.style); - let symbols = self.border_set; + self.render_left_side(area, buf); + self.render_top_side(area, buf); + self.render_right_side(area, buf); + self.render_bottom_side(area, buf); - // Sides - if self.borders.intersects(Borders::LEFT) { - for y in area.top()..area.bottom() { - buf.get_mut(area.left(), y) - .set_symbol(symbols.vertical_left) - .set_style(self.border_style); - } - } - if self.borders.intersects(Borders::TOP) { - for x in area.left()..area.right() { - buf.get_mut(x, area.top()) - .set_symbol(symbols.horizontal_top) - .set_style(self.border_style); - } - } - if self.borders.intersects(Borders::RIGHT) { - let x = area.right() - 1; - for y in area.top()..area.bottom() { - buf.get_mut(x, y) - .set_symbol(symbols.vertical_right) - .set_style(self.border_style); - } - } - if self.borders.intersects(Borders::BOTTOM) { - let y = area.bottom() - 1; - for x in area.left()..area.right() { - buf.get_mut(x, y) - .set_symbol(symbols.horizontal_bottom) - .set_style(self.border_style); - } - } - - // Corners - if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) { - buf.get_mut(area.right() - 1, area.bottom() - 1) - .set_symbol(symbols.bottom_right) - .set_style(self.border_style); - } - if self.borders.contains(Borders::RIGHT | Borders::TOP) { - buf.get_mut(area.right() - 1, area.top()) - .set_symbol(symbols.top_right) - .set_style(self.border_style); - } - if self.borders.contains(Borders::LEFT | Borders::BOTTOM) { - buf.get_mut(area.left(), area.bottom() - 1) - .set_symbol(symbols.bottom_left) - .set_style(self.border_style); - } - if self.borders.contains(Borders::LEFT | Borders::TOP) { - buf.get_mut(area.left(), area.top()) - .set_symbol(symbols.top_left) - .set_style(self.border_style); - } - } - - /* Titles Rendering */ - fn get_title_y(&self, position: Position, area: Rect) -> u16 { - match position { - Position::Bottom => area.bottom() - 1, - Position::Top => area.top(), - } - } - - fn title_filter(&self, title: &Title, alignment: Alignment, position: Position) -> bool { - title.alignment.unwrap_or(self.titles_alignment) == alignment - && title.position.unwrap_or(self.titles_position) == position - } - - fn calculate_title_area_offsets(&self, area: Rect) -> (u16, u16, u16) { - let left_border_dx = u16::from(self.borders.intersects(Borders::LEFT)); - let right_border_dx = u16::from(self.borders.intersects(Borders::RIGHT)); - - let title_area_width = area - .width - .saturating_sub(left_border_dx) - .saturating_sub(right_border_dx); - - (left_border_dx, right_border_dx, title_area_width) - } - - fn render_left_titles(&self, position: Position, area: Rect, buf: &mut Buffer) { - let (left_border_dx, _, title_area_width) = self.calculate_title_area_offsets(area); - - let mut current_offset = left_border_dx; - self.titles - .iter() - .filter(|title| self.title_filter(title, Alignment::Left, position)) - .for_each(|title| { - let title_x = current_offset; - current_offset += title.content.width() as u16 + 1; - - // Clone the title's content, applying block title style then the title style - let mut content = title.content.clone(); - for span in content.spans.iter_mut() { - span.style = self.titles_style.patch(span.style); - } - - buf.set_line( - title_x + area.left(), - self.get_title_y(position, area), - &content, - title_area_width, - ); - }); - } - - fn render_center_titles(&self, position: Position, area: Rect, buf: &mut Buffer) { - let (_, _, title_area_width) = self.calculate_title_area_offsets(area); - - let titles = self - .titles - .iter() - .filter(|title| self.title_filter(title, Alignment::Center, position)); - - let titles_sum = titles - .clone() - .fold(-1, |acc, f| acc + f.content.width() as i16 + 1); // First element isn't spaced - - let mut current_offset = area.width.saturating_sub(titles_sum as u16) / 2; - titles.for_each(|title| { - let title_x = current_offset; - current_offset += title.content.width() as u16 + 1; - - // Clone the title's content, applying block title style then the title style - let mut content = title.content.clone(); - for span in content.spans.iter_mut() { - span.style = self.titles_style.patch(span.style); - } - - buf.set_line( - title_x + area.left(), - self.get_title_y(position, area), - &content, - title_area_width, - ); - }); - } - - fn render_right_titles(&self, position: Position, area: Rect, buf: &mut Buffer) { - let (_, right_border_dx, title_area_width) = self.calculate_title_area_offsets(area); - - let mut current_offset = right_border_dx; - self.titles - .iter() - .filter(|title| self.title_filter(title, Alignment::Right, position)) - .rev() // so that the titles appear in the order they have been set - .for_each(|title| { - current_offset += title.content.width() as u16 + 1; - let title_x = current_offset - 1; // First element isn't spaced - - // Clone the title's content, applying block title style then the title style - let mut content = title.content.clone(); - for span in content.spans.iter_mut() { - span.style = self.titles_style.patch(span.style); - } - - buf.set_line( - area.width.saturating_sub(title_x) + area.left(), - self.get_title_y(position, area), - &content, - title_area_width, - ); - }); - } - - fn render_title_position(&self, position: Position, area: Rect, buf: &mut Buffer) { - // Note: the order in which these functions are called define the overlapping behavior - self.render_right_titles(position, area, buf); - self.render_center_titles(position, area, buf); - self.render_left_titles(position, area, buf); + self.render_bottom_right_corner(buf, area); + self.render_top_right_corner(buf, area); + self.render_bottom_left_corner(buf, area); + self.render_top_left_corner(buf, area); } fn render_titles(&self, area: Rect, buf: &mut Buffer) { self.render_title_position(Position::Top, area, buf); self.render_title_position(Position::Bottom, area, buf); } + + fn render_title_position(&self, position: Position, area: Rect, buf: &mut Buffer) { + // NOTE: the order in which these functions are called defines the overlapping behavior + self.render_right_titles(position, area, buf); + self.render_center_titles(position, area, buf); + self.render_left_titles(position, area, buf); + } + + fn render_left_side(&self, area: Rect, buf: &mut Buffer) { + if self.borders.contains(Borders::LEFT) { + for y in area.top()..area.bottom() { + buf.get_mut(area.left(), y) + .set_symbol(self.border_set.vertical_left) + .set_style(self.border_style); + } + } + } + + fn render_top_side(&self, area: Rect, buf: &mut Buffer) { + if self.borders.contains(Borders::TOP) { + for x in area.left()..area.right() { + buf.get_mut(x, area.top()) + .set_symbol(self.border_set.horizontal_top) + .set_style(self.border_style); + } + } + } + + fn render_right_side(&self, area: Rect, buf: &mut Buffer) { + if self.borders.contains(Borders::RIGHT) { + let x = area.right() - 1; + for y in area.top()..area.bottom() { + buf.get_mut(x, y) + .set_symbol(self.border_set.vertical_right) + .set_style(self.border_style); + } + } + } + + fn render_bottom_side(&self, area: Rect, buf: &mut Buffer) { + if self.borders.contains(Borders::BOTTOM) { + let y = area.bottom() - 1; + for x in area.left()..area.right() { + buf.get_mut(x, y) + .set_symbol(self.border_set.horizontal_bottom) + .set_style(self.border_style); + } + } + } + + fn render_bottom_right_corner(&self, buf: &mut Buffer, area: Rect) { + if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) { + buf.get_mut(area.right() - 1, area.bottom() - 1) + .set_symbol(self.border_set.bottom_right) + .set_style(self.border_style); + } + } + + fn render_top_right_corner(&self, buf: &mut Buffer, area: Rect) { + if self.borders.contains(Borders::RIGHT | Borders::TOP) { + buf.get_mut(area.right() - 1, area.top()) + .set_symbol(self.border_set.top_right) + .set_style(self.border_style); + } + } + + fn render_bottom_left_corner(&self, buf: &mut Buffer, area: Rect) { + if self.borders.contains(Borders::LEFT | Borders::BOTTOM) { + buf.get_mut(area.left(), area.bottom() - 1) + .set_symbol(self.border_set.bottom_left) + .set_style(self.border_style); + } + } + + fn render_top_left_corner(&self, buf: &mut Buffer, area: Rect) { + if self.borders.contains(Borders::LEFT | Borders::TOP) { + buf.get_mut(area.left(), area.top()) + .set_symbol(self.border_set.top_left) + .set_style(self.border_style); + } + } + + /// Render titles aligned to the right of the block + /// + /// Currently (due to the way lines are truncated), the right side of the leftmost title will + /// be cut off if the block is too small to fit all titles. This is not ideal and should be + /// the left side of that leftmost that is cut off. This is due to the line being truncated + /// incorrectly. See https://github.com/ratatui-org/ratatui/issues/932 + fn render_right_titles(&self, position: Position, area: Rect, buf: &mut Buffer) { + let titles = self.filtered_titles(position, Alignment::Right); + let mut titles_area = self.titles_area(area, position); + + // render titles in reverse order to align them to the right + for title in titles.rev() { + if titles_area.is_empty() { + break; + } + let title_width = title.content.width() as u16; + let title_area = Rect { + x: titles_area + .right() + .saturating_sub(title_width) + .max(titles_area.left()), + ..titles_area + }; + buf.set_style(title_area, self.titles_style); + title.content.render_ref(title_area, buf); + + // bump the width of the titles area to the left + titles_area.width = titles_area + .width + .saturating_sub(title_width) + .saturating_sub(1); // space between titles + } + } + + /// Render titles in the center of the block + /// + /// Currently this method aligns the titles to the left inside a centered area. This is not + /// ideal and should be fixed in the future to align the titles to the center of the block and + /// truncate both sides of the titles if the block is too small to fit all titles. + fn render_center_titles(&self, position: Position, area: Rect, buf: &mut Buffer) { + let titles = self + .filtered_titles(position, Alignment::Center) + .collect_vec(); + let total_width = titles + .iter() + .map(|title| title.content.width() as u16 + 1) // space between titles + .sum::() + .saturating_sub(1); // no space for the last title + + let titles_area = self.titles_area(area, position); + let mut titles_area = Rect { + x: titles_area.left() + (titles_area.width.saturating_sub(total_width) / 2), + ..titles_area + }; + for title in titles { + if titles_area.is_empty() { + break; + } + let title_width = title.content.width() as u16; + let title_area = Rect { + width: title_width.min(titles_area.width), + ..titles_area + }; + buf.set_style(title_area, self.titles_style); + title.content.render_ref(title_area, buf); + + // bump the titles area to the right and reduce its width + titles_area.x = titles_area.x.saturating_add(title_width + 1); + titles_area.width = titles_area.width.saturating_sub(title_width + 1); + } + } + + /// Render titles aligned to the left of the block + fn render_left_titles(&self, position: Position, area: Rect, buf: &mut Buffer) { + let titles = self.filtered_titles(position, Alignment::Left); + let mut titles_area = self.titles_area(area, position); + for title in titles { + if titles_area.is_empty() { + break; + } + let title_width = title.content.width() as u16; + let title_area = Rect { + width: title_width.min(titles_area.width), + ..titles_area + }; + buf.set_style(title_area, self.titles_style); + title.content.render_ref(title_area, buf); + + // bump the titles area to the right and reduce its width + titles_area.x = titles_area.x.saturating_add(title_width + 1); + titles_area.width = titles_area.width.saturating_sub(title_width + 1); + } + } + + /// An iterator over the titles that match the position and alignment + fn filtered_titles( + &self, + position: Position, + alignment: Alignment, + ) -> impl DoubleEndedIterator { + self.titles.iter().filter(move |title| { + title.position.unwrap_or(self.titles_position) == position + && title.alignment.unwrap_or(self.titles_alignment) == alignment + }) + } + + /// An area that is one line tall and spans the width of the block excluding the borders and + /// is positioned at the top or bottom of the block. + fn titles_area(&self, area: Rect, position: Position) -> Rect { + let left_border = u16::from(self.borders.contains(Borders::LEFT)); + let right_border = u16::from(self.borders.contains(Borders::RIGHT)); + Rect { + x: area.left() + left_border, + y: match position { + Position::Top => area.top(), + Position::Bottom => area.bottom() - 1, + }, + width: area + .width + .saturating_sub(left_border) + .saturating_sub(right_border), + height: 1, + } + } } /// An extension trait for [`Block`] that provides some convenience methods. @@ -752,7 +796,7 @@ mod tests { use super::*; use crate::{ assert_buffer_eq, - layout::Rect, + layout::{Alignment, Rect}, style::{Color, Modifier, Stylize}, }; @@ -1120,6 +1164,24 @@ mod tests { } } + /// This is a regression test for bug https://github.com/ratatui-org/ratatui/issues/929 + #[test] + fn render_right_aligned_empty_title() { + let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3)); + Block::default() + .title("") + .title_alignment(Alignment::Right) + .render(buffer.area, &mut buffer); + assert_buffer_eq!( + buffer, + Buffer::with_lines(vec![ + " ", + " ", + " ", + ]) + ); + } + #[test] fn title_position() { let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2)); diff --git a/tests/widgets_block.rs b/tests/widgets_block.rs index fb79995a..6a5be481 100644 --- a/tests/widgets_block.rs +++ b/tests/widgets_block.rs @@ -15,21 +15,11 @@ use ratatui::{ fn widgets_block_renders() { let backend = TestBackend::new(10, 10); let mut terminal = Terminal::new(backend).unwrap(); + let block = Block::default() + .title(Span::styled("Title", Style::default().fg(Color::LightBlue))) + .borders(Borders::ALL); terminal - .draw(|f| { - let block = Block::default() - .title(Span::styled("Title", Style::default().fg(Color::LightBlue))) - .borders(Borders::ALL); - f.render_widget( - block, - Rect { - x: 0, - y: 0, - width: 8, - height: 8, - }, - ); - }) + .draw(|frame| frame.render_widget(block, Rect::new(0, 0, 8, 8))) .unwrap(); let mut expected = Buffer::with_lines(vec![ "┌Title─┐ ", @@ -51,16 +41,15 @@ fn widgets_block_renders() { #[test] fn widgets_block_titles_overlap() { - let test_case = |block, area: Rect, expected| { + #[track_caller] + fn test_case(block: Block, area: Rect, expected: Buffer) { let backend = TestBackend::new(area.width, area.height); let mut terminal = Terminal::new(backend).unwrap(); terminal - .draw(|f| { - f.render_widget(block, area); - }) + .draw(|frame| frame.render_widget(block, area)) .unwrap(); terminal.backend().assert_buffer(&expected); - }; + } // Left overrides the center test_case( @@ -68,12 +57,7 @@ fn widgets_block_titles_overlap() { .title(Title::from("aaaaa").alignment(Alignment::Left)) .title(Title::from("bbb").alignment(Alignment::Center)) .title(Title::from("ccc").alignment(Alignment::Right)), - Rect { - x: 0, - y: 0, - width: 10, - height: 1, - }, + Rect::new(0, 0, 10, 1), Buffer::with_lines(vec!["aaaaab ccc"]), ); @@ -83,12 +67,7 @@ fn widgets_block_titles_overlap() { .title(Title::from("aaaaa").alignment(Alignment::Left)) .title(Title::from("bbbbb").alignment(Alignment::Center)) .title(Title::from("ccccc").alignment(Alignment::Right)), - Rect { - x: 0, - y: 0, - width: 11, - height: 1, - }, + Rect::new(0, 0, 11, 1), Buffer::with_lines(vec!["aaaaabbbccc"]), ); @@ -99,12 +78,7 @@ fn widgets_block_titles_overlap() { .title(Title::from("aaaaa").alignment(Alignment::Left)) .title(Title::from("bbbbb").alignment(Alignment::Center)) .title(Title::from("ccccc").alignment(Alignment::Right)), - Rect { - x: 0, - y: 0, - width: 11, - height: 1, - }, + Rect::new(0, 0, 11, 1), Buffer::with_lines(vec!["aaaaabaaaaa"]), ); @@ -113,28 +87,22 @@ fn widgets_block_titles_overlap() { Block::default() .title(Title::from("bbbbb").alignment(Alignment::Center)) .title(Title::from("ccccccccccc").alignment(Alignment::Right)), - Rect { - x: 0, - y: 0, - width: 11, - height: 1, - }, + Rect::new(0, 0, 11, 1), Buffer::with_lines(vec!["cccbbbbbccc"]), ); } #[test] fn widgets_block_renders_on_small_areas() { - let test_case = |block, area: Rect, expected| { + #[track_caller] + fn test_case(block: Block, area: Rect, expected: Buffer) { let backend = TestBackend::new(area.width, area.height); let mut terminal = Terminal::new(backend).unwrap(); terminal - .draw(|f| { - f.render_widget(block, area); - }) + .draw(|frame| frame.render_widget(block, area)) .unwrap(); terminal.backend().assert_buffer(&expected); - }; + } let one_cell_test_cases = [ (Borders::NONE, "T"), @@ -147,152 +115,78 @@ fn widgets_block_renders_on_small_areas() { for (borders, symbol) in one_cell_test_cases.iter().cloned() { test_case( Block::default().title("Test").borders(borders), - Rect { - x: 0, - y: 0, - width: 0, - height: 0, - }, - Buffer::empty(Rect { - x: 0, - y: 0, - width: 0, - height: 0, - }), + Rect::new(0, 0, 0, 0), + Buffer::empty(Rect::new(0, 0, 0, 0)), ); test_case( Block::default().title("Test").borders(borders), - Rect { - x: 0, - y: 0, - width: 1, - height: 0, - }, - Buffer::empty(Rect { - x: 0, - y: 0, - width: 1, - height: 0, - }), + Rect::new(0, 0, 1, 0), + Buffer::empty(Rect::new(0, 0, 1, 0)), ); test_case( Block::default().title("Test").borders(borders), - Rect { - x: 0, - y: 0, - width: 0, - height: 1, - }, - Buffer::empty(Rect { - x: 0, - y: 0, - width: 0, - height: 1, - }), + Rect::new(0, 0, 0, 1), + Buffer::empty(Rect::new(0, 0, 0, 1)), ); test_case( Block::default().title("Test").borders(borders), - Rect { - x: 0, - y: 0, - width: 1, - height: 1, - }, + Rect::new(0, 0, 1, 1), Buffer::with_lines(vec![symbol]), ); } test_case( Block::default().title("Test").borders(Borders::LEFT), - Rect { - x: 0, - y: 0, - width: 4, - height: 1, - }, + Rect::new(0, 0, 4, 1), Buffer::with_lines(vec!["│Tes"]), ); test_case( Block::default().title("Test").borders(Borders::RIGHT), - Rect { - x: 0, - y: 0, - width: 4, - height: 1, - }, + Rect::new(0, 0, 4, 1), Buffer::with_lines(vec!["Tes│"]), ); test_case( Block::default().title("Test").borders(Borders::RIGHT), - Rect { - x: 0, - y: 0, - width: 4, - height: 1, - }, + Rect::new(0, 0, 4, 1), Buffer::with_lines(vec!["Tes│"]), ); test_case( Block::default() .title("Test") .borders(Borders::LEFT | Borders::RIGHT), - Rect { - x: 0, - y: 0, - width: 4, - height: 1, - }, + Rect::new(0, 0, 4, 1), Buffer::with_lines(vec!["│Te│"]), ); test_case( Block::default().title("Test").borders(Borders::TOP), - Rect { - x: 0, - y: 0, - width: 4, - height: 1, - }, + Rect::new(0, 0, 4, 1), Buffer::with_lines(vec!["Test"]), ); test_case( Block::default().title("Test").borders(Borders::TOP), - Rect { - x: 0, - y: 0, - width: 5, - height: 1, - }, + Rect::new(0, 0, 5, 1), Buffer::with_lines(vec!["Test─"]), ); test_case( Block::default() .title("Test") .borders(Borders::LEFT | Borders::TOP), - Rect { - x: 0, - y: 0, - width: 5, - height: 1, - }, + Rect::new(0, 0, 5, 1), Buffer::with_lines(vec!["┌Test"]), ); test_case( Block::default() .title("Test") .borders(Borders::LEFT | Borders::TOP), - Rect { - x: 0, - y: 0, - width: 6, - height: 1, - }, + Rect::new(0, 0, 6, 1), Buffer::with_lines(vec!["┌Test─"]), ); } #[test] fn widgets_block_title_alignment() { - let test_case = |alignment, borders, expected| { - let backend = TestBackend::new(15, 2); + #[track_caller] + fn test_case(alignment: Alignment, borders: Borders, expected: Buffer) { + let backend = TestBackend::new(15, 3); let mut terminal = Terminal::new(backend).unwrap(); let block1 = Block::default() @@ -304,270 +198,371 @@ fn widgets_block_title_alignment() { .title_alignment(alignment) .borders(borders); - let area = Rect { - x: 1, - y: 0, - width: 13, - height: 2, - }; + let area = Rect::new(1, 0, 13, 3); for block in [block1, block2] { terminal - .draw(|f| { - f.render_widget(block, area); - }) + .draw(|frame| frame.render_widget(block, area)) .unwrap(); - terminal.backend().assert_buffer(&expected); } - }; + } // title top-left with all borders test_case( Alignment::Left, Borders::ALL, - Buffer::with_lines(vec![" ┌Title──────┐ ", " └───────────┘ "]), + Buffer::with_lines(vec![ + " ┌Title──────┐ ", + " │ │ ", + " └───────────┘ ", + ]), ); // title top-left without top border test_case( Alignment::Left, Borders::LEFT | Borders::BOTTOM | Borders::RIGHT, - Buffer::with_lines(vec![" │Title │ ", " └───────────┘ "]), + Buffer::with_lines(vec![ + " │Title │ ", + " │ │ ", + " └───────────┘ ", + ]), ); // title top-left with no left border test_case( Alignment::Left, Borders::TOP | Borders::RIGHT | Borders::BOTTOM, - Buffer::with_lines(vec![" Title───────┐ ", " ────────────┘ "]), + Buffer::with_lines(vec![ + " Title───────┐ ", + " │ ", + " ────────────┘ ", + ]), ); // title top-left without right border test_case( Alignment::Left, Borders::LEFT | Borders::TOP | Borders::BOTTOM, - Buffer::with_lines(vec![" ┌Title─────── ", " └──────────── "]), + Buffer::with_lines(vec![ + " ┌Title─────── ", + " │ ", + " └──────────── ", + ]), ); // title top-left without borders test_case( Alignment::Left, Borders::NONE, - Buffer::with_lines(vec![" Title ", " "]), + Buffer::with_lines(vec![ + " Title ", + " ", + " ", + ]), ); // title center with all borders test_case( Alignment::Center, Borders::ALL, - Buffer::with_lines(vec![" ┌───Title───┐ ", " └───────────┘ "]), + Buffer::with_lines(vec![ + " ┌───Title───┐ ", + " │ │ ", + " └───────────┘ ", + ]), ); // title center without top border test_case( Alignment::Center, Borders::LEFT | Borders::BOTTOM | Borders::RIGHT, - Buffer::with_lines(vec![" │ Title │ ", " └───────────┘ "]), + Buffer::with_lines(vec![ + " │ Title │ ", + " │ │ ", + " └───────────┘ ", + ]), ); // title center with no left border test_case( Alignment::Center, Borders::TOP | Borders::RIGHT | Borders::BOTTOM, - Buffer::with_lines(vec![" ────Title───┐ ", " ────────────┘ "]), + Buffer::with_lines(vec![ + " ───Title────┐ ", + " │ ", + " ────────────┘ ", + ]), ); // title center without right border test_case( Alignment::Center, Borders::LEFT | Borders::TOP | Borders::BOTTOM, - Buffer::with_lines(vec![" ┌───Title──── ", " └──────────── "]), + Buffer::with_lines(vec![ + " ┌───Title──── ", + " │ ", + " └──────────── ", + ]), ); // title center without borders test_case( Alignment::Center, Borders::NONE, - Buffer::with_lines(vec![" Title ", " "]), + Buffer::with_lines(vec![ + " Title ", + " ", + " ", + ]), ); // title top-right with all borders test_case( Alignment::Right, Borders::ALL, - Buffer::with_lines(vec![" ┌──────Title┐ ", " └───────────┘ "]), + Buffer::with_lines(vec![ + " ┌──────Title┐ ", + " │ │ ", + " └───────────┘ ", + ]), ); // title top-right without top border test_case( Alignment::Right, Borders::LEFT | Borders::BOTTOM | Borders::RIGHT, - Buffer::with_lines(vec![" │ Title│ ", " └───────────┘ "]), + Buffer::with_lines(vec![ + " │ Title│ ", + " │ │ ", + " └───────────┘ ", + ]), ); // title top-right with no left border test_case( Alignment::Right, Borders::TOP | Borders::RIGHT | Borders::BOTTOM, - Buffer::with_lines(vec![" ───────Title┐ ", " ────────────┘ "]), + Buffer::with_lines(vec![ + " ───────Title┐ ", + " │ ", + " ────────────┘ ", + ]), ); // title top-right without right border test_case( Alignment::Right, Borders::LEFT | Borders::TOP | Borders::BOTTOM, - Buffer::with_lines(vec![" ┌───────Title ", " └──────────── "]), + Buffer::with_lines(vec![ + " ┌───────Title ", + " │ ", + " └──────────── ", + ]), ); // title top-right without borders test_case( Alignment::Right, Borders::NONE, - Buffer::with_lines(vec![" Title ", " "]), + Buffer::with_lines(vec![ + " Title ", + " ", + " ", + ]), ); } #[test] fn widgets_block_title_alignment_bottom() { - let test_case = |alignment, borders, expected| { - let backend = TestBackend::new(15, 2); + #[track_caller] + fn test_case(alignment: Alignment, borders: Borders, expected: Buffer) { + let backend = TestBackend::new(15, 3); let mut terminal = Terminal::new(backend).unwrap(); - let block = Block::default() - .title( - Title::from(Span::styled("Title", Style::default())) - .alignment(alignment) - .position(Position::Bottom), - ) - .borders(borders); - - let area = Rect { - x: 1, - y: 0, - width: 13, - height: 2, - }; - + let title = Title::from(Span::styled("Title", Style::default())) + .alignment(alignment) + .position(Position::Bottom); + let block = Block::default().title(title).borders(borders); + let area = Rect::new(1, 0, 13, 3); terminal - .draw(|f| { - f.render_widget(block, area); - }) + .draw(|frame| frame.render_widget(block, area)) .unwrap(); - terminal.backend().assert_buffer(&expected); - }; + } // title bottom-left with all borders test_case( Alignment::Left, Borders::ALL, - Buffer::with_lines(vec![" ┌───────────┐ ", " └Title──────┘ "]), + Buffer::with_lines(vec![ + " ┌───────────┐ ", + " │ │ ", + " └Title──────┘ ", + ]), ); // title bottom-left without bottom border test_case( Alignment::Left, Borders::LEFT | Borders::TOP | Borders::RIGHT, - Buffer::with_lines(vec![" ┌───────────┐ ", " │Title │ "]), + Buffer::with_lines(vec![ + " ┌───────────┐ ", + " │ │ ", + " │Title │ ", + ]), ); // title bottom-left with no left border test_case( Alignment::Left, Borders::TOP | Borders::RIGHT | Borders::BOTTOM, - Buffer::with_lines(vec![" ────────────┐ ", " Title───────┘ "]), + Buffer::with_lines(vec![ + " ────────────┐ ", + " │ ", + " Title───────┘ ", + ]), ); // title bottom-left without right border test_case( Alignment::Left, Borders::LEFT | Borders::TOP | Borders::BOTTOM, - Buffer::with_lines(vec![" ┌──────────── ", " └Title─────── "]), + Buffer::with_lines(vec![ + " ┌──────────── ", + " │ ", + " └Title─────── ", + ]), ); // title bottom-left without borders test_case( Alignment::Left, Borders::NONE, - Buffer::with_lines(vec![" ", " Title "]), + Buffer::with_lines(vec![ + " ", + " ", + " Title ", + ]), ); // title center with all borders test_case( Alignment::Center, Borders::ALL, - Buffer::with_lines(vec![" ┌───────────┐ ", " └───Title───┘ "]), + Buffer::with_lines(vec![ + " ┌───────────┐ ", + " │ │ ", + " └───Title───┘ ", + ]), ); // title center without bottom border test_case( Alignment::Center, Borders::LEFT | Borders::TOP | Borders::RIGHT, - Buffer::with_lines(vec![" ┌───────────┐ ", " │ Title │ "]), + Buffer::with_lines(vec![ + " ┌───────────┐ ", + " │ │ ", + " │ Title │ ", + ]), ); // title center with no left border test_case( Alignment::Center, Borders::TOP | Borders::RIGHT | Borders::BOTTOM, - Buffer::with_lines(vec![" ────────────┐ ", " ────Title───┘ "]), + Buffer::with_lines(vec![ + " ────────────┐ ", + " │ ", + " ───Title────┘ ", + ]), ); // title center without right border test_case( Alignment::Center, Borders::LEFT | Borders::TOP | Borders::BOTTOM, - Buffer::with_lines(vec![" ┌──────────── ", " └───Title──── "]), + Buffer::with_lines(vec![ + " ┌──────────── ", + " │ ", + " └───Title──── ", + ]), ); // title center without borders test_case( Alignment::Center, Borders::NONE, - Buffer::with_lines(vec![" ", " Title "]), + Buffer::with_lines(vec![ + " ", + " ", + " Title ", + ]), ); // title bottom-right with all borders test_case( Alignment::Right, Borders::ALL, - Buffer::with_lines(vec![" ┌───────────┐ ", " └──────Title┘ "]), + Buffer::with_lines(vec![ + " ┌───────────┐ ", + " │ │ ", + " └──────Title┘ ", + ]), ); // title bottom-right without bottom border test_case( Alignment::Right, Borders::LEFT | Borders::TOP | Borders::RIGHT, - Buffer::with_lines(vec![" ┌───────────┐ ", " │ Title│ "]), + Buffer::with_lines(vec![ + " ┌───────────┐ ", + " │ │ ", + " │ Title│ ", + ]), ); // title bottom-right with no left border test_case( Alignment::Right, Borders::TOP | Borders::RIGHT | Borders::BOTTOM, - Buffer::with_lines(vec![" ────────────┐ ", " ───────Title┘ "]), + Buffer::with_lines(vec![ + " ────────────┐ ", + " │ ", + " ───────Title┘ ", + ]), ); // title bottom-right without right border test_case( Alignment::Right, Borders::LEFT | Borders::TOP | Borders::BOTTOM, - Buffer::with_lines(vec![" ┌──────────── ", " └───────Title "]), + Buffer::with_lines(vec![ + " ┌──────────── ", + " │ ", + " └───────Title ", + ]), ); // title bottom-right without borders test_case( Alignment::Right, Borders::NONE, - Buffer::with_lines(vec![" ", " Title "]), + Buffer::with_lines(vec![ + " ", + " ", + " Title ", + ]), ); } #[test] fn widgets_block_multiple_titles() { - let test_case = |title_a, title_b, borders, expected| { - let backend = TestBackend::new(15, 2); + #[track_caller] + fn test_case(title_a: Title, title_b: Title, borders: Borders, expected: Buffer) { + let backend = TestBackend::new(15, 3); let mut terminal = Terminal::new(backend).unwrap(); let block = Block::default() @@ -575,12 +570,7 @@ fn widgets_block_multiple_titles() { .title(title_b) .borders(borders); - let area = Rect { - x: 1, - y: 0, - width: 13, - height: 2, - }; + let area = Rect::new(1, 0, 13, 3); terminal .draw(|f| { @@ -589,14 +579,18 @@ fn widgets_block_multiple_titles() { .unwrap(); terminal.backend().assert_buffer(&expected); - }; + } // title bottom-left with all borders test_case( Title::from("foo"), Title::from("bar"), Borders::ALL, - Buffer::with_lines(vec![" ┌foo─bar────┐ ", " └───────────┘ "]), + Buffer::with_lines(vec![ + " ┌foo─bar────┐ ", + " │ │ ", + " └───────────┘ ", + ]), ); // title top-left without top border @@ -604,7 +598,11 @@ fn widgets_block_multiple_titles() { Title::from("foo"), Title::from("bar"), Borders::LEFT | Borders::BOTTOM | Borders::RIGHT, - Buffer::with_lines(vec![" │foo bar │ ", " └───────────┘ "]), + Buffer::with_lines(vec![ + " │foo bar │ ", + " │ │ ", + " └───────────┘ ", + ]), ); // title top-left with no left border @@ -612,7 +610,11 @@ fn widgets_block_multiple_titles() { Title::from("foo"), Title::from("bar"), Borders::TOP | Borders::RIGHT | Borders::BOTTOM, - Buffer::with_lines(vec![" foo─bar─────┐ ", " ────────────┘ "]), + Buffer::with_lines(vec![ + " foo─bar─────┐ ", + " │ ", + " ────────────┘ ", + ]), ); // title top-left without right border @@ -620,7 +622,11 @@ fn widgets_block_multiple_titles() { Title::from("foo"), Title::from("bar"), Borders::LEFT | Borders::TOP | Borders::BOTTOM, - Buffer::with_lines(vec![" ┌foo─bar───── ", " └──────────── "]), + Buffer::with_lines(vec![ + " ┌foo─bar───── ", + " │ ", + " └──────────── ", + ]), ); // title top-left without borders @@ -628,7 +634,11 @@ fn widgets_block_multiple_titles() { Title::from("foo"), Title::from("bar"), Borders::NONE, - Buffer::with_lines(vec![" foo bar ", " "]), + Buffer::with_lines(vec![ + " foo bar ", + " ", + " ", + ]), ); // title center with all borders @@ -636,7 +646,11 @@ fn widgets_block_multiple_titles() { Title::from("foo").alignment(Alignment::Center), Title::from("bar").alignment(Alignment::Center), Borders::ALL, - Buffer::with_lines(vec![" ┌──foo─bar──┐ ", " └───────────┘ "]), + Buffer::with_lines(vec![ + " ┌──foo─bar──┐ ", + " │ │ ", + " └───────────┘ ", + ]), ); // title center without top border @@ -644,7 +658,11 @@ fn widgets_block_multiple_titles() { Title::from("foo").alignment(Alignment::Center), Title::from("bar").alignment(Alignment::Center), Borders::LEFT | Borders::BOTTOM | Borders::RIGHT, - Buffer::with_lines(vec![" │ foo bar │ ", " └───────────┘ "]), + Buffer::with_lines(vec![ + " │ foo bar │ ", + " │ │ ", + " └───────────┘ ", + ]), ); // title center with no left border @@ -652,7 +670,11 @@ fn widgets_block_multiple_titles() { Title::from("foo").alignment(Alignment::Center), Title::from("bar").alignment(Alignment::Center), Borders::TOP | Borders::RIGHT | Borders::BOTTOM, - Buffer::with_lines(vec![" ───foo─bar──┐ ", " ────────────┘ "]), + Buffer::with_lines(vec![ + " ──foo─bar───┐ ", + " │ ", + " ────────────┘ ", + ]), ); // title center without right border @@ -660,7 +682,11 @@ fn widgets_block_multiple_titles() { Title::from("foo").alignment(Alignment::Center), Title::from("bar").alignment(Alignment::Center), Borders::LEFT | Borders::TOP | Borders::BOTTOM, - Buffer::with_lines(vec![" ┌──foo─bar─── ", " └──────────── "]), + Buffer::with_lines(vec![ + " ┌──foo─bar─── ", + " │ ", + " └──────────── ", + ]), ); // title center without borders @@ -668,7 +694,11 @@ fn widgets_block_multiple_titles() { Title::from("foo").alignment(Alignment::Center), Title::from("bar").alignment(Alignment::Center), Borders::NONE, - Buffer::with_lines(vec![" foo bar ", " "]), + Buffer::with_lines(vec![ + " foo bar ", + " ", + " ", + ]), ); // title top-right with all borders @@ -676,7 +706,11 @@ fn widgets_block_multiple_titles() { Title::from("foo").alignment(Alignment::Right), Title::from("bar").alignment(Alignment::Right), Borders::ALL, - Buffer::with_lines(vec![" ┌────foo─bar┐ ", " └───────────┘ "]), + Buffer::with_lines(vec![ + " ┌────foo─bar┐ ", + " │ │ ", + " └───────────┘ ", + ]), ); // title top-right without top border @@ -684,7 +718,11 @@ fn widgets_block_multiple_titles() { Title::from("foo").alignment(Alignment::Right), Title::from("bar").alignment(Alignment::Right), Borders::LEFT | Borders::BOTTOM | Borders::RIGHT, - Buffer::with_lines(vec![" │ foo bar│ ", " └───────────┘ "]), + Buffer::with_lines(vec![ + " │ foo bar│ ", + " │ │ ", + " └───────────┘ ", + ]), ); // title top-right with no left border @@ -692,7 +730,11 @@ fn widgets_block_multiple_titles() { Title::from("foo").alignment(Alignment::Right), Title::from("bar").alignment(Alignment::Right), Borders::TOP | Borders::RIGHT | Borders::BOTTOM, - Buffer::with_lines(vec![" ─────foo─bar┐ ", " ────────────┘ "]), + Buffer::with_lines(vec![ + " ─────foo─bar┐ ", + " │ ", + " ────────────┘ ", + ]), ); // title top-right without right border @@ -700,7 +742,11 @@ fn widgets_block_multiple_titles() { Title::from("foo").alignment(Alignment::Right), Title::from("bar").alignment(Alignment::Right), Borders::LEFT | Borders::TOP | Borders::BOTTOM, - Buffer::with_lines(vec![" ┌─────foo─bar ", " └──────────── "]), + Buffer::with_lines(vec![ + " ┌─────foo─bar ", + " │ ", + " └──────────── ", + ]), ); // title top-right without borders @@ -708,6 +754,10 @@ fn widgets_block_multiple_titles() { Title::from("foo").alignment(Alignment::Right), Title::from("bar").alignment(Alignment::Right), Borders::NONE, - Buffer::with_lines(vec![" foo bar ", " "]), + Buffer::with_lines(vec![ + " foo bar ", + " ", + " ", + ]), ); }