From 728f82c0845f7381d72e43ff7fe092a4927864f3 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Thu, 18 May 2023 11:21:43 -0700 Subject: [PATCH] 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`. 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> 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` * 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 --- examples/demo/ui.rs | 22 +-- examples/inline.rs | 6 +- examples/list.rs | 14 +- examples/panic.rs | 32 ++-- examples/paragraph.rs | 16 +- examples/tabs.rs | 4 +- examples/user_input.rs | 6 +- src/buffer.rs | 26 +++- src/terminal.rs | 4 +- src/text.rs | 309 +++++++------------------------------ src/text/line.rs | 213 +++++++++++++++++++++++++ src/text/masked.rs | 127 +++++++++++++++ src/text/spans.rs | 203 ++++++++++++++++++++++++ src/widgets/block.rs | 12 +- src/widgets/calendar.rs | 14 +- src/widgets/canvas/mod.rs | 12 +- src/widgets/chart.rs | 14 +- src/widgets/gauge.rs | 12 +- src/widgets/list.rs | 10 +- src/widgets/paragraph.rs | 15 +- src/widgets/table.rs | 12 +- src/widgets/tabs.rs | 17 +- tests/widgets_list.rs | 16 +- tests/widgets_paragraph.rs | 12 +- tests/widgets_table.rs | 6 +- tests/widgets_tabs.rs | 8 +- 26 files changed, 757 insertions(+), 385 deletions(-) create mode 100644 src/text/line.rs create mode 100644 src/text/masked.rs create mode 100644 src/text/spans.rs diff --git a/examples/demo/ui.rs b/examples/demo/ui.rs index ec848524..dd969e58 100644 --- a/examples/demo/ui.rs +++ b/examples/demo/ui.rs @@ -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(f: &mut Frame, 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, diff --git a/examples/inline.rs b/examples/inline.rs index 0a7ce2b9..506d6a8f 100644 --- a/examples/inline.rs +++ b/examples/inline.rs @@ -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( 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(f: &mut Frame, 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), diff --git a/examples/list.rs b/examples/list.rs index 11fe65ad..e262eb96 100644 --- a/examples/list.rs +++ b/examples/list.rs @@ -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(f: &mut Frame, 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(f: &mut Frame, 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(f: &mut Frame, 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(f: &mut Frame, 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, ]) }) diff --git a/examples/panic.rs b/examples/panic.rs index 48fba221..a088a72c 100644 --- a/examples/panic.rs +++ b/examples/panic.rs @@ -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(terminal: &mut Terminal, app: &mut App) -> io::Result< fn ui(f: &mut Frame, 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() diff --git a/examples/paragraph.rs b/examples/paragraph.rs index 0c8b92d8..073b208f 100644 --- a/examples/paragraph.rs +++ b/examples/paragraph.rs @@ -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(f: &mut Frame, 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", '*'), diff --git a/examples/tabs.rs b/examples/tabs.rs index abcafe88..fa4af318 100644 --- a/examples/tabs.rs +++ b/examples/tabs.rs @@ -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(f: &mut Frame, 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)), ]) diff --git a/examples/user_input.rs b/examples/user_input.rs index 37cd3428..b55dd8c0 100644 --- a/examples/user_input.rs +++ b/examples/user_input.rs @@ -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(f: &mut Frame, 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(f: &mut Frame, 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(); diff --git a/src/buffer.rs b/src/buffer.rs index 3ca935f3..6e9f42c5 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -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) } diff --git a/src/terminal.rs b/src/terminal.rs index 37aab480..80434e8f 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -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") diff --git a/src/text.rs b/src/text.rs index 5439d296..8a09cfd9 100644 --- a/src/text.rs +++ b/src/text.rs @@ -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>); - -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 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>> for Spans<'a> { - fn from(spans: Vec>) -> Spans<'a> { - Spans(spans) - } -} - -impl<'a> From> for Spans<'a> { - fn from(span: Span<'a>) -> Spans<'a> { - Spans(vec![span]) - } -} - -impl<'a> From> 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> for String { /// ``` #[derive(Debug, Clone, PartialEq, Default, Eq)] pub struct Text<'a> { - pub lines: Vec>, + pub lines: Vec>, } impl<'a> Text<'a> { @@ -382,13 +280,13 @@ impl<'a> Text<'a> { T: Into>, { 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> for Text<'a> { impl<'a> From> 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> for Text<'a> { fn from(spans: Spans<'a>) -> Text<'a> { - Text { lines: vec![spans] } + Text { + lines: vec![spans.into()], + } } } +impl<'a> From> for Text<'a> { + fn from(line: Line<'a>) -> Text<'a> { + Text { lines: vec![line] } + } +} + +#[allow(deprecated)] impl<'a> From>> for Text<'a> { fn from(lines: Vec>) -> Text<'a> { + Text { + lines: lines.into_iter().map(|l| l.0.into()).collect(), + } + } +} + +impl<'a> From>> for Text<'a> { + fn from(lines: Vec>) -> Text<'a> { Text { lines } } } impl<'a> IntoIterator for Text<'a> { - type Item = Spans<'a>; + type Item = Line<'a>; type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { @@ -534,129 +446,12 @@ impl<'a> IntoIterator for Text<'a> { } } -impl<'a> Extend> for Text<'a> { - fn extend>>(&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>, 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> 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> 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 = masked.borrow().into(); - assert_eq!(cow, "xxxxx"); - - let cow: Cow = masked.to_owned().into(); - assert_eq!(cow, "xxxxx"); +impl<'a, T> Extend for Text<'a> +where + T: Into>, +{ + fn extend>(&mut self, iter: I) { + let lines = iter.into_iter().map(|s| s.into()); + self.lines.extend(lines); } } diff --git a/src/text/line.rs b/src/text/line.rs new file mode 100644 index 00000000..c1997fe8 --- /dev/null +++ b/src/text/line.rs @@ -0,0 +1,213 @@ +#![allow(deprecated)] +use super::{Span, Spans, Style}; + +#[derive(Debug, Clone, PartialEq, Default, Eq)] +pub struct Line<'a> { + pub spans: Vec>, +} + +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 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>> for Line<'a> { + fn from(spans: Vec>) -> Self { + Self { spans } + } +} + +impl<'a> From> for Line<'a> { + fn from(span: Span<'a>) -> Self { + Self::from(vec![span]) + } +} + +impl<'a> From> 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> 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); + } +} diff --git a/src/text/masked.rs b/src/text/masked.rs new file mode 100644 index 00000000..1ee8bb54 --- /dev/null +++ b/src/text/masked.rs @@ -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>, 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> 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> 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 = masked.borrow().into(); + assert_eq!(cow, "xxxxx"); + + let cow: Cow = masked.to_owned().into(); + assert_eq!(cow, "xxxxx"); + } +} diff --git a/src/text/spans.rs b/src/text/spans.rs new file mode 100644 index 00000000..1e20eedc --- /dev/null +++ b/src/text/spans.rs @@ -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> (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>); + +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 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>> for Spans<'a> { + fn from(spans: Vec>) -> Spans<'a> { + Spans(spans) + } +} + +impl<'a> From> for Spans<'a> { + fn from(span: Span<'a>) -> Spans<'a> { + Spans(vec![span]) + } +} + +impl<'a> From> 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); + } +} diff --git a/src/widgets/block.rs b/src/widgets/block.rs index d63838cf..7f6b1f95 100644 --- a/src/widgets/block.rs +++ b/src/widgets/block.rs @@ -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>, + title: Option>, /// Title alignment. The default is top left of the block, but one can choose to place /// title in the top middle, or top right of the block title_alignment: Alignment, @@ -136,7 +136,7 @@ impl<'a> Default for Block<'a> { impl<'a> Block<'a> { pub fn title(mut self, title: T) -> Block<'a> where - T: Into>, + T: Into>, { 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); } } } diff --git a/src/widgets/calendar.rs b/src/widgets/calendar.rs index 2afdb2f6..94c03474 100644 --- a/src/widgets/calendar.rs +++ b/src/widgets/calendar.rs @@ -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; } } diff --git a/src/widgets/canvas/mod.rs b/src/widgets/canvas/mod.rs index c4d0a7e6..1d8ea603 100644 --- a/src/widgets/canvas/mod.rs +++ b/src/widgets/canvas/mod.rs @@ -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(&mut self, x: f64, y: f64, spans: T) + pub fn print(&mut self, x: f64, y: f64, line: T) where - T: Into>, + T: Into>, { 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); } } } diff --git a/src/widgets/chart.rs b/src/widgets/chart.rs index a8642b3a..61ac0108 100644 --- a/src/widgets/chart.rs +++ b/src/widgets/chart.rs @@ -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>, + title: Option>, /// 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(mut self, title: T) -> Axis<'a> where - T: Into>, + T: Into>, { 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); } } } diff --git a/src/widgets/gauge.rs b/src/widgets/gauge.rs index 4f7b0a6b..5b8ee2bc 100644 --- a/src/widgets/gauge.rs +++ b/src/widgets/gauge.rs @@ -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>, ratio: f64, - label: Option>, + label: Option>, line_set: symbols::line::Set, style: Style, gauge_style: Style, @@ -215,7 +215,7 @@ impl<'a> LineGauge<'a> { pub fn label(mut self, label: T) -> Self where - T: Into>, + T: Into>, { 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, diff --git a/src/widgets/list.rs b/src/widgets/list.rs index 8b607454..d4457fef 100644 --- a/src/widgets/list.rs +++ b/src/widgets/list.rs @@ -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)), ]), diff --git a/src/widgets/paragraph.rs b/src/widgets/paragraph.rs index 963bc54e..f343fbd0 100644 --- a/src/widgets/paragraph.rs +++ b/src/widgets/paragraph.rs @@ -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)), ])) diff --git a/src/widgets/table.rs b/src/widgets/table.rs index 7af42316..11dd0d1f 100644 --- a/src/widgets/table.rs +++ b/src/widgets/table.rs @@ -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); } } diff --git a/src/widgets/tabs.rs b/src/widgets/tabs.rs index d5f1d159..fabcd2b4 100644 --- a/src/widgets/tabs.rs +++ b/src/widgets/tabs.rs @@ -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>, /// One title for each tab - titles: Vec>, + titles: Vec>, /// 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>) -> Tabs<'a> { + pub fn new(titles: Vec) -> Tabs<'a> + where + T: Into>, + { 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 { diff --git a/tests/widgets_list.rs b/tests/widgets_list.rs index b0478981..a25e8fed 100644 --- a/tests/widgets_list.rs +++ b/tests/widgets_list.rs @@ -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)) diff --git a/tests/widgets_paragraph.rs b/tests/widgets_paragraph.rs index 553a283e..3149a6e7 100644 --- a/tests/widgets_paragraph.rs +++ b/tests/widgets_paragraph.rs @@ -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![ diff --git a/tests/widgets_table.rs b/tests/widgets_table.rs index d884767b..096e80dc 100644 --- a/tests/widgets_table.rs +++ b/tests/widgets_table.rs @@ -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)), ])) diff --git a/tests/widgets_tabs.rs b/tests/widgets_tabs.rs index 2a8ddfcd..d3fcfae3 100644 --- a/tests/widgets_tabs.rs +++ b/tests/widgets_tabs.rs @@ -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 {