diff --git a/examples/block.rs b/examples/block.rs index d5cbca6c..27aa84b2 100644 --- a/examples/block.rs +++ b/examples/block.rs @@ -8,8 +8,7 @@ use crossterm::{ use ratatui::{ backend::{Backend, CrosstermBackend}, layout::{Alignment, Constraint, Direction, Layout}, - style::{Color, Modifier, Style}, - text::Span, + style::{Color, Style, Stylize}, widgets::{block::title::Title, Block, BorderType, Borders, Padding, Paragraph}, Frame, Terminal, }; @@ -80,24 +79,13 @@ fn ui(f: &mut Frame) { // Top left inner block with green background let block = Block::default() - .title(vec![ - Span::styled("With", Style::default().fg(Color::Yellow)), - Span::from(" background"), - ]) - .style(Style::default().bg(Color::Green)); + .title(vec!["With".yellow(), " background".into()]) + .on_green(); f.render_widget(block, top_chunks[0]); // Top right inner block with styled title aligned to the right - let block = Block::default().title( - Title::from(Span::styled( - "Styled title", - Style::default() - .fg(Color::White) - .bg(Color::Red) - .add_modifier(Modifier::BOLD), - )) - .alignment(Alignment::Right), - ); + let block = Block::default() + .title(Title::from("Styled title".white().on_red().bold()).alignment(Alignment::Right)); f.render_widget(block, top_chunks[1]); // Bottom two inner blocks diff --git a/examples/canvas.rs b/examples/canvas.rs index 0c99536d..5fc8d1ca 100644 --- a/examples/canvas.rs +++ b/examples/canvas.rs @@ -12,9 +12,8 @@ use crossterm::{ use ratatui::{ backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Style}, + style::{Color, Stylize}, symbols::Marker, - text::Span, widgets::{ canvas::{Canvas, Map, MapResolution, Rectangle}, Block, Borders, @@ -177,11 +176,7 @@ fn ui(f: &mut Frame, app: &App) { color: Color::White, resolution: MapResolution::High, }); - ctx.print( - app.x, - -app.y, - Span::styled("You are here", Style::default().fg(Color::Yellow)), - ); + ctx.print(app.x, -app.y, "You are here".yellow()); }) .x_bounds([-180.0, 180.0]) .y_bounds([-90.0, 90.0]); diff --git a/examples/chart.rs b/examples/chart.rs index 0bfb850d..7db2ab16 100644 --- a/examples/chart.rs +++ b/examples/chart.rs @@ -12,7 +12,7 @@ use crossterm::{ use ratatui::{ backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout}, - style::{Color, Modifier, Style}, + style::{Color, Modifier, Style, Stylize}, symbols, text::Span, widgets::{Axis, Block, Borders, Chart, Dataset, GraphType}, @@ -190,12 +190,7 @@ fn ui(f: &mut Frame, app: &App) { let chart = Chart::new(datasets) .block( Block::default() - .title(Span::styled( - "Chart 1", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )) + .title("Chart 1".cyan().bold()) .borders(Borders::ALL), ) .x_axis( @@ -209,11 +204,7 @@ fn ui(f: &mut Frame, app: &App) { Axis::default() .title("Y Axis") .style(Style::default().fg(Color::Gray)) - .labels(vec![ - Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)), - Span::raw("0"), - Span::styled("20", Style::default().add_modifier(Modifier::BOLD)), - ]) + .labels(vec!["-20".bold(), "0".into(), "20".bold()]) .bounds([-20.0, 20.0]), ); f.render_widget(chart, chunks[0]); @@ -227,12 +218,7 @@ fn ui(f: &mut Frame, app: &App) { let chart = Chart::new(datasets) .block( Block::default() - .title(Span::styled( - "Chart 2", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )) + .title("Chart 2".cyan().bold()) .borders(Borders::ALL), ) .x_axis( @@ -240,22 +226,14 @@ fn ui(f: &mut Frame, app: &App) { .title("X Axis") .style(Style::default().fg(Color::Gray)) .bounds([0.0, 5.0]) - .labels(vec![ - Span::styled("0", Style::default().add_modifier(Modifier::BOLD)), - Span::raw("2.5"), - Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)), - ]), + .labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]), ) .y_axis( Axis::default() .title("Y Axis") .style(Style::default().fg(Color::Gray)) .bounds([0.0, 5.0]) - .labels(vec![ - Span::styled("0", Style::default().add_modifier(Modifier::BOLD)), - Span::raw("2.5"), - Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)), - ]), + .labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]), ); f.render_widget(chart, chunks[1]); @@ -268,12 +246,7 @@ fn ui(f: &mut Frame, app: &App) { let chart = Chart::new(datasets) .block( Block::default() - .title(Span::styled( - "Chart 3", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )) + .title("Chart 3".cyan().bold()) .borders(Borders::ALL), ) .x_axis( @@ -281,22 +254,14 @@ fn ui(f: &mut Frame, app: &App) { .title("X Axis") .style(Style::default().fg(Color::Gray)) .bounds([0.0, 50.0]) - .labels(vec![ - Span::styled("0", Style::default().add_modifier(Modifier::BOLD)), - Span::raw("25"), - Span::styled("50", Style::default().add_modifier(Modifier::BOLD)), - ]), + .labels(vec!["0".bold(), "25".into(), "50".bold()]), ) .y_axis( Axis::default() .title("Y Axis") .style(Style::default().fg(Color::Gray)) .bounds([0.0, 5.0]) - .labels(vec![ - Span::styled("0", Style::default().add_modifier(Modifier::BOLD)), - Span::raw("2.5"), - Span::styled("5", Style::default().add_modifier(Modifier::BOLD)), - ]), + .labels(vec!["0".bold(), "2.5".into(), "5".bold()]), ); f.render_widget(chart, chunks[2]); } diff --git a/examples/list.rs b/examples/list.rs index 66fa60fa..5ac57083 100644 --- a/examples/list.rs +++ b/examples/list.rs @@ -12,7 +12,7 @@ use crossterm::{ use ratatui::{ backend::{Backend, CrosstermBackend}, layout::{Constraint, Corner, Direction, Layout}, - style::{Color, Modifier, Style}, + style::{Color, Modifier, Style, Stylize}, text::{Line, Span}, widgets::{Block, Borders, List, ListItem, ListState}, Frame, Terminal, @@ -220,10 +220,11 @@ fn ui(f: &mut Frame, app: &mut App) { .map(|i| { let mut lines = vec![Line::from(i.0)]; for _ in 0..i.1 { - lines.push(Line::from(Span::styled( - "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - Style::default().add_modifier(Modifier::ITALIC), - ))); + lines.push( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + .italic() + .into(), + ); } ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White)) }) @@ -260,14 +261,11 @@ fn ui(f: &mut Frame, app: &mut App) { // Add a example datetime and apply proper spacing between them let header = Line::from(vec![ Span::styled(format!("{level:<9}"), s), - Span::raw(" "), - Span::styled( - "2020-01-01 10:00:00", - Style::default().add_modifier(Modifier::ITALIC), - ), + " ".into(), + "2020-01-01 10:00:00".italic(), ]); // The event gets its own line - let log = Line::from(vec![Span::raw(event)]); + let log = Line::from(vec![event.into()]); // Here several things happen: // 1. Add a `---` spacing line above the final list entry diff --git a/examples/paragraph.rs b/examples/paragraph.rs index add8ce61..65d7dfde 100644 --- a/examples/paragraph.rs +++ b/examples/paragraph.rs @@ -12,7 +12,7 @@ use crossterm::{ use ratatui::{ backend::{Backend, CrosstermBackend}, layout::{Alignment, Constraint, Direction, Layout}, - style::{Color, Modifier, Style}, + style::{Color, Modifier, Style, Stylize}, text::{Line, Masked, Span}, widgets::{Block, Borders, Paragraph, Wrap}, Frame, Terminal, @@ -96,7 +96,7 @@ fn ui(f: &mut Frame, app: &App) { let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4); long_line.push('\n'); - let block = Block::default().style(Style::default().fg(Color::Black)); + let block = Block::default().black(); f.render_widget(block, size); let chunks = Layout::default() @@ -115,27 +115,13 @@ fn ui(f: &mut Frame, app: &App) { let text = vec![ Line::from("This is a line "), - Line::from(Span::styled( - "This is a line ", - Style::default().fg(Color::Red), - )), - Line::from(Span::styled( - "This is a line", - Style::default().bg(Color::Blue), - )), - Line::from(Span::styled( - "This is a longer line", - Style::default().add_modifier(Modifier::CROSSED_OUT), - )), - 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), - )), + Line::from("This is a line ".red()), + Line::from("This is a line".on_blue()), + Line::from("This is a longer line".crossed_out()), + Line::from(long_line.on_green()), + Line::from("This is a line".green().italic()), Line::from(vec![ - Span::raw("Masked text: "), + "Masked text: ".into(), Span::styled( Masked::new("password", '*'), Style::default().fg(Color::Red), diff --git a/examples/popup.rs b/examples/popup.rs index 70edde24..fa6888f9 100644 --- a/examples/popup.rs +++ b/examples/popup.rs @@ -8,8 +8,7 @@ use crossterm::{ use ratatui::{ backend::{Backend, CrosstermBackend}, layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - text::Span, + style::Stylize, widgets::{Block, Borders, Clear, Paragraph, Wrap}, Frame, Terminal, }; @@ -80,18 +79,15 @@ fn ui(f: &mut Frame, app: &App) { } else { "Press p to show the popup" }; - let paragraph = Paragraph::new(Span::styled( - text, - Style::default().add_modifier(Modifier::SLOW_BLINK), - )) - .alignment(Alignment::Center) - .wrap(Wrap { trim: true }); + let paragraph = Paragraph::new(text.slow_blink()) + .alignment(Alignment::Center) + .wrap(Wrap { trim: true }); f.render_widget(paragraph, chunks[0]); let block = Block::default() .title("Content") .borders(Borders::ALL) - .style(Style::default().bg(Color::Blue)); + .on_blue(); f.render_widget(block, chunks[1]); if app.show_popup { diff --git a/examples/scrollbar.rs b/examples/scrollbar.rs index e3c61b24..883d289c 100644 --- a/examples/scrollbar.rs +++ b/examples/scrollbar.rs @@ -12,7 +12,7 @@ use crossterm::{ use ratatui::{ backend::{Backend, CrosstermBackend}, layout::{Alignment, Constraint, Direction, Layout, Margin}, - style::{Color, Modifier, Style}, + style::{Color, Modifier, Style, Stylize}, text::{Line, Masked, Span}, widgets::{ scrollbar, Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, @@ -115,7 +115,7 @@ fn ui(f: &mut Frame, app: &mut App) { let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4); long_line.push('\n'); - let block = Block::default().style(Style::default().fg(Color::Black)); + let block = Block::default().black(); f.render_widget(block, size); let chunks = Layout::default() @@ -135,20 +135,11 @@ fn ui(f: &mut Frame, app: &mut App) { let text = vec![ Line::from("This is a line "), - Line::from(Span::styled( - "This is a line ", - Style::default().fg(Color::Red), - )), - Line::from(Span::styled( - "This is a line", - Style::default().bg(Color::DarkGray), - )), - Line::from(Span::styled( - "This is a longer line", - Style::default().add_modifier(Modifier::CROSSED_OUT), - )), - Line::from(Span::styled(&long_line, Style::default())), - Line::from(Span::styled("This is a line", Style::default())), + Line::from("This is a line ".red()), + Line::from("This is a line".on_dark_gray()), + Line::from("This is a longer line".crossed_out()), + Line::from(long_line.reset()), + Line::from("This is a line".reset()), Line::from(vec![ Span::raw("Masked text: "), Span::styled( @@ -157,20 +148,11 @@ fn ui(f: &mut Frame, app: &mut App) { ), ]), Line::from("This is a line "), - Line::from(Span::styled( - "This is a line ", - Style::default().fg(Color::Red), - )), - Line::from(Span::styled( - "This is a line", - Style::default().bg(Color::DarkGray), - )), - Line::from(Span::styled( - "This is a longer line", - Style::default().add_modifier(Modifier::CROSSED_OUT), - )), - Line::from(Span::styled(&long_line, Style::default())), - Line::from(Span::styled("This is a line", Style::default())), + Line::from("This is a line ".red()), + Line::from("This is a line".on_dark_gray()), + Line::from("This is a longer line".crossed_out()), + Line::from(long_line.reset()), + Line::from("This is a line".reset()), Line::from(vec![ Span::raw("Masked text: "), Span::styled( @@ -187,7 +169,7 @@ fn ui(f: &mut Frame, app: &mut App) { let create_block = |title| { Block::default() .borders(Borders::ALL) - .style(Style::default().fg(Color::Gray)) + .gray() .title(Span::styled( title, Style::default().add_modifier(Modifier::BOLD), @@ -200,7 +182,7 @@ fn ui(f: &mut Frame, app: &mut App) { f.render_widget(title, chunks[0]); let paragraph = Paragraph::new(text.clone()) - .style(Style::default().fg(Color::Gray)) + .gray() .block(create_block("Vertical scrollbar with arrows")) .scroll((app.vertical_scroll as u16, 0)); f.render_widget(paragraph, chunks[1]); @@ -214,7 +196,7 @@ fn ui(f: &mut Frame, app: &mut App) { ); let paragraph = Paragraph::new(text.clone()) - .style(Style::default().fg(Color::Gray)) + .gray() .block(create_block( "Vertical scrollbar without arrows and mirrored", )) @@ -234,7 +216,7 @@ fn ui(f: &mut Frame, app: &mut App) { ); let paragraph = Paragraph::new(text.clone()) - .style(Style::default().fg(Color::Gray)) + .gray() .block(create_block( "Horizontal scrollbar with only begin arrow & custom thumb symbol", )) @@ -253,7 +235,7 @@ fn ui(f: &mut Frame, app: &mut App) { ); let paragraph = Paragraph::new(text.clone()) - .style(Style::default().fg(Color::Gray)) + .gray() .block(create_block( "Horizontal scrollbar without arrows & custom thumb and track symbol", )) diff --git a/examples/tabs.rs b/examples/tabs.rs index d47c4771..dc8275df 100644 --- a/examples/tabs.rs +++ b/examples/tabs.rs @@ -8,8 +8,8 @@ use crossterm::{ use ratatui::{ backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout}, - style::{Color, Modifier, Style}, - text::{Line, Span}, + style::{Color, Modifier, Style, Stylize}, + text::Line, widgets::{Block, Borders, Tabs}, Frame, Terminal, }; @@ -93,17 +93,14 @@ fn ui(f: &mut Frame, app: &App) { .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) .split(size); - let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black)); + let block = Block::default().on_white().black(); f.render_widget(block, size); let titles = app .titles .iter() .map(|t| { let (first, rest) = t.split_at(1); - Line::from(vec![ - Span::styled(first, Style::default().fg(Color::Yellow)), - Span::styled(rest, Style::default().fg(Color::Green)), - ]) + Line::from(vec![first.yellow(), rest.green()]) }) .collect(); let tabs = Tabs::new(titles) diff --git a/examples/user_input.rs b/examples/user_input.rs index 767bce39..11c17ccc 100644 --- a/examples/user_input.rs +++ b/examples/user_input.rs @@ -19,7 +19,7 @@ use crossterm::{ use ratatui::{ backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout}, - style::{Color, Modifier, Style}, + style::{Color, Modifier, Style, Stylize}, text::{Line, Span, Text}, widgets::{Block, Borders, List, ListItem, Paragraph}, Frame, Terminal, @@ -132,21 +132,21 @@ fn ui(f: &mut Frame, app: &App) { let (msg, style) = match app.input_mode { InputMode::Normal => ( vec![ - Span::raw("Press "), - Span::styled("q", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to exit, "), - Span::styled("e", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to start editing."), + "Press ".into(), + "q".bold(), + " to exist, ".into(), + "e".bold(), + " to start editing.".bold(), ], Style::default().add_modifier(Modifier::RAPID_BLINK), ), InputMode::Editing => ( vec![ - Span::raw("Press "), - Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to stop editing, "), - Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to record the message"), + "Press ".into(), + "Esc".bold(), + " to stop editing, ".into(), + "Enter".bold(), + " to record the message".into(), ], Style::default(), ), diff --git a/src/style.rs b/src/style.rs index 83273570..56f2a41e 100644 --- a/src/style.rs +++ b/src/style.rs @@ -1,4 +1,34 @@ //! `style` contains the primitives used to control how your user interface will look. +//! +//! # Using the `Style` struct +//! +//! This is useful when creating style variables. +//! ## Example +//! ``` +//! use ratatui::style::{Color, Modifier, Style}; +//! +//! Style::default() +//! .fg(Color::Black) +//! .bg(Color::Green) +//! .add_modifier(Modifier::ITALIC | Modifier::BOLD); +//! ``` +//! +//! # Using style shorthands +//! +//! This is best for consise styling. +//! ## Example +//! ``` +//! use ratatui::{ +//! style::{Color, Modifier, Style, Styled, Stylize}, +//! text::Span, +//! }; +//! +//! assert_eq!( +//! "hello".red().on_blue().bold(), +//! Span::styled("hello", Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD)) +//! ) +//! ``` +mod stylized; use std::{ fmt::{self, Debug}, @@ -6,6 +36,7 @@ use std::{ }; use bitflags::bitflags; +pub use stylized::{Styled, Stylize}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/src/style/stylized.rs b/src/style/stylized.rs new file mode 100644 index 00000000..84d9356e --- /dev/null +++ b/src/style/stylized.rs @@ -0,0 +1,228 @@ +use crate::{ + style::{Color, Modifier, Style}, + text::Span, +}; + +pub trait Styled { + type Item; + + fn style(&self) -> Style; + fn set_style(self, style: Style) -> Self::Item; +} + +// Otherwise rustfmt behaves weirdly for some reason +macro_rules! calculated_docs { + ($(#[doc = $doc:expr] $item:item)*) => { $(#[doc = $doc] $item)* }; +} + +macro_rules! modifier_method { + ($method_name:ident Modifier::$modifier:ident) => { + calculated_docs! { + #[doc = concat!( + "Applies the [`", + stringify!($modifier), + "`](crate::style::Modifier::", + stringify!($modifier), + ") modifier.", + )] + fn $method_name(self) -> T { + self.modifier(Modifier::$modifier) + } + } + }; +} + +macro_rules! color_method { + ($method_name_fg:ident, $method_name_bg:ident Color::$color:ident) => { + calculated_docs! { + #[doc = concat!( + "Sets the foreground color to [`", + stringify!($color), + "`](Color::", + stringify!($color), + ")." + )] + fn $method_name_fg(self) -> T { + self.fg(Color::$color) + } + + #[doc = concat!( + "Sets the background color to [`", + stringify!($color), + "`](Color::", + stringify!($color), + ")." + )] + fn $method_name_bg(self) -> T { + self.bg(Color::$color) + } + } + }; +} + +/// The trait that enables something to be have a style. +/// # Examples +/// ``` +/// use ratatui::{ +/// style::{Color, Modifier, Style, Styled, Stylize}, +/// text::Span, +/// }; +/// +/// assert_eq!( +/// "hello".red().on_blue().bold(), +/// Span::styled("hello", Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD)) +/// ) +pub trait Stylize<'a, T>: Sized { + // Colors + fn fg>(self, color: S) -> T; + fn bg(self, color: Color) -> T; + + color_method!(black, on_black Color::Black); + color_method!(red, on_red Color::Red); + color_method!(green, on_green Color::Green); + color_method!(yellow, on_yellow Color::Yellow); + color_method!(blue, on_blue Color::Blue); + color_method!(magenta, on_magenta Color::Magenta); + color_method!(cyan, on_cyan Color::Cyan); + color_method!(gray, on_gray Color::Gray); + color_method!(dark_gray, on_dark_gray Color::DarkGray); + color_method!(light_red, on_light_red Color::LightRed); + color_method!(light_green, on_light_green Color::LightGreen); + color_method!(light_yellow, on_light_yellow Color::LightYellow); + color_method!(light_blue, on_light_blue Color::LightBlue); + color_method!(light_magenta, on_light_magenta Color::LightMagenta); + color_method!(light_cyan, on_light_cyan Color::LightCyan); + color_method!(white, on_white Color::White); + + // Styles + fn reset(self) -> T; + + // Modifiers + fn modifier(self, modifier: Modifier) -> T; + + modifier_method!(bold Modifier::BOLD); + modifier_method!(dimmed Modifier::DIM); + modifier_method!(italic Modifier::ITALIC); + modifier_method!(underline Modifier::UNDERLINED); + modifier_method!(slow_blink Modifier::SLOW_BLINK); + modifier_method!(rapid_blink Modifier::RAPID_BLINK); + modifier_method!(reversed Modifier::REVERSED); + modifier_method!(hidden Modifier::HIDDEN); + modifier_method!(crossed_out Modifier::CROSSED_OUT); +} + +impl<'a, T, U> Stylize<'a, T> for U +where + U: Styled, +{ + fn fg>(self, color: S) -> T { + let style = self.style().fg(color.into()); + self.set_style(style) + } + + fn modifier(self, modifier: Modifier) -> T { + let style = self.style().add_modifier(modifier); + self.set_style(style) + } + + fn bg(self, color: Color) -> T { + let style = self.style().bg(color); + self.set_style(style) + } + + fn reset(self) -> T { + self.set_style(Style::default()) + } +} + +impl<'a> Styled for &'a str { + type Item = Span<'a>; + + fn style(&self) -> Style { + Style::default() + } + + fn set_style(self, style: Style) -> Self::Item { + Span::styled(self, style) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reset() { + assert_eq!( + "hello".on_cyan().light_red().bold().underline().reset(), + Span::from("hello") + ) + } + + #[test] + fn fg() { + let cyan_fg = Style::default().fg(Color::Cyan); + + assert_eq!("hello".cyan(), Span::styled("hello", cyan_fg)); + } + + #[test] + fn bg() { + let cyan_bg = Style::default().bg(Color::Cyan); + + assert_eq!("hello".on_cyan(), Span::styled("hello", cyan_bg)); + } + + #[test] + fn color_modifier() { + let cyan_bold = Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD); + + assert_eq!("hello".cyan().bold(), Span::styled("hello", cyan_bold)) + } + + #[test] + fn fg_bg() { + let cyan_fg_bg = Style::default().bg(Color::Cyan).fg(Color::Cyan); + + assert_eq!("hello".cyan().on_cyan(), Span::styled("hello", cyan_fg_bg)) + } + + #[test] + fn repeated_attributes() { + let cyan_bg = Style::default().bg(Color::Cyan); + let cyan_fg = Style::default().fg(Color::Cyan); + + // Behavior: the last one set is the definitive one + assert_eq!("hello".on_red().on_cyan(), Span::styled("hello", cyan_bg)); + assert_eq!("hello".red().cyan(), Span::styled("hello", cyan_fg)); + } + + #[test] + fn all_chained() { + let all_modifier_black = Style::default() + .bg(Color::Black) + .fg(Color::Black) + .add_modifier( + Modifier::UNDERLINED + | Modifier::BOLD + | Modifier::DIM + | Modifier::SLOW_BLINK + | Modifier::REVERSED + | Modifier::CROSSED_OUT, + ); + assert_eq!( + "hello" + .on_black() + .black() + .bold() + .underline() + .dimmed() + .slow_blink() + .crossed_out() + .reversed(), + Span::styled("hello", all_modifier_black) + ); + } +} diff --git a/src/text.rs b/src/text.rs index 07ac0f41..0f3fc525 100644 --- a/src/text.rs +++ b/src/text.rs @@ -51,7 +51,7 @@ use std::{borrow::Cow, fmt::Debug}; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; -use crate::style::Style; +use crate::style::{Style, Styled}; mod line; mod masked; @@ -238,6 +238,17 @@ impl<'a> From<&'a str> for Span<'a> { } } +impl<'a> Styled for Span<'a> { + type Item = Span<'a>; + fn style(&self) -> Style { + self.style + } + + fn set_style(self, style: Style) -> Self { + Self::styled(self.content, style) + } +} + /// A string split over multiple lines where each line is composed of several clusters, each with /// their own style. /// diff --git a/src/widgets/block.rs b/src/widgets/block.rs index 06f708ab..ecb6371b 100644 --- a/src/widgets/block.rs +++ b/src/widgets/block.rs @@ -5,7 +5,7 @@ use self::title::{Position, Title}; use crate::{ buffer::Buffer, layout::{Alignment, Rect}, - style::Style, + style::{Style, Styled}, symbols::line, widgets::{Borders, Widget}, }; @@ -499,6 +499,18 @@ impl<'a> Widget for Block<'a> { } } +impl<'a> Styled for Block<'a> { + type Item = Block<'a>; + + fn style(&self) -> Style { + self.style + } + + fn set_style(self, style: Style) -> Self::Item { + self.style(style) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/widgets/paragraph.rs b/src/widgets/paragraph.rs index 1beb1701..856a5ce0 100644 --- a/src/widgets/paragraph.rs +++ b/src/widgets/paragraph.rs @@ -3,7 +3,7 @@ use unicode_width::UnicodeWidthStr; use crate::{ buffer::Buffer, layout::{Alignment, Rect}, - style::Style, + style::{Style, Styled}, text::{StyledGrapheme, Text}, widgets::{ reflow::{LineComposer, LineTruncator, WordWrapper}, @@ -197,6 +197,18 @@ impl<'a> Widget for Paragraph<'a> { } } +impl<'a> Styled for Paragraph<'a> { + type Item = Paragraph<'a>; + + fn style(&self) -> Style { + self.style + } + + fn set_style(self, style: Style) -> Self::Item { + self.style(style) + } +} + #[cfg(test)] mod test { use super::*; diff --git a/tests/stylize.rs b/tests/stylize.rs new file mode 100644 index 00000000..950fbe45 --- /dev/null +++ b/tests/stylize.rs @@ -0,0 +1,88 @@ +use ratatui::{ + backend::TestBackend, + buffer::Buffer, + layout::Rect, + style::{Color, Stylize}, + widgets::{Block, Borders, Paragraph}, + Terminal, +}; + +#[test] +fn paragraph_block_styles() { + let backend = TestBackend::new(10, 1); + let mut terminal = Terminal::new(backend).unwrap(); + + terminal + .draw(|f| { + let paragraph = Paragraph::new("Text".cyan()); + f.render_widget( + paragraph, + Rect { + x: 0, + y: 0, + width: 10, + height: 1, + }, + ); + }) + .unwrap(); + + let mut expected = Buffer::with_lines(vec!["Text "]); + for x in 0..4 { + expected.get_mut(x, 0).set_fg(Color::Cyan); + } + + terminal.backend().assert_buffer(&expected); +} + +#[test] +fn block_styles() { + let backend = TestBackend::new(10, 10); + let mut terminal = Terminal::new(backend).unwrap(); + + terminal + .draw(|f| { + let block = Block::default() + .title("Title".light_blue()) + .on_cyan() + .cyan() + .borders(Borders::ALL); + f.render_widget( + block, + Rect { + x: 0, + y: 0, + width: 8, + height: 8, + }, + ); + }) + .unwrap(); + + let mut expected = Buffer::with_lines(vec![ + "┌Title─┐ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "└──────┘ ", + " ", + " ", + ]); + for x in 0..8 { + for y in 0..8 { + expected + .get_mut(x, y) + .set_fg(Color::Cyan) + .set_bg(Color::Cyan); + } + } + + for x in 1..=5 { + expected.get_mut(x, 0).set_fg(Color::LightBlue); + } + + terminal.backend().assert_buffer(&expected); +}