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
This commit is contained in:
Josh McKinney 2024-02-07 15:24:14 -08:00 committed by GitHub
parent 8fb46301a0
commit 2202059259
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 513 additions and 401 deletions

View file

@ -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::<u16>()
.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<Item = &Title> {
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));

View file

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