refactor(text): replace Spans with Line (#178)

* refactor: add Line type to replace Spans

`Line` is a significantly better name over `Spans` as the plural causes
confusion and the type really is a representation of a line of text made
up of spans.

This is a backwards compatible version of the approach from
https://github.com/tui-rs-revival/ratatui/pull/175

There is a significant amount of code that uses the Spans type and
methods, so instead of just renaming it, we add a new type and replace
parameters that accepts a `Spans` with a parameter that accepts
`Into<Line>`.

Note that the examples have been intentionally left using `Spans` in
this commit to demonstrate the compiler warnings that will be emitted in
existing code.

Implementation notes:
- moves the Spans code to text::spans and publicly reexports on the text
module. This makes the test in that module only relevant to the Spans
type.
- adds a line module with a copy of the code and tests from Spans with a
single addition: `impl<'a> From<Spans<'a>> for Line<'a>`
- adds tests for `Spans` (created and checked before refactoring)
- adds the same tests for `Line`
- updates all widget methods that accept and store Spans to instead
store `Line` and accept `Into<Line>`

* refactor: move text::Masked to text::masked::Masked

Re-exports the Masked type at text::Masked

* refactor: replace Spans with Line in tests/examples/docs
This commit is contained in:
Josh McKinney 2023-05-18 11:21:43 -07:00 committed by GitHub
parent 4437835057
commit 728f82c084
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 757 additions and 385 deletions

View file

@ -4,8 +4,8 @@ use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Span, Spans},
widgets::canvas::{Canvas, Circle, Line, Map, MapResolution, Rectangle},
text::{Line, Span},
widgets::canvas::{Canvas, Circle, Line as CanvasLine, Map, MapResolution, Rectangle},
widgets::{
Axis, BarChart, Block, Borders, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem,
Paragraph, Row, Sparkline, Table, Tabs, Wrap,
@ -21,7 +21,7 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
.tabs
.titles
.iter()
.map(|t| Spans::from(Span::styled(*t, Style::default().fg(Color::Green))))
.map(|t| Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
.collect();
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL).title(app.title))
@ -137,7 +137,7 @@ where
.tasks
.items
.iter()
.map(|i| ListItem::new(vec![Spans::from(Span::raw(*i))]))
.map(|i| ListItem::new(vec![Line::from(Span::raw(*i))]))
.collect();
let tasks = List::new(tasks)
.block(Block::default().borders(Borders::ALL).title("List"))
@ -161,7 +161,7 @@ where
"WARNING" => warning_style,
_ => info_style,
};
let content = vec![Spans::from(vec![
let content = vec![Line::from(vec![
Span::styled(format!("{:<9}", level), s),
Span::raw(evt),
])];
@ -261,9 +261,9 @@ where
B: Backend,
{
let text = vec![
Spans::from("This is a paragraph with several lines. You can change style your text the way you want"),
Spans::from(""),
Spans::from(vec![
Line::from("This is a paragraph with several lines. You can change style your text the way you want"),
Line::from(""),
Line::from(vec![
Span::from("For example: "),
Span::styled("under", Style::default().fg(Color::Red)),
Span::raw(" "),
@ -272,7 +272,7 @@ where
Span::styled("rainbow", Style::default().fg(Color::Blue)),
Span::raw("."),
]),
Spans::from(vec![
Line::from(vec![
Span::raw("Oh and if you didn't "),
Span::styled("notice", Style::default().add_modifier(Modifier::ITALIC)),
Span::raw(" you can "),
@ -283,7 +283,7 @@ where
Span::styled("text", Style::default().add_modifier(Modifier::UNDERLINED)),
Span::raw(".")
]),
Spans::from(
Line::from(
"One more thing is that it should display unicode characters: 10€"
),
];
@ -354,7 +354,7 @@ where
});
for (i, s1) in app.servers.iter().enumerate() {
for s2 in &app.servers[i + 1..] {
ctx.draw(&Line {
ctx.draw(&CanvasLine {
x1: s1.coords.1,
y1: s1.coords.0,
y2: s2.coords.0,

View file

@ -4,7 +4,7 @@ use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Span, Spans},
text::{Line, Span},
widgets::{Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
Frame, Terminal, TerminalOptions, Viewport,
};
@ -193,7 +193,7 @@ fn run_app<B: Backend>(
Event::DownloadDone(worker_id, download_id) => {
let download = downloads.in_progress.remove(&worker_id).unwrap();
terminal.insert_before(1, |buf| {
Paragraph::new(Spans::from(vec![
Paragraph::new(Line::from(vec![
Span::from("Finished "),
Span::styled(
format!("download {}", download_id),
@ -254,7 +254,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, downloads: &Downloads) {
.in_progress
.values()
.map(|download| {
ListItem::new(Spans::from(vec![
ListItem::new(Line::from(vec![
Span::raw(symbols::DOT),
Span::styled(
format!(" download {:>2}", download.id),

View file

@ -7,7 +7,7 @@ use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Corner, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem, ListState},
Frame, Terminal,
};
@ -217,9 +217,9 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
.items
.iter()
.map(|i| {
let mut lines = vec![Spans::from(i.0)];
let mut lines = vec![Line::from(i.0)];
for _ in 0..i.1 {
lines.push(Spans::from(Span::styled(
lines.push(Line::from(Span::styled(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
Style::default().add_modifier(Modifier::ITALIC),
)));
@ -257,7 +257,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
_ => Style::default(),
};
// Add a example datetime and apply proper spacing between them
let header = Spans::from(vec![
let header = Line::from(vec![
Span::styled(format!("{:<9}", level), s),
Span::raw(" "),
Span::styled(
@ -266,7 +266,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
),
]);
// The event gets its own line
let log = Spans::from(vec![Span::raw(event)]);
let log = Line::from(vec![Span::raw(event)]);
// Here several things happen:
// 1. Add a `---` spacing line above the final list entry
@ -274,9 +274,9 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
// 3. Add a spacer line
// 4. Add the actual event
ListItem::new(vec![
Spans::from("-".repeat(chunks[1].width as usize)),
Line::from("-".repeat(chunks[1].width as usize)),
header,
Spans::from(""),
Line::from(""),
log,
])
})

View file

@ -26,7 +26,7 @@ use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
use ratatui::backend::{Backend, CrosstermBackend};
use ratatui::layout::Alignment;
use ratatui::text::Spans;
use ratatui::text::Line;
use ratatui::widgets::{Block, Borders, Paragraph};
use ratatui::{Frame, Terminal};
@ -113,23 +113,23 @@ fn run_tui<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let text = vec![
if app.hook_enabled {
Spans::from("HOOK IS CURRENTLY **ENABLED**")
Line::from("HOOK IS CURRENTLY **ENABLED**")
} else {
Spans::from("HOOK IS CURRENTLY **DISABLED**")
Line::from("HOOK IS CURRENTLY **DISABLED**")
},
Spans::from(""),
Spans::from("press `p` to panic"),
Spans::from("press `e` to enable the terminal-resetting panic hook"),
Spans::from("press any other key to quit without panic"),
Spans::from(""),
Spans::from("when you panic without the chained hook,"),
Spans::from("you will likely have to reset your terminal afterwards"),
Spans::from("with the `reset` command"),
Spans::from(""),
Spans::from("with the chained panic hook enabled,"),
Spans::from("you should see the panic report as you would without ratatui"),
Spans::from(""),
Spans::from("try first without the panic handler to see the difference"),
Line::from(""),
Line::from("press `p` to panic"),
Line::from("press `e` to enable the terminal-resetting panic hook"),
Line::from("press any other key to quit without panic"),
Line::from(""),
Line::from("when you panic without the chained hook,"),
Line::from("you will likely have to reset your terminal afterwards"),
Line::from("with the `reset` command"),
Line::from(""),
Line::from("with the chained panic hook enabled,"),
Line::from("you should see the panic report as you would without ratatui"),
Line::from(""),
Line::from("try first without the panic handler to see the difference"),
];
let b = Block::default()

View file

@ -7,7 +7,7 @@ use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Masked, Span, Spans},
text::{Line, Masked, Span},
widgets::{Block, Borders, Paragraph, Wrap},
Frame, Terminal,
};
@ -113,27 +113,27 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
.split(size);
let text = vec![
Spans::from("This is a line "),
Spans::from(Span::styled(
Line::from("This is a line "),
Line::from(Span::styled(
"This is a line ",
Style::default().fg(Color::Red),
)),
Spans::from(Span::styled(
Line::from(Span::styled(
"This is a line",
Style::default().bg(Color::Blue),
)),
Spans::from(Span::styled(
Line::from(Span::styled(
"This is a longer line",
Style::default().add_modifier(Modifier::CROSSED_OUT),
)),
Spans::from(Span::styled(&long_line, Style::default().bg(Color::Green))),
Spans::from(Span::styled(
Line::from(Span::styled(&long_line, Style::default().bg(Color::Green))),
Line::from(Span::styled(
"This is a line",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::ITALIC),
)),
Spans::from(vec![
Line::from(vec![
Span::raw("Masked text: "),
Span::styled(
Masked::new("password", '*'),

View file

@ -7,7 +7,7 @@ use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans},
text::{Line, Span},
widgets::{Block, Borders, Tabs},
Frame, Terminal,
};
@ -99,7 +99,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
.iter()
.map(|t| {
let (first, rest) = t.split_at(1);
Spans::from(vec![
Line::from(vec![
Span::styled(first, Style::default().fg(Color::Yellow)),
Span::styled(rest, Style::default().fg(Color::Green)),
])

View file

@ -18,7 +18,7 @@ use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans, Text},
text::{Line, Span, Text},
widgets::{Block, Borders, List, ListItem, Paragraph},
Frame, Terminal,
};
@ -150,7 +150,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
Style::default(),
),
};
let mut text = Text::from(Spans::from(msg));
let mut text = Text::from(Line::from(msg));
text.patch_style(style);
let help_message = Paragraph::new(text);
f.render_widget(help_message, chunks[0]);
@ -183,7 +183,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
.iter()
.enumerate()
.map(|(i, m)| {
let content = Spans::from(Span::raw(format!("{}: {}", i, m)));
let content = Line::from(Span::raw(format!("{}: {}", i, m)));
ListItem::new(content)
})
.collect();

View file

@ -1,7 +1,8 @@
#[allow(deprecated)]
use crate::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Span, Spans},
text::{Line, Span, Spans},
};
use std::{
cmp::min,
@ -309,6 +310,8 @@ impl Buffer {
(x_offset as u16, y)
}
#[allow(deprecated)]
#[deprecated(note = "Use `Buffer::set_line` instead")]
pub fn set_spans(&mut self, x: u16, y: u16, spans: &Spans<'_>, width: u16) -> (u16, u16) {
let mut remaining_width = width;
let mut x = x;
@ -330,6 +333,27 @@ impl Buffer {
(x, y)
}
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, width: u16) -> (u16, u16) {
let mut remaining_width = width;
let mut x = x;
for span in &line.spans {
if remaining_width == 0 {
break;
}
let pos = self.set_stringn(
x,
y,
span.content.as_ref(),
remaining_width as usize,
span.style,
);
let w = pos.0.saturating_sub(x);
x = pos.0;
remaining_width = remaining_width.saturating_sub(w);
}
(x, y)
}
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16) -> (u16, u16) {
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
}

View file

@ -386,14 +386,14 @@ where
///
/// ```rust
/// # use ratatui::widgets::{Paragraph, Widget};
/// # use ratatui::text::{Spans, Span};
/// # use ratatui::text::{Line, Span};
/// # use ratatui::style::{Color, Style};
/// # use ratatui::{Terminal};
/// # use ratatui::backend::TestBackend;
/// # let backend = TestBackend::new(10, 10);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// terminal.insert_before(1, |buf| {
/// Paragraph::new(Spans::from(vec![
/// Paragraph::new(Line::from(vec![
/// Span::raw("This line will be added "),
/// Span::styled("before", Style::default().fg(Color::Blue)),
/// Span::raw(" the current viewport")

View file

@ -3,12 +3,12 @@
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
//! those strings may be associated to a set of styles. `ratatui` has three ways to represent them:
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
//! - A single line string where each grapheme may have its own style is represented by [`Spans`].
//! - A single line string where each grapheme may have its own style is represented by [`Line`].
//! - A multiple line string where each grapheme may have its own style is represented by a
//! [`Text`].
//!
//! These types form a hierarchy: [`Spans`] is a collection of [`Span`] and each line of [`Text`]
//! is a [`Spans`].
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`]
//! is a [`Line`].
//!
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
//! supported for their properties. Moreover, `ratatui` provides convenient `From` implementations so
@ -16,20 +16,20 @@
//! primitives when you need additional styling capabilities.
//!
//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
//! its `title` property (which is a [`Spans`] under the hood):
//! its `title` property (which is a [`Line`] under the hood):
//!
//! ```rust
//! # use ratatui::widgets::Block;
//! # use ratatui::text::{Span, Spans};
//! # use ratatui::text::{Span, Line};
//! # use ratatui::style::{Color, Style};
//! // A simple string with no styling.
//! // Converted to Spans(vec![
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
//! // ])
//! let block = Block::default().title("My title");
//!
//! // A simple string with a unique style.
//! // Converted to Spans(vec![
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
//! // ])
//! let block = Block::default().title(
@ -37,7 +37,7 @@
//! );
//!
//! // A string with multiple styles.
//! // Converted to Spans(vec![
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
//! // Span { content: Cow::Borrowed(" title"), .. }
//! // ])
@ -47,11 +47,16 @@
//! ]);
//! ```
use crate::style::Style;
use std::borrow::Cow;
use std::fmt::{self, Debug, Display};
use std::{borrow::Cow, fmt::Debug};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
mod line;
mod masked;
mod spans;
#[allow(deprecated)]
pub use {line::Line, masked::Masked, spans::Spans};
/// A grapheme associated to a style.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StyledGrapheme<'a> {
@ -231,113 +236,6 @@ impl<'a> From<&'a str> for Span<'a> {
}
}
/// A string composed of clusters of graphemes, each with their own style.
#[derive(Debug, Clone, PartialEq, Default, Eq)]
pub struct Spans<'a>(pub Vec<Span<'a>>);
impl<'a> Spans<'a> {
/// Returns the width of the underlying string.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Spans};
/// # use ratatui::style::{Color, Style};
/// let spans = Spans::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::raw(" text"),
/// ]);
/// assert_eq!(7, spans.width());
/// ```
pub fn width(&self) -> usize {
self.0.iter().map(Span::width).sum()
}
/// Patches the style of each Span in an existing Spans, adding modifiers from the given style.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Spans};
/// # use ratatui::style::{Color, Style, Modifier};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut raw_spans = Spans::from(vec![
/// Span::raw("My"),
/// Span::raw(" text"),
/// ]);
/// let mut styled_spans = Spans::from(vec![
/// Span::styled("My", style),
/// Span::styled(" text", style),
/// ]);
///
/// assert_ne!(raw_spans, styled_spans);
///
/// raw_spans.patch_style(style);
/// assert_eq!(raw_spans, styled_spans);
/// ```
pub fn patch_style(&mut self, style: Style) {
for span in &mut self.0 {
span.patch_style(style);
}
}
/// Resets the style of each Span in the Spans.
/// Equivalent to calling `patch_style(Style::reset())`.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Spans};
/// # use ratatui::style::{Color, Style, Modifier};
/// let mut spans = Spans::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
/// ]);
///
/// spans.reset_style();
/// assert_eq!(Style::reset(), spans.0[0].style);
/// assert_eq!(Style::reset(), spans.0[1].style);
/// ```
pub fn reset_style(&mut self) {
for span in &mut self.0 {
span.reset_style();
}
}
}
impl<'a> From<String> for Spans<'a> {
fn from(s: String) -> Spans<'a> {
Spans(vec![Span::from(s)])
}
}
impl<'a> From<&'a str> for Spans<'a> {
fn from(s: &'a str) -> Spans<'a> {
Spans(vec![Span::from(s)])
}
}
impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
Spans(spans)
}
}
impl<'a> From<Span<'a>> for Spans<'a> {
fn from(span: Span<'a>) -> Spans<'a> {
Spans(vec![span])
}
}
impl<'a> From<Spans<'a>> for String {
fn from(line: Spans<'a>) -> String {
line.0.iter().fold(String::new(), |mut acc, s| {
acc.push_str(s.content.as_ref());
acc
})
}
}
/// A string split over multiple lines where each line is composed of several clusters, each with
/// their own style.
///
@ -364,7 +262,7 @@ impl<'a> From<Spans<'a>> for String {
/// ```
#[derive(Debug, Clone, PartialEq, Default, Eq)]
pub struct Text<'a> {
pub lines: Vec<Spans<'a>>,
pub lines: Vec<Line<'a>>,
}
impl<'a> Text<'a> {
@ -382,13 +280,13 @@ impl<'a> Text<'a> {
T: Into<Cow<'a, str>>,
{
let lines: Vec<_> = match content.into() {
Cow::Borrowed("") => vec![Spans::from("")],
Cow::Borrowed(s) => s.lines().map(Spans::from).collect(),
Cow::Owned(s) if s.is_empty() => vec![Spans::from("")],
Cow::Owned(s) => s.lines().map(|l| Spans::from(l.to_owned())).collect(),
Cow::Borrowed("") => vec![Line::from("")],
Cow::Borrowed(s) => s.lines().map(Line::from).collect(),
Cow::Owned(s) if s.is_empty() => vec![Line::from("")],
Cow::Owned(s) => s.lines().map(|l| Line::from(l.to_owned())).collect(),
};
Text { lines }
Text::from(lines)
}
/// Create some text (potentially multiple lines) with a style.
@ -421,11 +319,7 @@ impl<'a> Text<'a> {
/// assert_eq!(15, text.width());
/// ```
pub fn width(&self) -> usize {
self.lines
.iter()
.map(Spans::width)
.max()
.unwrap_or_default()
self.lines.iter().map(Line::width).max().unwrap_or_default()
}
/// Returns the height.
@ -468,14 +362,14 @@ impl<'a> Text<'a> {
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Spans, Text};
/// # use ratatui::text::{Span, Line, Text};
/// # use ratatui::style::{Color, Style, Modifier};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut text = Text::styled("The first line\nThe second line", style);
///
/// text.reset_style();
/// for line in &text.lines {
/// for span in &line.0 {
/// for span in &line.spans {
/// assert_eq!(Style::reset(), span.style);
/// }
/// }
@ -508,25 +402,43 @@ impl<'a> From<Cow<'a, str>> for Text<'a> {
impl<'a> From<Span<'a>> for Text<'a> {
fn from(span: Span<'a>) -> Text<'a> {
Text {
lines: vec![Spans::from(span)],
lines: vec![Line::from(span)],
}
}
}
#[allow(deprecated)]
impl<'a> From<Spans<'a>> for Text<'a> {
fn from(spans: Spans<'a>) -> Text<'a> {
Text { lines: vec![spans] }
Text {
lines: vec![spans.into()],
}
}
}
impl<'a> From<Line<'a>> for Text<'a> {
fn from(line: Line<'a>) -> Text<'a> {
Text { lines: vec![line] }
}
}
#[allow(deprecated)]
impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
fn from(lines: Vec<Spans<'a>>) -> Text<'a> {
Text {
lines: lines.into_iter().map(|l| l.0.into()).collect(),
}
}
}
impl<'a> From<Vec<Line<'a>>> for Text<'a> {
fn from(lines: Vec<Line<'a>>) -> Text<'a> {
Text { lines }
}
}
impl<'a> IntoIterator for Text<'a> {
type Item = Spans<'a>;
type Item = Line<'a>;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
@ -534,129 +446,12 @@ impl<'a> IntoIterator for Text<'a> {
}
}
impl<'a> Extend<Spans<'a>> for Text<'a> {
fn extend<T: IntoIterator<Item = Spans<'a>>>(&mut self, iter: T) {
self.lines.extend(iter);
}
}
/// A wrapper around a string that is masked when displayed.
///
/// The masked string is displayed as a series of the same character.
/// This might be used to display a password field or similar secure data.
///
/// # Examples
///
/// ```rust
/// use ratatui::{buffer::Buffer, layout::Rect, text::Masked, widgets::{Paragraph, Widget}};
///
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
/// let password = Masked::new("12345", 'x');
///
/// Paragraph::new(password).render(buffer.area, &mut buffer);
/// assert_eq!(buffer, Buffer::with_lines(vec!["xxxxx"]));
/// ```
#[derive(Clone)]
pub struct Masked<'a> {
inner: Cow<'a, str>,
mask_char: char,
}
impl<'a> Masked<'a> {
pub fn new(s: impl Into<Cow<'a, str>>, mask_char: char) -> Self {
Self {
inner: s.into(),
mask_char,
}
}
/// The character to use for masking.
pub fn mask_char(&self) -> char {
self.mask_char
}
/// The underlying string, with all characters masked.
pub fn value(&self) -> Cow<'a, str> {
self.inner.chars().map(|_| self.mask_char).collect()
}
}
impl Debug for Masked<'_> {
/// Debug representation of a masked string is the underlying string
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.inner).map_err(|_| fmt::Error)
}
}
impl Display for Masked<'_> {
/// Display representation of a masked string is the masked string
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.value()).map_err(|_| fmt::Error)
}
}
impl<'a> From<&'a Masked<'a>> for Cow<'a, str> {
fn from(masked: &'a Masked) -> Cow<'a, str> {
masked.value()
}
}
impl<'a> From<Masked<'a>> for Cow<'a, str> {
fn from(masked: Masked<'a>) -> Cow<'a, str> {
masked.value()
}
}
impl<'a> From<&'a Masked<'_>> for Text<'a> {
fn from(masked: &'a Masked) -> Text<'a> {
Text::raw(masked.value())
}
}
impl<'a> From<Masked<'a>> for Text<'a> {
fn from(masked: Masked<'a>) -> Text<'a> {
Text::raw(masked.value())
}
}
#[cfg(test)]
mod tests {
use std::borrow::Borrow;
use super::*;
#[test]
fn test_masked_value() {
let masked = Masked::new("12345", 'x');
assert_eq!(masked.value(), "xxxxx");
}
#[test]
fn test_masked_debug() {
let masked = Masked::new("12345", 'x');
assert_eq!(format!("{masked:?}"), "12345");
}
#[test]
fn test_masked_display() {
let masked = Masked::new("12345", 'x');
assert_eq!(format!("{masked}"), "xxxxx");
}
#[test]
fn test_masked_conversions() {
let masked = Masked::new("12345", 'x');
let text: Text = masked.borrow().into();
assert_eq!(text.lines, vec![Spans::from("xxxxx")]);
let text: Text = masked.to_owned().into();
assert_eq!(text.lines, vec![Spans::from("xxxxx")]);
let cow: Cow<str> = masked.borrow().into();
assert_eq!(cow, "xxxxx");
let cow: Cow<str> = masked.to_owned().into();
assert_eq!(cow, "xxxxx");
impl<'a, T> Extend<T> for Text<'a>
where
T: Into<Line<'a>>,
{
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
let lines = iter.into_iter().map(|s| s.into());
self.lines.extend(lines);
}
}

213
src/text/line.rs Normal file
View file

@ -0,0 +1,213 @@
#![allow(deprecated)]
use super::{Span, Spans, Style};
#[derive(Debug, Clone, PartialEq, Default, Eq)]
pub struct Line<'a> {
pub spans: Vec<Span<'a>>,
}
impl<'a> Line<'a> {
/// Returns the width of the underlying string.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Line};
/// # use ratatui::style::{Color, Style};
/// let line = Line::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::raw(" text"),
/// ]);
/// assert_eq!(7, line.width());
/// ```
pub fn width(&self) -> usize {
self.spans.iter().map(Span::width).sum()
}
/// Patches the style of each Span in an existing Line, adding modifiers from the given style.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Line};
/// # use ratatui::style::{Color, Style, Modifier};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut raw_line = Line::from(vec![
/// Span::raw("My"),
/// Span::raw(" text"),
/// ]);
/// let mut styled_line = Line::from(vec![
/// Span::styled("My", style),
/// Span::styled(" text", style),
/// ]);
///
/// assert_ne!(raw_line, styled_line);
///
/// raw_line.patch_style(style);
/// assert_eq!(raw_line, styled_line);
/// ```
pub fn patch_style(&mut self, style: Style) {
for span in &mut self.spans {
span.patch_style(style);
}
}
/// Resets the style of each Span in the Line.
/// Equivalent to calling `patch_style(Style::reset())`.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Line};
/// # use ratatui::style::{Color, Style, Modifier};
/// let mut line = Line::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
/// ]);
///
/// line.reset_style();
/// assert_eq!(Style::reset(), line.spans[0].style);
/// assert_eq!(Style::reset(), line.spans[1].style);
/// ```
pub fn reset_style(&mut self) {
for span in &mut self.spans {
span.reset_style();
}
}
}
impl<'a> From<String> for Line<'a> {
fn from(s: String) -> Self {
Self::from(vec![Span::from(s)])
}
}
impl<'a> From<&'a str> for Line<'a> {
fn from(s: &'a str) -> Self {
Self::from(vec![Span::from(s)])
}
}
impl<'a> From<Vec<Span<'a>>> for Line<'a> {
fn from(spans: Vec<Span<'a>>) -> Self {
Self { spans }
}
}
impl<'a> From<Span<'a>> for Line<'a> {
fn from(span: Span<'a>) -> Self {
Self::from(vec![span])
}
}
impl<'a> From<Line<'a>> for String {
fn from(line: Line<'a>) -> String {
line.spans.iter().fold(String::new(), |mut acc, s| {
acc.push_str(s.content.as_ref());
acc
})
}
}
impl<'a> From<Spans<'a>> for Line<'a> {
fn from(value: Spans<'a>) -> Self {
Self::from(value.0)
}
}
#[cfg(test)]
mod tests {
use crate::style::{Color, Modifier, Style};
use crate::text::{Line, Span, Spans};
#[test]
fn test_width() {
let line = Line::from(vec![
Span::styled("My", Style::default().fg(Color::Yellow)),
Span::raw(" text"),
]);
assert_eq!(7, line.width());
let empty_line = Line::default();
assert_eq!(0, empty_line.width());
}
#[test]
fn test_patch_style() {
let style = Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::ITALIC);
let mut raw_line = Line::from(vec![Span::raw("My"), Span::raw(" text")]);
let styled_line = Line::from(vec![
Span::styled("My", style),
Span::styled(" text", style),
]);
assert_ne!(raw_line, styled_line);
raw_line.patch_style(style);
assert_eq!(raw_line, styled_line);
}
#[test]
fn test_reset_style() {
let mut line = Line::from(vec![
Span::styled("My", Style::default().fg(Color::Yellow)),
Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
]);
line.reset_style();
assert_eq!(Style::reset(), line.spans[0].style);
assert_eq!(Style::reset(), line.spans[1].style);
}
#[test]
fn test_from_string() {
let s = String::from("Hello, world!");
let line = Line::from(s);
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
}
#[test]
fn test_from_str() {
let s = "Hello, world!";
let line = Line::from(s);
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
}
#[test]
fn test_from_vec() {
let spans = vec![
Span::styled("Hello,", Style::default().fg(Color::Red)),
Span::styled(" world!", Style::default().fg(Color::Green)),
];
let line = Line::from(spans.clone());
assert_eq!(spans, line.spans);
}
#[test]
fn test_from_span() {
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
let line = Line::from(span.clone());
assert_eq!(vec![span], line.spans);
}
#[test]
fn test_from_spans() {
let spans = vec![
Span::styled("Hello,", Style::default().fg(Color::Red)),
Span::styled(" world!", Style::default().fg(Color::Green)),
];
assert_eq!(Line::from(Spans::from(spans.clone())), Line::from(spans));
}
#[test]
fn test_into_string() {
let line = Line::from(vec![
Span::styled("Hello,", Style::default().fg(Color::Red)),
Span::styled(" world!", Style::default().fg(Color::Green)),
]);
let s: String = line.into();
assert_eq!("Hello, world!", s);
}
}

127
src/text/masked.rs Normal file
View file

@ -0,0 +1,127 @@
use std::{
borrow::Cow,
fmt::{self, Debug, Display},
};
use super::Text;
/// A wrapper around a string that is masked when displayed.
///
/// The masked string is displayed as a series of the same character.
/// This might be used to display a password field or similar secure data.
///
/// # Examples
///
/// ```rust
/// use ratatui::{buffer::Buffer, layout::Rect, text::Masked, widgets::{Paragraph, Widget}};
///
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
/// let password = Masked::new("12345", 'x');
///
/// Paragraph::new(password).render(buffer.area, &mut buffer);
/// assert_eq!(buffer, Buffer::with_lines(vec!["xxxxx"]));
/// ```
#[derive(Clone)]
pub struct Masked<'a> {
inner: Cow<'a, str>,
mask_char: char,
}
impl<'a> Masked<'a> {
pub fn new(s: impl Into<Cow<'a, str>>, mask_char: char) -> Self {
Self {
inner: s.into(),
mask_char,
}
}
/// The character to use for masking.
pub fn mask_char(&self) -> char {
self.mask_char
}
/// The underlying string, with all characters masked.
pub fn value(&self) -> Cow<'a, str> {
self.inner.chars().map(|_| self.mask_char).collect()
}
}
impl Debug for Masked<'_> {
/// Debug representation of a masked string is the underlying string
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.inner).map_err(|_| fmt::Error)
}
}
impl Display for Masked<'_> {
/// Display representation of a masked string is the masked string
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.value()).map_err(|_| fmt::Error)
}
}
impl<'a> From<&'a Masked<'a>> for Cow<'a, str> {
fn from(masked: &'a Masked) -> Cow<'a, str> {
masked.value()
}
}
impl<'a> From<Masked<'a>> for Cow<'a, str> {
fn from(masked: Masked<'a>) -> Cow<'a, str> {
masked.value()
}
}
impl<'a> From<&'a Masked<'_>> for Text<'a> {
fn from(masked: &'a Masked) -> Text<'a> {
Text::raw(masked.value())
}
}
impl<'a> From<Masked<'a>> for Text<'a> {
fn from(masked: Masked<'a>) -> Text<'a> {
Text::raw(masked.value())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::text::Line;
use std::borrow::Borrow;
#[test]
fn test_masked_value() {
let masked = Masked::new("12345", 'x');
assert_eq!(masked.value(), "xxxxx");
}
#[test]
fn test_masked_debug() {
let masked = Masked::new("12345", 'x');
assert_eq!(format!("{masked:?}"), "12345");
}
#[test]
fn test_masked_display() {
let masked = Masked::new("12345", 'x');
assert_eq!(format!("{masked}"), "xxxxx");
}
#[test]
fn test_masked_conversions() {
let masked = Masked::new("12345", 'x');
let text: Text = masked.borrow().into();
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
let text: Text = masked.to_owned().into();
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
let cow: Cow<str> = masked.borrow().into();
assert_eq!(cow, "xxxxx");
let cow: Cow<str> = masked.to_owned().into();
assert_eq!(cow, "xxxxx");
}
}

203
src/text/spans.rs Normal file
View file

@ -0,0 +1,203 @@
#![allow(deprecated)]
use super::{Span, Style};
/// A string composed of clusters of graphemes, each with their own style.
///
/// `Spans` has been deprecated in favor of `Line`, and will be removed in the
/// future. All methods that accept Spans have been replaced with methods that
/// accept Into<Line<'a>> (which is implemented on `Spans`) to allow users of
/// this crate to gradually transition to Line.
#[derive(Debug, Clone, PartialEq, Default, Eq)]
#[deprecated(note = "Use `ratatui::text::Line` instead")]
pub struct Spans<'a>(pub Vec<Span<'a>>);
impl<'a> Spans<'a> {
/// Returns the width of the underlying string.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Spans};
/// # use ratatui::style::{Color, Style};
/// let spans = Spans::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::raw(" text"),
/// ]);
/// assert_eq!(7, spans.width());
/// ```
pub fn width(&self) -> usize {
self.0.iter().map(Span::width).sum()
}
/// Patches the style of each Span in an existing Spans, adding modifiers from the given style.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Spans};
/// # use ratatui::style::{Color, Style, Modifier};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut raw_spans = Spans::from(vec![
/// Span::raw("My"),
/// Span::raw(" text"),
/// ]);
/// let mut styled_spans = Spans::from(vec![
/// Span::styled("My", style),
/// Span::styled(" text", style),
/// ]);
///
/// assert_ne!(raw_spans, styled_spans);
///
/// raw_spans.patch_style(style);
/// assert_eq!(raw_spans, styled_spans);
/// ```
pub fn patch_style(&mut self, style: Style) {
for span in &mut self.0 {
span.patch_style(style);
}
}
/// Resets the style of each Span in the Spans.
/// Equivalent to calling `patch_style(Style::reset())`.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Spans};
/// # use ratatui::style::{Color, Style, Modifier};
/// let mut spans = Spans::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
/// ]);
///
/// spans.reset_style();
/// assert_eq!(Style::reset(), spans.0[0].style);
/// assert_eq!(Style::reset(), spans.0[1].style);
/// ```
pub fn reset_style(&mut self) {
for span in &mut self.0 {
span.reset_style();
}
}
}
impl<'a> From<String> for Spans<'a> {
fn from(s: String) -> Spans<'a> {
Spans(vec![Span::from(s)])
}
}
impl<'a> From<&'a str> for Spans<'a> {
fn from(s: &'a str) -> Spans<'a> {
Spans(vec![Span::from(s)])
}
}
impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
Spans(spans)
}
}
impl<'a> From<Span<'a>> for Spans<'a> {
fn from(span: Span<'a>) -> Spans<'a> {
Spans(vec![span])
}
}
impl<'a> From<Spans<'a>> for String {
fn from(line: Spans<'a>) -> String {
line.0.iter().fold(String::new(), |mut acc, s| {
acc.push_str(s.content.as_ref());
acc
})
}
}
#[cfg(test)]
mod tests {
use crate::style::{Color, Modifier, Style};
use crate::text::{Span, Spans};
#[test]
fn test_width() {
let spans = Spans::from(vec![
Span::styled("My", Style::default().fg(Color::Yellow)),
Span::raw(" text"),
]);
assert_eq!(7, spans.width());
let empty_spans = Spans::default();
assert_eq!(0, empty_spans.width());
}
#[test]
fn test_patch_style() {
let style = Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::ITALIC);
let mut raw_spans = Spans::from(vec![Span::raw("My"), Span::raw(" text")]);
let styled_spans = Spans::from(vec![
Span::styled("My", style),
Span::styled(" text", style),
]);
assert_ne!(raw_spans, styled_spans);
raw_spans.patch_style(style);
assert_eq!(raw_spans, styled_spans);
}
#[test]
fn test_reset_style() {
let mut spans = Spans::from(vec![
Span::styled("My", Style::default().fg(Color::Yellow)),
Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
]);
spans.reset_style();
assert_eq!(Style::reset(), spans.0[0].style);
assert_eq!(Style::reset(), spans.0[1].style);
}
#[test]
fn test_from_string() {
let s = String::from("Hello, world!");
let spans = Spans::from(s);
assert_eq!(vec![Span::from("Hello, world!")], spans.0);
}
#[test]
fn test_from_str() {
let s = "Hello, world!";
let spans = Spans::from(s);
assert_eq!(vec![Span::from("Hello, world!")], spans.0);
}
#[test]
fn test_from_vec() {
let spans_vec = vec![
Span::styled("Hello,", Style::default().fg(Color::Red)),
Span::styled(" world!", Style::default().fg(Color::Green)),
];
let spans = Spans::from(spans_vec.clone());
assert_eq!(spans_vec, spans.0);
}
#[test]
fn test_from_span() {
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
let spans = Spans::from(span.clone());
assert_eq!(vec![span], spans.0);
}
#[test]
fn test_into_string() {
let spans = Spans::from(vec![
Span::styled("Hello,", Style::default().fg(Color::Red)),
Span::styled(" world!", Style::default().fg(Color::Green)),
]);
let s: String = spans.into();
assert_eq!("Hello, world!", s);
}
}

View file

@ -3,7 +3,7 @@ use crate::{
layout::{Alignment, Rect},
style::Style,
symbols::line,
text::{Span, Spans},
text::{Line, Span},
widgets::{Borders, Widget},
};
@ -99,7 +99,7 @@ impl Padding {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Block<'a> {
/// Optional title place on the upper left of the block
title: Option<Spans<'a>>,
title: Option<Line<'a>>,
/// 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,
@ -136,7 +136,7 @@ impl<'a> Default for Block<'a> {
impl<'a> Block<'a> {
pub fn title<T>(mut self, title: T) -> Block<'a>
where
T: Into<Spans<'a>>,
T: Into<Line<'a>>,
{
self.title = Some(title.into());
self
@ -144,12 +144,12 @@ impl<'a> Block<'a> {
#[deprecated(
since = "0.10.0",
note = "You should use styling capabilities of `text::Spans` given as argument of the `title` method to apply styling to the title."
note = "You should use styling capabilities of `text::Line` given as argument of the `title` method to apply styling to the title."
)]
pub fn title_style(mut self, style: Style) -> Block<'a> {
if let Some(t) = self.title {
let title = String::from(t);
self.title = Some(Spans::from(Span::styled(title, style)));
self.title = Some(Line::from(Span::styled(title, style)));
}
self
}
@ -337,7 +337,7 @@ impl<'a> Widget for Block<'a> {
area.top()
};
buf.set_spans(title_x, title_y, &title, title_area_width);
buf.set_line(title_x, title_y, &title, title_area_width);
}
}
}

View file

@ -14,7 +14,7 @@ use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
text::{Span, Spans},
text::Span,
widgets::{Block, Widget},
};
@ -128,7 +128,7 @@ impl<'a, S: DateStyler> Widget for Monthly<'a, S> {
);
// cal is 21 cells wide, so hard code the 11
let x_off = 11_u16.saturating_sub(line.width() as u16 / 2);
buf.set_spans(area.x + x_off, area.y, &line.into(), area.width);
buf.set_line(area.x + x_off, area.y, &line.into(), area.width);
area.y += 1
}
@ -146,19 +146,19 @@ impl<'a, S: DateStyler> Widget for Monthly<'a, S> {
// go through all the weeks containing a day in the target month.
while curr_day.month() as u8 != self.display_date.month().next() as u8 {
let mut line = Spans(Vec::with_capacity(14));
let mut spans = Vec::with_capacity(14);
for i in 0..7 {
// Draw the gutter. Do it here so we can avoid worrying about
// styling the ' ' in the format_date method
if i == 0 {
line.0.push(Span::styled(" ", Style::default()));
spans.push(Span::styled(" ", Style::default()));
} else {
line.0.push(Span::styled(" ", self.default_bg()));
spans.push(Span::styled(" ", self.default_bg()));
}
line.0.push(self.format_date(curr_day));
spans.push(self.format_date(curr_day));
curr_day += Duration::DAY;
}
buf.set_spans(area.x, area.y, &line, area.width);
buf.set_line(area.x, area.y, &spans.into(), area.width);
area.y += 1;
}
}

View file

@ -16,7 +16,7 @@ use crate::{
layout::Rect,
style::{Color, Style},
symbols,
text::Spans,
text::Line as TextLine,
widgets::{Block, Widget},
};
use std::fmt::Debug;
@ -31,7 +31,7 @@ pub trait Shape {
pub struct Label<'a> {
x: f64,
y: f64,
spans: Spans<'a>,
line: TextLine<'a>,
}
#[derive(Debug, Clone)]
@ -299,14 +299,14 @@ impl<'a> Context<'a> {
}
/// Print a string on the canvas at the given position
pub fn print<T>(&mut self, x: f64, y: f64, spans: T)
pub fn print<T>(&mut self, x: f64, y: f64, line: T)
where
T: Into<Spans<'a>>,
T: Into<TextLine<'a>>,
{
self.labels.push(Label {
x,
y,
spans: spans.into(),
line: line.into(),
});
}
@ -510,7 +510,7 @@ where
{
let x = ((label.x - left) * resolution.0 / width) as u16 + canvas_area.left();
let y = ((top - label.y) * resolution.1 / height) as u16 + canvas_area.top();
buf.set_spans(x, y, &label.spans, canvas_area.right() - x);
buf.set_line(x, y, &label.line, canvas_area.right() - x);
}
}
}

View file

@ -8,7 +8,7 @@ use crate::{
layout::{Constraint, Rect},
style::{Color, Style},
symbols,
text::{Span, Spans},
text::{Line as TextLine, Span},
widgets::{
canvas::{Canvas, Line, Points},
Block, Borders, Widget,
@ -19,7 +19,7 @@ use crate::{
#[derive(Debug, Clone)]
pub struct Axis<'a> {
/// Title displayed next to axis end
title: Option<Spans<'a>>,
title: Option<TextLine<'a>>,
/// Bounds for the axis (all data points outside these limits will not be represented)
bounds: [f64; 2],
/// A list of labels to put to the left or below the axis
@ -45,7 +45,7 @@ impl<'a> Default for Axis<'a> {
impl<'a> Axis<'a> {
pub fn title<T>(mut self, title: T) -> Axis<'a>
where
T: Into<Spans<'a>>,
T: Into<TextLine<'a>>,
{
self.title = Some(title.into());
self
@ -53,12 +53,12 @@ impl<'a> Axis<'a> {
#[deprecated(
since = "0.10.0",
note = "You should use styling capabilities of `text::Spans` given as argument of the `title` method to apply styling to the title."
note = "You should use styling capabilities of `text::Line` given as argument of the `title` method to apply styling to the title."
)]
pub fn title_style(mut self, style: Style) -> Axis<'a> {
if let Some(t) = self.title {
let title = String::from(t);
self.title = Some(Spans::from(Span::styled(title, style)));
self.title = Some(TextLine::from(Span::styled(title, style)));
}
self
}
@ -597,7 +597,7 @@ impl<'a> Widget for Chart<'a> {
},
original_style,
);
buf.set_spans(x, y, &title, width);
buf.set_line(x, y, &title, width);
}
if let Some((x, y)) = layout.title_y {
@ -612,7 +612,7 @@ impl<'a> Widget for Chart<'a> {
},
original_style,
);
buf.set_spans(x, y, &title, width);
buf.set_line(x, y, &title, width);
}
}
}

View file

@ -3,7 +3,7 @@ use crate::{
layout::Rect,
style::{Color, Style},
symbols,
text::{Span, Spans},
text::{Line, Span},
widgets::{Block, Widget},
};
@ -138,7 +138,7 @@ impl<'a> Widget for Gauge<'a> {
.set_symbol(get_unicode_block(filled_width % 1.0));
}
}
// set the span
// set the line
buf.set_span(label_col, label_row, &label, clamped_label_width);
}
}
@ -174,7 +174,7 @@ fn get_unicode_block<'a>(frac: f64) -> &'a str {
pub struct LineGauge<'a> {
block: Option<Block<'a>>,
ratio: f64,
label: Option<Spans<'a>>,
label: Option<Line<'a>>,
line_set: symbols::line::Set,
style: Style,
gauge_style: Style,
@ -215,7 +215,7 @@ impl<'a> LineGauge<'a> {
pub fn label<T>(mut self, label: T) -> Self
where
T: Into<Spans<'a>>,
T: Into<Line<'a>>,
{
self.label = Some(label.into());
self
@ -251,8 +251,8 @@ impl<'a> Widget for LineGauge<'a> {
let ratio = self.ratio;
let label = self
.label
.unwrap_or_else(move || Spans::from(format!("{:.0}%", ratio * 100.0)));
let (col, row) = buf.set_spans(
.unwrap_or_else(move || Line::from(format!("{:.0}%", ratio * 100.0)));
let (col, row) = buf.set_line(
gauge_area.left(),
gauge_area.top(),
&label,

View file

@ -269,7 +269,7 @@ impl<'a> StatefulWidget for List<'a> {
} else {
(x, list_area.width)
};
buf.set_spans(elem_x, y + j as u16, line, max_element_width);
buf.set_line(elem_x, y + j as u16, line, max_element_width);
}
if is_selected {
buf.set_style(area, self.highlight_style);
@ -292,7 +292,7 @@ mod tests {
use crate::{
assert_buffer_eq,
style::Color,
text::{Span, Spans},
text::{Line, Span},
widgets::{Borders, StatefulWidget, Widget},
};
@ -356,7 +356,7 @@ mod tests {
#[test]
fn test_list_item_new_from_spans() {
let spans = Spans::from(vec![
let spans = Line::from(vec![
Span::styled("Test ", Style::default().fg(Color::Blue)),
Span::styled("item", Style::default().fg(Color::Red)),
]);
@ -368,11 +368,11 @@ mod tests {
#[test]
fn test_list_item_new_from_vec_spans() {
let lines = vec![
Spans::from(vec![
Line::from(vec![
Span::styled("Test ", Style::default().fg(Color::Blue)),
Span::styled("item", Style::default().fg(Color::Red)),
]),
Spans::from(vec![
Line::from(vec![
Span::styled("Second ", Style::default().fg(Color::Green)),
Span::styled("line", Style::default().fg(Color::Yellow)),
]),

View file

@ -24,17 +24,17 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
/// # Examples
///
/// ```
/// # use ratatui::text::{Text, Spans, Span};
/// # use ratatui::text::{Text, Line, Span};
/// # use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
/// # use ratatui::style::{Style, Color, Modifier};
/// # use ratatui::layout::{Alignment};
/// let text = vec![
/// Spans::from(vec![
/// Line::from(vec![
/// Span::raw("First"),
/// Span::styled("line",Style::default().add_modifier(Modifier::ITALIC)),
/// Span::raw("."),
/// ]),
/// Spans::from(Span::styled("Second line", Style::default().fg(Color::Red))),
/// Line::from(Span::styled("Second line", Style::default().fg(Color::Red))),
/// ];
/// Paragraph::new(text)
/// .block(Block::default().title("Paragraph").borders(Borders::ALL))
@ -149,9 +149,8 @@ impl<'a> Widget for Paragraph<'a> {
}
let style = self.style;
let mut styled = self.text.lines.iter().flat_map(|spans| {
spans
.0
let mut styled = self.text.lines.iter().flat_map(|line| {
line.spans
.iter()
.flat_map(|span| span.styled_graphemes(style))
// Required given the way composers work but might be refactored out if we change
@ -205,7 +204,7 @@ mod test {
use super::*;
use crate::{
style::Color,
text::{Span, Spans},
text::{Line, Span},
widgets::Borders,
};
@ -386,7 +385,7 @@ mod test {
fn test_render_paragraph_with_styled_text() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 13, 1));
Paragraph::new(Spans::from(vec![
Paragraph::new(Line::from(vec![
Span::styled("Hello, ", Style::default().fg(Color::Red)),
Span::styled("world!", Style::default().fg(Color::Blue)),
]))

View file

@ -13,13 +13,13 @@ use unicode_width::UnicodeWidthStr;
/// ```rust
/// # use ratatui::widgets::Cell;
/// # use ratatui::style::{Style, Modifier};
/// # use ratatui::text::{Span, Spans, Text};
/// # use ratatui::text::{Span, Line, Text};
/// # use std::borrow::Cow;
/// Cell::from("simple string");
///
/// Cell::from(Span::from("span"));
///
/// Cell::from(Spans::from(vec![
/// Cell::from(Line::from(vec![
/// Span::raw("a vec of "),
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD))
/// ]));
@ -142,7 +142,7 @@ impl<'a> Row<'a> {
/// # use ratatui::widgets::{Block, Borders, Table, Row, Cell};
/// # use ratatui::layout::Constraint;
/// # use ratatui::style::{Style, Color, Modifier};
/// # use ratatui::text::{Text, Spans, Span};
/// # use ratatui::text::{Text, Line, Span};
/// Table::new(vec![
/// // Row can be created from simple strings.
/// Row::new(vec!["Row11", "Row12", "Row13"]),
@ -152,7 +152,7 @@ impl<'a> Row<'a> {
/// Row::new(vec![
/// Cell::from("Row31"),
/// Cell::from("Row32").style(Style::default().fg(Color::Yellow)),
/// Cell::from(Spans::from(vec![
/// Cell::from(Line::from(vec![
/// Span::raw("Row"),
/// Span::styled("33", Style::default().fg(Color::Green))
/// ])),
@ -477,11 +477,11 @@ impl<'a> StatefulWidget for Table<'a> {
fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
buf.set_style(area, cell.style);
for (i, spans) in cell.content.lines.iter().enumerate() {
for (i, line) in cell.content.lines.iter().enumerate() {
if i as u16 >= area.height {
break;
}
buf.set_spans(area.x, area.y + i as u16, spans, area.width);
buf.set_line(area.x, area.y + i as u16, line, area.width);
}
}

View file

@ -3,7 +3,7 @@ use crate::{
layout::Rect,
style::Style,
symbols,
text::{Span, Spans},
text::{Line, Span},
widgets::{Block, Widget},
};
@ -14,9 +14,9 @@ use crate::{
/// ```
/// # use ratatui::widgets::{Block, Borders, Tabs};
/// # use ratatui::style::{Style, Color};
/// # use ratatui::text::{Spans};
/// # use ratatui::text::{Line};
/// # use ratatui::symbols::{DOT};
/// let titles = ["Tab1", "Tab2", "Tab3", "Tab4"].iter().cloned().map(Spans::from).collect();
/// let titles = ["Tab1", "Tab2", "Tab3", "Tab4"].iter().cloned().map(Line::from).collect();
/// Tabs::new(titles)
/// .block(Block::default().title("Tabs").borders(Borders::ALL))
/// .style(Style::default().fg(Color::White))
@ -28,7 +28,7 @@ pub struct Tabs<'a> {
/// A block to wrap this widget in if necessary
block: Option<Block<'a>>,
/// One title for each tab
titles: Vec<Spans<'a>>,
titles: Vec<Line<'a>>,
/// The index of the selected tabs
selected: usize,
/// The style used to draw the text
@ -40,10 +40,13 @@ pub struct Tabs<'a> {
}
impl<'a> Tabs<'a> {
pub fn new(titles: Vec<Spans<'a>>) -> Tabs<'a> {
pub fn new<T>(titles: Vec<T>) -> Tabs<'a>
where
T: Into<Line<'a>>,
{
Tabs {
block: None,
titles,
titles: titles.into_iter().map(Into::into).collect(),
selected: 0,
style: Default::default(),
highlight_style: Default::default(),
@ -105,7 +108,7 @@ impl<'a> Widget for Tabs<'a> {
if remaining_width == 0 {
break;
}
let pos = buf.set_spans(x, tabs_area.top(), &title, remaining_width);
let pos = buf.set_line(x, tabs_area.top(), &title, remaining_width);
if i == self.selected {
buf.set_style(
Rect {

View file

@ -1,10 +1,12 @@
#![allow(deprecated)]
use ratatui::{
backend::TestBackend,
buffer::Buffer,
layout::Rect,
style::{Color, Style},
symbols,
text::Spans,
text::Line,
widgets::{Block, Borders, List, ListItem, ListState},
Terminal,
};
@ -154,9 +156,9 @@ fn widgets_list_should_display_multiline_items() {
.draw(|f| {
let size = f.size();
let items = vec![
ListItem::new(vec![Spans::from("Item 1"), Spans::from("Item 1a")]),
ListItem::new(vec![Spans::from("Item 2"), Spans::from("Item 2b")]),
ListItem::new(vec![Spans::from("Item 3"), Spans::from("Item 3c")]),
ListItem::new(vec![Line::from("Item 1"), Line::from("Item 1a")]),
ListItem::new(vec![Line::from("Item 2"), Line::from("Item 2b")]),
ListItem::new(vec![Line::from("Item 3"), Line::from("Item 3c")]),
];
let list = List::new(items)
.highlight_style(Style::default().bg(Color::Yellow))
@ -189,9 +191,9 @@ fn widgets_list_should_repeat_highlight_symbol() {
.draw(|f| {
let size = f.size();
let items = vec![
ListItem::new(vec![Spans::from("Item 1"), Spans::from("Item 1a")]),
ListItem::new(vec![Spans::from("Item 2"), Spans::from("Item 2b")]),
ListItem::new(vec![Spans::from("Item 3"), Spans::from("Item 3c")]),
ListItem::new(vec![Line::from("Item 1"), Line::from("Item 1a")]),
ListItem::new(vec![Line::from("Item 2"), Line::from("Item 2b")]),
ListItem::new(vec![Line::from("Item 3"), Line::from("Item 3c")]),
];
let list = List::new(items)
.highlight_style(Style::default().bg(Color::Yellow))

View file

@ -1,8 +1,10 @@
#![allow(deprecated)]
use ratatui::{
backend::TestBackend,
buffer::Buffer,
layout::Alignment,
text::{Span, Spans, Text},
text::{Line, Span, Text},
widgets::{Block, Borders, Padding, Paragraph, Wrap},
Terminal,
};
@ -21,7 +23,7 @@ fn widgets_paragraph_can_wrap_its_content() {
terminal
.draw(|f| {
let size = f.size();
let text = vec![Spans::from(SAMPLE_STRING)];
let text = vec![Line::from(SAMPLE_STRING)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL).padding(Padding {
left: 2,
@ -99,7 +101,7 @@ fn widgets_paragraph_renders_double_width_graphemes() {
terminal
.draw(|f| {
let size = f.size();
let text = vec![Spans::from(s)];
let text = vec![Line::from(s)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.wrap(Wrap { trim: true });
@ -131,7 +133,7 @@ fn widgets_paragraph_renders_mixed_width_graphemes() {
terminal
.draw(|f| {
let size = f.size();
let text = vec![Spans::from(s)];
let text = vec![Line::from(s)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.wrap(Wrap { trim: true });
@ -155,7 +157,7 @@ fn widgets_paragraph_renders_mixed_width_graphemes() {
#[test]
fn widgets_paragraph_can_wrap_with_a_trailing_nbsp() {
let nbsp: &str = "\u{00a0}";
let line = Spans::from(vec![Span::raw("NBSP"), Span::raw(nbsp)]);
let line = Line::from(vec![Span::raw("NBSP"), Span::raw(nbsp)]);
let backend = TestBackend::new(20, 3);
let mut terminal = Terminal::new(backend).unwrap();
let expected = Buffer::with_lines(vec![

View file

@ -1,9 +1,11 @@
#![allow(deprecated)]
use ratatui::{
backend::TestBackend,
buffer::Buffer,
layout::Constraint,
style::{Color, Modifier, Style},
text::{Span, Spans},
text::{Line, Span},
widgets::{Block, Borders, Cell, Row, Table, TableState},
Terminal,
};
@ -623,7 +625,7 @@ fn widgets_table_can_have_elements_styled_individually() {
Row::new(vec![
Cell::from("Row21"),
Cell::from("Row22").style(Style::default().fg(Color::Yellow)),
Cell::from(Spans::from(vec![
Cell::from(Line::from(vec![
Span::raw("Row"),
Span::styled("23", Style::default().fg(Color::Blue)),
]))

View file

@ -1,5 +1,7 @@
#![allow(deprecated)]
use ratatui::{
backend::TestBackend, buffer::Buffer, layout::Rect, symbols, text::Spans, widgets::Tabs,
backend::TestBackend, buffer::Buffer, layout::Rect, symbols, text::Line, widgets::Tabs,
Terminal,
};
@ -9,7 +11,7 @@ fn widgets_tabs_should_not_panic_on_narrow_areas() {
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let tabs = Tabs::new(["Tab1", "Tab2"].iter().cloned().map(Spans::from).collect());
let tabs = Tabs::new(["Tab1", "Tab2"].iter().cloned().map(Line::from).collect());
f.render_widget(
tabs,
Rect {
@ -31,7 +33,7 @@ fn widgets_tabs_should_truncate_the_last_item() {
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let tabs = Tabs::new(["Tab1", "Tab2"].iter().cloned().map(Spans::from).collect());
let tabs = Tabs::new(["Tab1", "Tab2"].iter().cloned().map(Line::from).collect());
f.render_widget(
tabs,
Rect {