Skip to content

Commit 9182f47

Browse files
authored
feat: add Block::title_top and Block::title_top_bottom (#940)
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. -->
1 parent 91040c0 commit 9182f47

File tree

2 files changed

+144
-2
lines changed

2 files changed

+144
-2
lines changed

src/widgets/block.rs

+115
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ pub use title::{Position, Title};
2222
/// [`Title`] using [`Block::title`]. It can also be [styled](Block::style) and
2323
/// [padded](Block::padding).
2424
///
25+
/// You can call the title methods multiple times to add multiple titles. Each title will be
26+
/// rendered with a single space separating titles that are in the same position or alignment. When
27+
/// both centered and non-centered titles are rendered, the centered space is calculated based on
28+
/// the full width of the block, rather than the leftover width.
29+
///
30+
/// Titles are not rendered in the corners of the block unless there is no border on that edge.
31+
/// If the block is too small and multiple titles overlap, the border may get cut off at a corner.
32+
///
33+
/// ```plain
34+
/// ┌With at least a left border───
35+
///
36+
/// Without left border───
37+
/// ```
38+
///
2539
/// # Examples
2640
///
2741
/// ```
@@ -228,6 +242,62 @@ impl<'a> Block<'a> {
228242
self
229243
}
230244

245+
/// Adds a title to the top of the block.
246+
///
247+
/// You can provide any type that can be converted into [`Line`] including: strings, string
248+
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](crate::text::Span), or vectors of
249+
/// [spans](crate::text::Span) (`Vec<Span>`).
250+
///
251+
/// # Example
252+
///
253+
/// ```
254+
/// # use ratatui::{ prelude::*, widgets::* };
255+
/// Block::bordered()
256+
/// .title_top("Left1") // By default in the top left corner
257+
/// .title_top(Line::from("Left2").left_aligned())
258+
/// .title_top(Line::from("Right").right_aligned())
259+
/// .title_top(Line::from("Center").centered());
260+
///
261+
/// // Renders
262+
/// // ┌Left1─Left2───Center─────────Right┐
263+
/// // │ │
264+
/// // └──────────────────────────────────┘
265+
/// ```
266+
#[must_use = "method moves the value of self and returns the modified value"]
267+
pub fn title_top<T: Into<Line<'a>>>(mut self, title: T) -> Self {
268+
let title = Title::from(title).position(Position::Top);
269+
self.titles.push(title);
270+
self
271+
}
272+
273+
/// Adds a title to the bottom of the block.
274+
///
275+
/// You can provide any type that can be converted into [`Line`] including: strings, string
276+
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](crate::text::Span), or vectors of
277+
/// [spans](crate::text::Span) (`Vec<Span>`).
278+
///
279+
/// # Example
280+
///
281+
/// ```
282+
/// # use ratatui::{ prelude::*, widgets::* };
283+
/// Block::bordered()
284+
/// .title_bottom("Left1") // By default in the top left corner
285+
/// .title_bottom(Line::from("Left2").left_aligned())
286+
/// .title_bottom(Line::from("Right").right_aligned())
287+
/// .title_bottom(Line::from("Center").centered());
288+
///
289+
/// // Renders
290+
/// // ┌──────────────────────────────────┐
291+
/// // │ │
292+
/// // └Left1─Left2───Center─────────Right┘
293+
/// ```
294+
#[must_use = "method moves the value of self and returns the modified value"]
295+
pub fn title_bottom<T: Into<Line<'a>>>(mut self, title: T) -> Self {
296+
let title = Title::from(title).position(Position::Bottom);
297+
self.titles.push(title);
298+
self
299+
}
300+
231301
/// Applies the style to all titles.
232302
///
233303
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
@@ -655,6 +725,7 @@ impl Block<'_> {
655725
.right()
656726
.saturating_sub(title_width)
657727
.max(titles_area.left()),
728+
width: title_width.min(titles_area.width),
658729
..titles_area
659730
};
660731
buf.set_style(title_area, self.titles_style);
@@ -1130,6 +1201,50 @@ mod tests {
11301201
)
11311202
}
11321203

