mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-10 07:04:17 +00:00
feat(text): add style and alignment (#807)
Fixes #758, fixes #801 This PR adds: - `style` and `alignment` to `Text` - impl `Widget` for `Text` - replace `Text` manual draw to call for Widget impl All places that use `Text` have been updated and support its new features expect paragraph which still has a custom implementation.
This commit is contained in:
parent
d49bbb2590
commit
68d5783a69
7 changed files with 342 additions and 114 deletions
|
@ -207,7 +207,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let mut lines = vec![Line::from(i.0)];
|
let mut lines = vec![Line::from(i.0.bold()).alignment(Alignment::Center)];
|
||||||
for _ in 0..i.1 {
|
for _ in 0..i.1 {
|
||||||
lines.push(
|
lines.push(
|
||||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||||
|
|
|
@ -269,11 +269,11 @@ impl<'a> Line<'a> {
|
||||||
.flat_map(move |span| span.styled_graphemes(style))
|
.flat_map(move |span| span.styled_graphemes(style))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Patches the style of each Span in an existing Line, adding modifiers from the given style.
|
/// Patches the style of this Line, adding modifiers from the given style.
|
||||||
///
|
///
|
||||||
/// This is useful for when you want to apply a style to a line that already has some styling.
|
/// This is useful for when you want to apply a style to a line that already has some styling.
|
||||||
/// In contrast to [`Line::style`], this method will not overwrite the existing style, but
|
/// In contrast to [`Line::style`], this method will not overwrite the existing style, but
|
||||||
/// instead will add the given style's modifiers to the existing style of each `Span`.
|
/// instead will add the given style's modifiers to this Line's style.
|
||||||
///
|
///
|
||||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||||
/// your own type that implements [`Into<Style>`]).
|
/// your own type that implements [`Into<Style>`]).
|
||||||
|
@ -296,7 +296,7 @@ impl<'a> Line<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resets the style of each Span in the Line.
|
/// Resets the style of this Line.
|
||||||
///
|
///
|
||||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||||
///
|
///
|
||||||
|
@ -369,7 +369,7 @@ impl Widget for Line<'_> {
|
||||||
let span_width = span.width() as u16;
|
let span_width = span.width() as u16;
|
||||||
let span_area = Rect {
|
let span_area = Rect {
|
||||||
x,
|
x,
|
||||||
width: span_width,
|
width: span_width.min(area.right() - x),
|
||||||
..area
|
..area
|
||||||
};
|
};
|
||||||
span.render(span_area, buf);
|
span.render(span_area, buf);
|
||||||
|
@ -635,11 +635,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_truncates() {
|
fn render_truncates() {
|
||||||
let mut buf = Buffer::empty(Rect::new(0, 0, 11, 1));
|
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
|
||||||
hello_world().render(Rect::new(0, 0, 11, 1), &mut buf);
|
Line::from("Hello world!").render(Rect::new(0, 0, 5, 1), &mut buf);
|
||||||
let mut expected = Buffer::with_lines(vec!["Hello world"]);
|
let expected = Buffer::with_lines(vec!["Hello "]);
|
||||||
expected.set_style(Rect::new(0, 0, 6, 1), BLUE.italic());
|
|
||||||
expected.set_style(Rect::new(6, 0, 5, 1), GREEN.italic());
|
|
||||||
assert_buffer_eq!(buf, expected);
|
assert_buffer_eq!(buf, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -493,11 +493,13 @@ mod tests {
|
||||||
fn render_truncates_too_long_content() {
|
fn render_truncates_too_long_content() {
|
||||||
let style = Style::new().green().on_yellow();
|
let style = Style::new().green().on_yellow();
|
||||||
let span = Span::styled("test content", style);
|
let span = Span::styled("test content", style);
|
||||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
|
|
||||||
span.render(buf.area, &mut buf);
|
|
||||||
|
|
||||||
let expected =
|
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
|
||||||
Buffer::with_lines(vec![Line::from(vec!["test conte".green().on_yellow()])]);
|
span.render(Rect::new(0, 0, 5, 1), &mut buf);
|
||||||
|
|
||||||
|
let mut expected = Buffer::with_lines(vec![Line::from("test ")]);
|
||||||
|
expected.set_style(Rect::new(0, 0, 5, 1), (Color::Green, Color::Yellow));
|
||||||
|
|
||||||
assert_buffer_eq!(buf, expected);
|
assert_buffer_eq!(buf, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
324
src/text/text.rs
324
src/text/text.rs
|
@ -1,16 +1,32 @@
|
||||||
|
#![warn(missing_docs)]
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use itertools::{Itertools, Position};
|
use itertools::{Itertools, Position};
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::{prelude::*, widgets::Widget};
|
||||||
|
|
||||||
/// A string split over multiple lines where each line is composed of several clusters, each with
|
/// A string split over multiple lines where each line is composed of several clusters, each with
|
||||||
/// their own style.
|
/// their own style.
|
||||||
///
|
///
|
||||||
/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
|
/// A [`Text`], like a [`Line`], can be constructed using one of the many `From` implementations
|
||||||
/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
|
/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
|
||||||
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
|
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
|
||||||
///
|
///
|
||||||
|
/// The text's [`Style`] is used by the rendering widget to determine how to style the text. Each
|
||||||
|
/// [`Line`] in the text will be styled with the [`Style`] of the text, and then with its own
|
||||||
|
/// [`Style`]. `Text` also implements [`Styled`] which means you can use the methods of the
|
||||||
|
/// [`Stylize`] trait.
|
||||||
|
///
|
||||||
|
/// The text's [`Alignment`] can be set using [`Text::alignment`]. Lines composing the text can
|
||||||
|
/// also be individually aligned with [`Line::alignment`].
|
||||||
|
///
|
||||||
|
/// `Text` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`].
|
||||||
|
/// Usually apps will use the [`Paragraph`] widget instead of rendering a `Text` directly as it
|
||||||
|
/// provides more functionality.
|
||||||
|
///
|
||||||
|
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||||
|
/// [`Widget`]: crate::widgets::Widget
|
||||||
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use ratatui::prelude::*;
|
/// use ratatui::prelude::*;
|
||||||
///
|
///
|
||||||
|
@ -32,7 +48,12 @@ use crate::prelude::*;
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||||
pub struct Text<'a> {
|
pub struct Text<'a> {
|
||||||
|
/// The lines that make up this piece of text.
|
||||||
pub lines: Vec<Line<'a>>,
|
pub lines: Vec<Line<'a>>,
|
||||||
|
/// The style of this text.
|
||||||
|
pub style: Style,
|
||||||
|
/// The alignment of this text.
|
||||||
|
pub alignment: Option<Alignment>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Text<'a> {
|
impl<'a> Text<'a> {
|
||||||
|
@ -108,7 +129,36 @@ impl<'a> Text<'a> {
|
||||||
self.lines.len()
|
self.lines.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Patches the style of each line in an existing Text, adding modifiers from the given style.
|
/// Sets the style of this text.
|
||||||
|
///
|
||||||
|
/// Defaults to [`Style::default()`].
|
||||||
|
///
|
||||||
|
/// Note: This field was added in v0.26.0. Prior to that, the style of a text was determined
|
||||||
|
/// only by the style of each [`Line`] contained in the line. For this reason, this field may
|
||||||
|
/// not be supported by all widgets (outside of the `ratatui` crate itself).
|
||||||
|
///
|
||||||
|
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||||
|
/// your own type that implements [`Into<Style>`]).
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// # use ratatui::prelude::*;
|
||||||
|
/// let mut line = Text::from("foo").style(Style::new().red());
|
||||||
|
/// ```
|
||||||
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
|
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||||
|
self.style = style.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Patches the style of this Text, adding modifiers from the given style.
|
||||||
|
///
|
||||||
|
/// This is useful for when you want to apply a style to a text that already has some styling.
|
||||||
|
/// In contrast to [`Text::style`], this method will not overwrite the existing style, but
|
||||||
|
/// instead will add the given style's modifiers to this text's style.
|
||||||
|
///
|
||||||
|
/// `Text` also implements [`Styled`] which means you can use the methods of the [`Stylize`]
|
||||||
|
/// trait.
|
||||||
///
|
///
|
||||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||||
/// your own type that implements [`Into<Style>`]).
|
/// your own type that implements [`Into<Style>`]).
|
||||||
|
@ -119,7 +169,7 @@ impl<'a> Text<'a> {
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use ratatui::prelude::*;
|
/// # use ratatui::prelude::*;
|
||||||
/// let mut raw_text = Text::styled("The first line\nThe second line", Modifier::ITALIC);
|
/// let raw_text = Text::styled("The first line\nThe second line", Modifier::ITALIC);
|
||||||
/// let styled_text = Text::styled(
|
/// let styled_text = Text::styled(
|
||||||
/// String::from("The first line\nThe second line"),
|
/// String::from("The first line\nThe second line"),
|
||||||
/// (Color::Yellow, Modifier::ITALIC),
|
/// (Color::Yellow, Modifier::ITALIC),
|
||||||
|
@ -131,17 +181,13 @@ impl<'a> Text<'a> {
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use = "method moves the value of self and returns the modified value"]
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
|
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||||
let style = style.into();
|
self.style = self.style.patch(style);
|
||||||
self.lines = self
|
|
||||||
.lines
|
|
||||||
.into_iter()
|
|
||||||
.map(|line| line.patch_style(style))
|
|
||||||
.collect();
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resets the style of the Text.
|
/// Resets the style of the Text.
|
||||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
///
|
||||||
|
/// Equivalent to calling [`patch_style(Style::reset())`](Text::patch_style).
|
||||||
///
|
///
|
||||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||||
///
|
///
|
||||||
|
@ -149,24 +195,66 @@ impl<'a> Text<'a> {
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use ratatui::prelude::*;
|
/// # use ratatui::prelude::*;
|
||||||
/// let style = Style::default()
|
/// let text = Text::styled(
|
||||||
/// .fg(Color::Yellow)
|
/// "The first line\nThe second line",
|
||||||
/// .add_modifier(Modifier::ITALIC);
|
/// (Color::Yellow, Modifier::ITALIC),
|
||||||
/// let text = Text::styled("The first line\nThe second line", style);
|
/// );
|
||||||
///
|
///
|
||||||
/// let text = text.reset_style();
|
/// let text = text.reset_style();
|
||||||
/// for line in &text.lines {
|
/// assert_eq!(Style::reset(), text.style);
|
||||||
/// assert_eq!(Style::reset(), line.style);
|
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use = "method moves the value of self and returns the modified value"]
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
pub fn reset_style(mut self) -> Self {
|
pub fn reset_style(self) -> Self {
|
||||||
self.lines = self
|
self.patch_style(Style::reset())
|
||||||
.lines
|
}
|
||||||
.into_iter()
|
|
||||||
.map(|line| line.reset_style())
|
/// Sets the alignment for this text.
|
||||||
.collect();
|
///
|
||||||
self
|
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
|
||||||
|
///
|
||||||
|
/// Alignment can be set individually on each line to override this text's alignment.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// Set alignment to the whole text.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use ratatui::prelude::*;
|
||||||
|
/// let mut text = Text::from("Hi, what's up?");
|
||||||
|
/// assert_eq!(None, text.alignment);
|
||||||
|
/// assert_eq!(
|
||||||
|
/// Some(Alignment::Right),
|
||||||
|
/// text.alignment(Alignment::Right).alignment
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Set a default alignment and override it on a per line basis.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use ratatui::prelude::*;
|
||||||
|
/// let text = Text::from(vec![
|
||||||
|
/// Line::from("left").alignment(Alignment::Left),
|
||||||
|
/// Line::from("default"),
|
||||||
|
/// Line::from("default"),
|
||||||
|
/// Line::from("right").alignment(Alignment::Right),
|
||||||
|
/// ])
|
||||||
|
/// .alignment(Alignment::Center);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Will render the following
|
||||||
|
///
|
||||||
|
/// ```plain
|
||||||
|
/// left
|
||||||
|
/// default
|
||||||
|
/// default
|
||||||
|
/// right
|
||||||
|
/// ```
|
||||||
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
|
pub fn alignment(self, alignment: Alignment) -> Self {
|
||||||
|
Self {
|
||||||
|
alignment: Some(alignment),
|
||||||
|
..self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,19 +280,26 @@ impl<'a> From<Span<'a>> for Text<'a> {
|
||||||
fn from(span: Span<'a>) -> Text<'a> {
|
fn from(span: Span<'a>) -> Text<'a> {
|
||||||
Text {
|
Text {
|
||||||
lines: vec![Line::from(span)],
|
lines: vec![Line::from(span)],
|
||||||
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<Line<'a>> for Text<'a> {
|
impl<'a> From<Line<'a>> for Text<'a> {
|
||||||
fn from(line: Line<'a>) -> Text<'a> {
|
fn from(line: Line<'a>) -> Text<'a> {
|
||||||
Text { lines: vec![line] }
|
Text {
|
||||||
|
lines: vec![line],
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<Vec<Line<'a>>> for Text<'a> {
|
impl<'a> From<Vec<Line<'a>>> for Text<'a> {
|
||||||
fn from(lines: Vec<Line<'a>>) -> Text<'a> {
|
fn from(lines: Vec<Line<'a>>) -> Text<'a> {
|
||||||
Text { lines }
|
Text {
|
||||||
|
lines,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,6 +335,42 @@ impl std::fmt::Display for Text<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for Text<'a> {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
buf.set_style(area, self.style);
|
||||||
|
for (line, row) in self.lines.into_iter().zip(area.rows()) {
|
||||||
|
let line_width = line.width() as u16;
|
||||||
|
|
||||||
|
let x_offset = match (self.alignment, line.alignment) {
|
||||||
|
(Some(Alignment::Center), None) => area.width.saturating_sub(line_width) / 2,
|
||||||
|
(Some(Alignment::Right), None) => area.width.saturating_sub(line_width),
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let line_area = Rect {
|
||||||
|
x: area.x + x_offset,
|
||||||
|
y: row.y,
|
||||||
|
width: area.width - x_offset,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
line.render(line_area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Styled for Text<'a> {
|
||||||
|
type Item = Text<'a>;
|
||||||
|
|
||||||
|
fn style(&self) -> Style {
|
||||||
|
self.style
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||||
|
self.style(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -257,14 +388,12 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn styled() {
|
fn styled() {
|
||||||
let style = Style::new().yellow().italic();
|
let style = Style::new().yellow().italic();
|
||||||
let text = Text::styled("The first line\nThe second line", style);
|
let styled_text = Text::styled("The first line\nThe second line", style);
|
||||||
assert_eq!(
|
|
||||||
text.lines,
|
let mut text = Text::raw("The first line\nThe second line");
|
||||||
vec![
|
text.style = style;
|
||||||
Line::styled("The first line", style),
|
|
||||||
Line::styled("The second line", style)
|
assert_eq!(styled_text, text);
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -286,13 +415,9 @@ mod tests {
|
||||||
let text = Text::styled("The first line\nThe second line", style).patch_style(style2);
|
let text = Text::styled("The first line\nThe second line", style).patch_style(style2);
|
||||||
|
|
||||||
let expected_style = Style::new().red().italic().underlined();
|
let expected_style = Style::new().red().italic().underlined();
|
||||||
assert_eq!(
|
let expected_text = Text::styled("The first line\nThe second line", expected_style);
|
||||||
text.lines,
|
|
||||||
vec![
|
assert_eq!(text, expected_text);
|
||||||
Line::styled("The first line", expected_style),
|
|
||||||
Line::styled("The second line", expected_style)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -300,13 +425,7 @@ mod tests {
|
||||||
let style = Style::new().yellow().italic();
|
let style = Style::new().yellow().italic();
|
||||||
let text = Text::styled("The first line\nThe second line", style).reset_style();
|
let text = Text::styled("The first line\nThe second line", style).reset_style();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(text.style, Style::reset());
|
||||||
text.lines,
|
|
||||||
vec![
|
|
||||||
Line::styled("The first line", Style::reset()),
|
|
||||||
Line::styled("The second line", Style::reset())
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -473,4 +592,111 @@ mod tests {
|
||||||
"The first line\nThe second line\nThe third line\nThe fourth line"
|
"The first line\nThe second line\nThe third line\nThe fourth line"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stylize() {
|
||||||
|
assert_eq!(Text::default().green().style, Color::Green.into());
|
||||||
|
assert_eq!(
|
||||||
|
Text::default().on_green().style,
|
||||||
|
Style::new().bg(Color::Green)
|
||||||
|
);
|
||||||
|
assert_eq!(Text::default().italic().style, Modifier::ITALIC.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
mod widget {
|
||||||
|
use super::*;
|
||||||
|
use crate::{assert_buffer_eq, style::Color};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render() {
|
||||||
|
let text = Text::from("foo");
|
||||||
|
|
||||||
|
let area = Rect::new(0, 0, 5, 1);
|
||||||
|
let mut buf = Buffer::empty(area);
|
||||||
|
text.render(area, &mut buf);
|
||||||
|
|
||||||
|
let expected_buf = Buffer::with_lines(vec!["foo "]);
|
||||||
|
|
||||||
|
assert_buffer_eq!(buf, expected_buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_right_aligned() {
|
||||||
|
let text = Text::from("foo").alignment(Alignment::Right);
|
||||||
|
|
||||||
|
let area = Rect::new(0, 0, 5, 1);
|
||||||
|
let mut buf = Buffer::empty(area);
|
||||||
|
text.render(area, &mut buf);
|
||||||
|
|
||||||
|
let expected_buf = Buffer::with_lines(vec![" foo"]);
|
||||||
|
|
||||||
|
assert_buffer_eq!(buf, expected_buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_centered_odd() {
|
||||||
|
let text = Text::from("foo").alignment(Alignment::Center);
|
||||||
|
|
||||||
|
let area = Rect::new(0, 0, 5, 1);
|
||||||
|
let mut buf = Buffer::empty(area);
|
||||||
|
text.render(area, &mut buf);
|
||||||
|
|
||||||
|
let expected_buf = Buffer::with_lines(vec![" foo "]);
|
||||||
|
|
||||||
|
assert_buffer_eq!(buf, expected_buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_centered_even() {
|
||||||
|
let text = Text::from("foo").alignment(Alignment::Center);
|
||||||
|
|
||||||
|
let area = Rect::new(0, 0, 6, 1);
|
||||||
|
let mut buf = Buffer::empty(area);
|
||||||
|
text.render(area, &mut buf);
|
||||||
|
|
||||||
|
let expected_buf = Buffer::with_lines(vec![" foo "]);
|
||||||
|
|
||||||
|
assert_buffer_eq!(buf, expected_buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_one_line_right() {
|
||||||
|
let text = Text::from(vec![
|
||||||
|
"foo".into(),
|
||||||
|
Line::from("bar").alignment(Alignment::Center),
|
||||||
|
])
|
||||||
|
.alignment(Alignment::Right);
|
||||||
|
|
||||||
|
let area = Rect::new(0, 0, 5, 2);
|
||||||
|
let mut buf = Buffer::empty(area);
|
||||||
|
text.render(area, &mut buf);
|
||||||
|
|
||||||
|
let expected_buf = Buffer::with_lines(vec![" foo", " bar "]);
|
||||||
|
|
||||||
|
assert_buffer_eq!(buf, expected_buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_only_styles_line_area() {
|
||||||
|
let area = Rect::new(0, 0, 5, 1);
|
||||||
|
let mut buf = Buffer::empty(area);
|
||||||
|
Text::from("foo".on_blue()).render(area, &mut buf);
|
||||||
|
|
||||||
|
let mut expected = Buffer::with_lines(vec!["foo "]);
|
||||||
|
expected.set_style(Rect::new(0, 0, 3, 1), Style::new().bg(Color::Blue));
|
||||||
|
|
||||||
|
assert_buffer_eq!(buf, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_truncates() {
|
||||||
|
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
|
||||||
|
Text::from("foobar".on_blue()).render(Rect::new(0, 0, 3, 1), &mut buf);
|
||||||
|
|
||||||
|
let mut expected = Buffer::with_lines(vec!["foo "]);
|
||||||
|
expected.set_style(Rect::new(0, 0, 3, 1), Style::new().bg(Color::Blue));
|
||||||
|
|
||||||
|
assert_buffer_eq!(buf, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -169,6 +169,10 @@ impl ListState {
|
||||||
/// This [`Style`] will be combined with the [`Style`] of the inner [`Text`]. The [`Style`]
|
/// This [`Style`] will be combined with the [`Style`] of the inner [`Text`]. The [`Style`]
|
||||||
/// of the [`Text`] will be added to the [`Style`] of the [`ListItem`].
|
/// of the [`Text`] will be added to the [`Style`] of the [`ListItem`].
|
||||||
///
|
///
|
||||||
|
/// You can also align a `ListItem` by aligning its underlying [`Text`] and [`Line`]s. For that,
|
||||||
|
/// see [`Text::alignment`] and [`Line::alignment`]. On a multiline `Text`, one `Line` can override
|
||||||
|
/// the alignment by setting it explicitly.
|
||||||
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// You can create [`ListItem`]s from simple `&str`
|
/// You can create [`ListItem`]s from simple `&str`
|
||||||
|
@ -203,6 +207,13 @@ impl ListState {
|
||||||
/// let item = ListItem::new(text);
|
/// let item = ListItem::new(text);
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
|
/// A right-aligned `ListItem`
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use ratatui::{prelude::*, widgets::*};
|
||||||
|
/// ListItem::new(Text::from("foo").alignment(Alignment::Right));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
/// [`Stylize`]: crate::style::Stylize
|
/// [`Stylize`]: crate::style::Stylize
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
pub struct ListItem<'a> {
|
pub struct ListItem<'a> {
|
||||||
|
@ -338,12 +349,14 @@ where
|
||||||
///
|
///
|
||||||
/// A list is a collection of [`ListItem`]s.
|
/// A list is a collection of [`ListItem`]s.
|
||||||
///
|
///
|
||||||
/// This is different from a [`Table`] because it does not handle columns or headers and the item's
|
/// This is different from a [`Table`] because it does not handle columns, headers or footers and
|
||||||
/// height is automatically determined. A `List` can also be put in reverse order (i.e. *bottom to
|
/// the item's height is automatically determined. A `List` can also be put in reverse order (i.e.
|
||||||
/// top*) whereas a [`Table`] cannot.
|
/// *bottom to top*) whereas a [`Table`] cannot.
|
||||||
///
|
///
|
||||||
/// [`Table`]: crate::widgets::Table
|
/// [`Table`]: crate::widgets::Table
|
||||||
///
|
///
|
||||||
|
/// List items can be aligned using [`Text::alignment`], for more details see [`ListItem`].
|
||||||
|
///
|
||||||
/// [`List`] implements [`Widget`] and so it can be drawn using
|
/// [`List`] implements [`Widget`] and so it can be drawn using
|
||||||
/// [`Frame::render_widget`](crate::terminal::Frame::render_widget).
|
/// [`Frame::render_widget`](crate::terminal::Frame::render_widget).
|
||||||
///
|
///
|
||||||
|
@ -810,17 +823,32 @@ impl<'a> StatefulWidget for List<'a> {
|
||||||
current_height += item.height() as u16;
|
current_height += item.height() as u16;
|
||||||
pos
|
pos
|
||||||
};
|
};
|
||||||
let area = Rect {
|
|
||||||
|
let row_area = Rect {
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
width: list_area.width,
|
width: list_area.width,
|
||||||
height: item.height() as u16,
|
height: item.height() as u16,
|
||||||
};
|
};
|
||||||
|
|
||||||
let item_style = self.style.patch(item.style);
|
let item_style = self.style.patch(item.style);
|
||||||
buf.set_style(area, item_style);
|
buf.set_style(row_area, item_style);
|
||||||
|
|
||||||
let is_selected = state.selected.map_or(false, |s| s == i);
|
let is_selected = state.selected.map_or(false, |s| s == i);
|
||||||
for (j, line) in item.content.lines.iter().enumerate() {
|
|
||||||
|
let item_area = if selection_spacing {
|
||||||
|
let highlight_symbol_width = self.highlight_symbol.unwrap_or("").len() as u16;
|
||||||
|
Rect {
|
||||||
|
x: row_area.x + highlight_symbol_width,
|
||||||
|
width: row_area.width - highlight_symbol_width,
|
||||||
|
..row_area
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
row_area
|
||||||
|
};
|
||||||
|
item.content.clone().render(item_area, buf);
|
||||||
|
|
||||||
|
for j in 0..item.content.height() {
|
||||||
// if the item is selected, we need to display the highlight symbol:
|
// if the item is selected, we need to display the highlight symbol:
|
||||||
// - either for the first line of the item only,
|
// - either for the first line of the item only,
|
||||||
// - or for each line of the item if the appropriate option is set
|
// - or for each line of the item if the appropriate option is set
|
||||||
|
@ -829,29 +857,19 @@ impl<'a> StatefulWidget for List<'a> {
|
||||||
} else {
|
} else {
|
||||||
&blank_symbol
|
&blank_symbol
|
||||||
};
|
};
|
||||||
let (elem_x, max_element_width) = if selection_spacing {
|
if selection_spacing {
|
||||||
let (elem_x, _) = buf.set_stringn(
|
buf.set_stringn(
|
||||||
x,
|
x,
|
||||||
y + j as u16,
|
y + j as u16,
|
||||||
symbol,
|
symbol,
|
||||||
list_area.width as usize,
|
list_area.width as usize,
|
||||||
item_style,
|
item_style,
|
||||||
);
|
);
|
||||||
(elem_x, (list_area.width - (elem_x - x)))
|
}
|
||||||
} else {
|
|
||||||
(x, list_area.width)
|
|
||||||
};
|
|
||||||
let x_offset = match line.alignment {
|
|
||||||
Some(Alignment::Center) => {
|
|
||||||
(area.width / 2).saturating_sub(line.width() as u16 / 2)
|
|
||||||
}
|
|
||||||
Some(Alignment::Right) => area.width.saturating_sub(line.width() as u16),
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
buf.set_line(elem_x + x_offset, y + j as u16, line, max_element_width);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_selected {
|
if is_selected {
|
||||||
buf.set_style(area, self.highlight_style);
|
buf.set_style(row_area, self.highlight_style);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1845,14 +1863,11 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_render_list_alignment_line_less_than_width() {
|
fn test_render_list_alignment_line_less_than_width() {
|
||||||
let items = [Line::from("Small").alignment(Alignment::Center)]
|
let items = [Line::from("Small").alignment(Alignment::Center)];
|
||||||
.into_iter()
|
|
||||||
.map(ListItem::new)
|
|
||||||
.collect::<Vec<ListItem>>();
|
|
||||||
let list = List::new(items);
|
let list = List::new(items);
|
||||||
let buffer = render_widget(list, 10, 5);
|
let buffer = render_widget(list, 10, 5);
|
||||||
let expected = Buffer::with_lines(vec![
|
let expected = Buffer::with_lines(vec![
|
||||||
" Small ",
|
" Small ",
|
||||||
" ",
|
" ",
|
||||||
" ",
|
" ",
|
||||||
" ",
|
" ",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::prelude::*;
|
use crate::{prelude::*, widgets::Widget};
|
||||||
|
|
||||||
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
|
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
|
||||||
///
|
///
|
||||||
|
@ -7,6 +7,8 @@ use crate::prelude::*;
|
||||||
/// [`Style`] of the [`Cell`] by adding the [`Style`] of the [`Text`] content to the [`Style`] of
|
/// [`Style`] of the [`Cell`] by adding the [`Style`] of the [`Text`] content to the [`Style`] of
|
||||||
/// the [`Cell`]. Styles set on the text content will only affect the content.
|
/// the [`Cell`]. Styles set on the text content will only affect the content.
|
||||||
///
|
///
|
||||||
|
/// You can use [`Text::alignment`] when creating a cell to align its content.
|
||||||
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// You can create a `Cell` from anything that can be converted to a [`Text`].
|
/// You can create a `Cell` from anything that can be converted to a [`Text`].
|
||||||
|
@ -132,24 +134,7 @@ impl<'a> Cell<'a> {
|
||||||
impl Cell<'_> {
|
impl Cell<'_> {
|
||||||
pub(crate) fn render(&self, area: Rect, buf: &mut Buffer) {
|
pub(crate) fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||||
buf.set_style(area, self.style);
|
buf.set_style(area, self.style);
|
||||||
for (i, line) in self.content.lines.iter().enumerate() {
|
self.content.clone().render(area, buf);
|
||||||
if i as u16 >= area.height {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let x_offset = match line.alignment {
|
|
||||||
Some(Alignment::Center) => (area.width / 2).saturating_sub(line.width() as u16 / 2),
|
|
||||||
Some(Alignment::Right) => area.width.saturating_sub(line.width() as u16),
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let x = area.x + x_offset;
|
|
||||||
if x >= area.right() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.set_line(x, area.y + i as u16, line, area.width);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,8 @@ use crate::{
|
||||||
/// You can construct a [`Table`] using either [`Table::new`] or [`Table::default`] and then chain
|
/// You can construct a [`Table`] using either [`Table::new`] or [`Table::default`] and then chain
|
||||||
/// builder style methods to set the desired properties.
|
/// builder style methods to set the desired properties.
|
||||||
///
|
///
|
||||||
|
/// Table cells can be aligned, for more details see [`Cell`].
|
||||||
|
///
|
||||||
/// Make sure to call the [`Table::widths`] method, otherwise the columns will all have a width of 0
|
/// Make sure to call the [`Table::widths`] method, otherwise the columns will all have a width of 0
|
||||||
/// and thus not be visible.
|
/// and thus not be visible.
|
||||||
///
|
///
|
||||||
|
@ -691,12 +693,12 @@ impl Table<'_> {
|
||||||
|
|
||||||
let is_selected = state.selected().is_some_and(|index| index == i);
|
let is_selected = state.selected().is_some_and(|index| index == i);
|
||||||
if selection_width > 0 && is_selected {
|
if selection_width > 0 && is_selected {
|
||||||
// this should in normal cases be safe, because "get_columns_widths" allocates
|
let selection_area = Rect {
|
||||||
// "highlight_symbol.width()" space but "get_columns_widths"
|
width: selection_width,
|
||||||
// currently does not bind it to max table.width()
|
..row_area
|
||||||
for (line, line_row) in highlight_symbol.lines.iter().zip(row_area.rows()) {
|
};
|
||||||
line.clone().style(row.style).render(line_row, buf);
|
buf.set_style(selection_area, row.style);
|
||||||
}
|
highlight_symbol.clone().render(selection_area, buf);
|
||||||
};
|
};
|
||||||
for ((x, width), cell) in columns_widths.iter().zip(row.cells.iter()) {
|
for ((x, width), cell) in columns_widths.iter().zip(row.cells.iter()) {
|
||||||
cell.render(
|
cell.render(
|
||||||
|
|
Loading…
Reference in a new issue