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 ", + " ", + " ", + ]), ); }