Skip to content

Commit

Permalink
feat: add Block::title_top and Block::title_top_bottom (#940)
Browse files Browse the repository at this point in the history
This adds the ability to add titles to the top and bottom of a block
without having to use the `Title` struct (which will be removed in a
future release - likely v0.28.0).

Fixes a subtle bug if the title was created from a right aligned Line
and was also right aligned. The title would be rendered one cell too far
to the right.

```rust
Block::bordered()
    .title_top(Line::raw("A").left_aligned())
    .title_top(Line::raw("B").centered())
    .title_top(Line::raw("C").right_aligned())
    .title_bottom(Line::raw("D").left_aligned())
    .title_bottom(Line::raw("E").centered())
    .title_bottom(Line::raw("F").right_aligned())
    .render(buffer.area, &mut buffer);
// renders
"┌A─────B─────C┐",
"│             │",
"└D─────E─────F┘",
```

Addresses part of #738

<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
  • Loading branch information
joshka committed Feb 9, 2024
1 parent 91040c0 commit 9182f47
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 2 deletions.
115 changes: 115 additions & 0 deletions src/widgets/block.rs
Expand Up @@ -22,6 +22,20 @@ pub use title::{Position, Title};
/// [`Title`] using [`Block::title`]. It can also be [styled](Block::style) and
/// [padded](Block::padding).
///
/// You can call the title methods multiple times to add multiple titles. Each title will be
/// rendered with a single space separating titles that are in the same position or alignment. When
/// both centered and non-centered titles are rendered, the centered space is calculated based on
/// the full width of the block, rather than the leftover width.
///
/// Titles are not rendered in the corners of the block unless there is no border on that edge.
/// If the block is too small and multiple titles overlap, the border may get cut off at a corner.
///
/// ```plain
/// ┌With at least a left border───
///
/// Without left border───
/// ```
///
/// # Examples
///
/// ```
Expand Down Expand Up @@ -228,6 +242,62 @@ impl<'a> Block<'a> {
self
}

/// Adds a title to the top of the block.
///
/// You can provide any type that can be converted into [`Line`] including: strings, string
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](crate::text::Span), or vectors of
/// [spans](crate::text::Span) (`Vec<Span>`).
///
/// # Example
///
/// ```
/// # use ratatui::{ prelude::*, widgets::* };
/// Block::bordered()
/// .title_top("Left1") // By default in the top left corner
/// .title_top(Line::from("Left2").left_aligned())
/// .title_top(Line::from("Right").right_aligned())
/// .title_top(Line::from("Center").centered());
///
/// // Renders
/// // ┌Left1─Left2───Center─────────Right┐
/// // │ │
/// // └──────────────────────────────────┘
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn title_top<T: Into<Line<'a>>>(mut self, title: T) -> Self {
let title = Title::from(title).position(Position::Top);
self.titles.push(title);
self
}

/// Adds a title to the bottom of the block.
///
/// You can provide any type that can be converted into [`Line`] including: strings, string
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](crate::text::Span), or vectors of
/// [spans](crate::text::Span) (`Vec<Span>`).
///
/// # Example
///
/// ```
/// # use ratatui::{ prelude::*, widgets::* };
/// Block::bordered()
/// .title_bottom("Left1") // By default in the top left corner
/// .title_bottom(Line::from("Left2").left_aligned())
/// .title_bottom(Line::from("Right").right_aligned())
/// .title_bottom(Line::from("Center").centered());
///
/// // Renders
/// // ┌──────────────────────────────────┐
/// // │ │
/// // └Left1─Left2───Center─────────Right┘
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn title_bottom<T: Into<Line<'a>>>(mut self, title: T) -> Self {
let title = Title::from(title).position(Position::Bottom);
self.titles.push(title);
self
}

/// Applies the style to all titles.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
Expand Down Expand Up @@ -655,6 +725,7 @@ impl Block<'_> {
.right()
.saturating_sub(title_width)
.max(titles_area.left()),
width: title_width.min(titles_area.width),
..titles_area
};
buf.set_style(title_area, self.titles_style);
Expand Down Expand Up @@ -1130,6 +1201,50 @@ mod tests {
)
}

#[test]
fn title() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
use Alignment::*;
use Position::*;
Block::bordered()
.title(Title::from("A").position(Top).alignment(Left))
.title(Title::from("B").position(Top).alignment(Center))
.title(Title::from("C").position(Top).alignment(Right))
.title(Title::from("D").position(Bottom).alignment(Left))
.title(Title::from("E").position(Bottom).alignment(Center))
.title(Title::from("F").position(Bottom).alignment(Right))
.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"┌A─────B─────C┐",
"│ │",
"└D─────E─────F┘",
])
);
}

#[test]
fn title_top_bottom() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Block::bordered()
.title_top(Line::raw("A").left_aligned())
.title_top(Line::raw("B").centered())
.title_top(Line::raw("C").right_aligned())
.title_bottom(Line::raw("D").left_aligned())
.title_bottom(Line::raw("E").centered())
.title_bottom(Line::raw("F").right_aligned())
.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"┌A─────B─────C┐",
"│ │",
"└D─────E─────F┘",
])
);
}

#[test]
fn title_alignment() {
let tests = vec![
Expand Down
31 changes: 29 additions & 2 deletions src/widgets/block/title.rs
Expand Up @@ -115,18 +115,25 @@ where
T: Into<Line<'a>>,
{
fn from(value: T) -> Self {
Self::default().content(value.into())
let content = value.into();
let alignment = content.alignment;
Self {
content,
alignment,
position: None,
}
}
}

#[cfg(test)]
mod tests {
use rstest::rstest;
use strum::ParseError;

use super::*;

#[test]
fn position_tostring() {
fn position_to_string() {
assert_eq!(Position::Top.to_string(), "Top");
assert_eq!(Position::Bottom.to_string(), "Bottom");
}
Expand All @@ -137,4 +144,24 @@ mod tests {
assert_eq!("Bottom".parse::<Position>(), Ok(Position::Bottom));
assert_eq!("".parse::<Position>(), Err(ParseError::VariantNotFound));
}

#[test]
fn title_from_line() {
let title = Title::from(Line::raw("Title"));
assert_eq!(title.content, Line::from("Title"));
assert_eq!(title.alignment, None);
assert_eq!(title.position, None);
}

#[rstest]
#[case::left(Alignment::Left)]
#[case::center(Alignment::Center)]
#[case::right(Alignment::Right)]
fn title_from_line_with_alignment(#[case] alignment: Alignment) {
let line = Line::raw("Title").alignment(alignment);
let title = Title::from(line.clone());
assert_eq!(title.content, line);
assert_eq!(title.alignment, Some(alignment));
assert_eq!(title.position, None);
}
}

0 comments on commit 9182f47

Please sign in to comment.