1204+
#[test]
1205+
fn title() {
1206+
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
1207+
use Alignment::*;
1208+
use Position::*;
1209+
Block::bordered()
1210+
.title(Title::from("A").position(Top).alignment(Left))
1211+
.title(Title::from("B").position(Top).alignment(Center))
1212+
.title(Title::from("C").position(Top).alignment(Right))
1213+
.title(Title::from("D").position(Bottom).alignment(Left))
1214+
.title(Title::from("E").position(Bottom).alignment(Center))
1215+
.title(Title::from("F").position(Bottom).alignment(Right))
1216+
.render(buffer.area, &mut buffer);
1217+
assert_buffer_eq!(
1218+
buffer,
1219+
Buffer::with_lines(vec![
1220+
"┌A─────B─────C┐",
1221+
"│ │",
1222+
"└D─────E─────F┘",
1223+
])
1224+
);
1225+
}
1226+
1227+
#[test]
1228+
fn title_top_bottom() {
1229+
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
1230+
Block::bordered()
1231+
.title_top(Line::raw("A").left_aligned())
1232+
.title_top(Line::raw("B").centered())
1233+
.title_top(Line::raw("C").right_aligned())
1234+
.title_bottom(Line::raw("D").left_aligned())
1235+
.title_bottom(Line::raw("E").centered())
1236+
.title_bottom(Line::raw("F").right_aligned())
1237+
.render(buffer.area, &mut buffer);
1238+
assert_buffer_eq!(
1239+
buffer,
1240+
Buffer::with_lines(vec![
1241+
"┌A─────B─────C┐",
1242+
"│ │",
1243+
"└D─────E─────F┘",
1244+
])
1245+
);
1246+
}
1247+
11331248
#[test]
11341249
fn title_alignment() {
11351250
let tests = vec![

src/widgets/block/title.rs

+29-2
Original file line numberDiff line numberDiff line change
@@ -115,18 +115,25 @@ where
115115
T: Into<Line<'a>>,
116116
{
117117
fn from(value: T) -> Self {
118-
Self::default().content(value.into())
118+
let content = value.into();
119+
let alignment = content.alignment;
120+
Self {
121+
content,
122+
alignment,
123+
position: None,
124+
}
119125
}
120126
}
121127

122128
#[cfg(test)]
123129
mod tests {
130+
use rstest::rstest;
124131
use strum::ParseError;
125132

126133
use super::*;
127134

128135
#[test]
129-
fn position_tostring() {
136+
fn position_to_string() {
130137
assert_eq!(Position::Top.to_string(), "Top");
131138
assert_eq!(Position::Bottom.to_string(), "Bottom");
132139
}
@@ -137,4 +144,24 @@ mod tests {
137144
assert_eq!("Bottom".parse::<Position>(), Ok(Position::Bottom));
138145
assert_eq!("".parse::<Position>(), Err(ParseError::VariantNotFound));
139146
}
147+
148+
#[test]
149+
fn title_from_line() {
150+
let title = Title::from(Line::raw("Title"));
151+
assert_eq!(title.content, Line::from("Title"));
152+
assert_eq!(title.alignment, None);
153+
assert_eq!(title.position, None);
154+
}
155+
156+
#[rstest]
157+
#[case::left(Alignment::Left)]
158+
#[case::center(Alignment::Center)]
159+
#[case::right(Alignment::Right)]
160+
fn title_from_line_with_alignment(#[case] alignment: Alignment) {
161+
let line = Line::raw("Title").alignment(alignment);
162+
let title = Title::from(line.clone());
163+
assert_eq!(title.content, line);
164+
assert_eq!(title.alignment, Some(alignment));
165+
assert_eq!(title.position, None);
166+
}
140167
}

0 commit comments

Comments
 (0)