diff --git a/examples/block.rs b/examples/block.rs index 4937d37d..26d00da0 100644 --- a/examples/block.rs +++ b/examples/block.rs @@ -6,7 +6,7 @@ use std::{error::Error, io}; use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use tui::{ backend::TermionBackend, - layout::{Constraint, Direction, Layout}, + layout::{Alignment, Constraint, Direction, Layout}, style::{Color, Modifier, Style}, text::Span, widgets::{Block, BorderType, Borders}, @@ -30,21 +30,28 @@ fn main() -> Result<(), Box> { // Just draw the block and the group on the same area and build the group // with at least a margin of 1 let size = f.size(); + + // Surounding block let block = Block::default() .borders(Borders::ALL) .title("Main block with round corners") + .title_alignment(Alignment::Center) .border_type(BorderType::Rounded); f.render_widget(block, size); + let chunks = Layout::default() .direction(Direction::Vertical) .margin(4) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .split(f.size()); + // Top two inner blocks let top_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .split(chunks[0]); + + // Top left inner block with green background let block = Block::default() .title(vec![ Span::styled("With", Style::default().fg(Color::Yellow)), @@ -53,21 +60,29 @@ fn main() -> Result<(), Box> { .style(Style::default().bg(Color::Green)); f.render_widget(block, top_chunks[0]); - let block = Block::default().title(Span::styled( - "Styled title", - Style::default() - .fg(Color::White) - .bg(Color::Red) - .add_modifier(Modifier::BOLD), - )); + // Top right inner block with styled title aligned to the right + let block = Block::default() + .title(Span::styled( + "Styled title", + Style::default() + .fg(Color::White) + .bg(Color::Red) + .add_modifier(Modifier::BOLD), + )) + .title_alignment(Alignment::Right); f.render_widget(block, top_chunks[1]); + // Bottom two inner blocks let bottom_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .split(chunks[1]); + + // Bottom left block with all default borders let block = Block::default().title("With borders").borders(Borders::ALL); f.render_widget(block, bottom_chunks[0]); + + // Bottom right block with styled left and right border let block = Block::default() .title("With styled borders and doubled borders") .border_style(Style::default().fg(Color::Cyan)) diff --git a/src/widgets/block.rs b/src/widgets/block.rs index 70610134..ae5b58a9 100644 --- a/src/widgets/block.rs +++ b/src/widgets/block.rs @@ -1,6 +1,6 @@ use crate::{ buffer::Buffer, - layout::Rect, + layout::{Alignment, Rect}, style::Style, symbols::line, text::{Span, Spans}, @@ -45,6 +45,9 @@ impl BorderType { pub struct Block<'a> { /// Optional title place on the upper left of the block title: Option>, + /// Title alignment. The default is top left of the block, but one can choose to place + /// title in the top middle, or top right of the block + title_alignment: Alignment, /// Visible borders borders: Borders, /// Border style @@ -60,6 +63,7 @@ impl<'a> Default for Block<'a> { fn default() -> Block<'a> { Block { title: None, + title_alignment: Alignment::Left, borders: Borders::NONE, border_style: Default::default(), border_type: BorderType::Plain, @@ -89,6 +93,11 @@ impl<'a> Block<'a> { self } + pub fn title_alignment(mut self, alignment: Alignment) -> Block<'a> { + self.title_alignment = alignment; + self + } + pub fn border_style(mut self, style: Style) -> Block<'a> { self.border_style = style; self @@ -192,19 +201,38 @@ impl<'a> Widget for Block<'a> { .set_style(self.border_style); } + // Title if let Some(title) = self.title { - let lx = if self.borders.intersects(Borders::LEFT) { + let left_border_dx = if self.borders.intersects(Borders::LEFT) { 1 } else { 0 }; - let rx = if self.borders.intersects(Borders::RIGHT) { + + let right_border_dx = if self.borders.intersects(Borders::RIGHT) { 1 } else { 0 }; - let width = area.width.saturating_sub(lx).saturating_sub(rx); - buf.set_spans(area.left() + lx, area.top(), &title, width); + + let title_area_width = area + .width + .saturating_sub(left_border_dx) + .saturating_sub(right_border_dx); + + let title_dx = match self.title_alignment { + Alignment::Left => left_border_dx, + Alignment::Center => area.width.saturating_sub(title.width() as u16) / 2, + Alignment::Right => area + .width + .saturating_sub(title.width() as u16) + .saturating_sub(right_border_dx), + }; + + let title_x = area.left() + title_dx; + let title_y = area.top(); + + buf.set_spans(title_x, title_y, &title, title_area_width); } } } @@ -507,5 +535,39 @@ mod tests { height: 0, }, ); + assert_eq!( + Block::default() + .title("Test") + .title_alignment(Alignment::Center) + .inner(Rect { + x: 0, + y: 0, + width: 0, + height: 1, + }), + Rect { + x: 0, + y: 1, + width: 0, + height: 0, + }, + ); + assert_eq!( + Block::default() + .title("Test") + .title_alignment(Alignment::Right) + .inner(Rect { + x: 0, + y: 0, + width: 0, + height: 1, + }), + Rect { + x: 0, + y: 1, + width: 0, + height: 0, + }, + ); } } diff --git a/tests/widgets_block.rs b/tests/widgets_block.rs index d3111081..06d59aa6 100644 --- a/tests/widgets_block.rs +++ b/tests/widgets_block.rs @@ -1,7 +1,7 @@ use tui::{ backend::TestBackend, buffer::Buffer, - layout::Rect, + layout::{Alignment, Rect}, style::{Color, Style}, text::Span, widgets::{Block, Borders}, @@ -211,3 +211,136 @@ fn widgets_block_renders_on_small_areas() { Buffer::with_lines(vec!["┌Test─"]), ); } + +#[test] +fn widgets_block_title_alignment() { + let test_case = |alignment, borders, expected| { + let backend = TestBackend::new(15, 2); + let mut terminal = Terminal::new(backend).unwrap(); + + let block = Block::default() + .title(Span::styled("Title", Style::default())) + .title_alignment(alignment) + .borders(borders); + + let area = Rect { + x: 1, + y: 0, + width: 13, + height: 2, + }; + + terminal + .draw(|f| { + f.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──────┐ ", " └───────────┘ "]), + ); + + // title top-left without top border + test_case( + Alignment::Left, + Borders::LEFT | Borders::BOTTOM | Borders::RIGHT, + 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───────┐ ", " ────────────┘ "]), + ); + + // title top-left without right border + test_case( + Alignment::Left, + Borders::LEFT | Borders::TOP | Borders::BOTTOM, + Buffer::with_lines(vec![" ┌Title─────── ", " └──────────── "]), + ); + + // title top-left without borders + test_case( + Alignment::Left, + Borders::NONE, + Buffer::with_lines(vec![" Title ", " "]), + ); + + // title center with all borders + test_case( + Alignment::Center, + Borders::ALL, + 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 │ ", " └───────────┘ "]), + ); + + // title center with no left border + test_case( + Alignment::Center, + Borders::TOP | Borders::RIGHT | Borders::BOTTOM, + 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──── ", " └──────────── "]), + ); + + // title center without borders + test_case( + Alignment::Center, + Borders::NONE, + Buffer::with_lines(vec![" Title ", " "]), + ); + + // title top-right with all borders + test_case( + Alignment::Right, + Borders::ALL, + 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│ ", " └───────────┘ "]), + ); + + // title top-right with no left border + test_case( + Alignment::Right, + Borders::TOP | Borders::RIGHT | Borders::BOTTOM, + 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 ", " └──────────── "]), + ); + + // title top-right without borders + test_case( + Alignment::Right, + Borders::NONE, + Buffer::with_lines(vec![" Title ", " "]), + ); +}