diff --git a/Cargo.toml b/Cargo.toml index f5504c7696..fee2da22a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ bitflags = "2.3" cassowary = "0.3" crossterm = { version = "0.26", optional = true } indoc = "2.0" +paste = "1.0" serde = { version = "1", optional = true, features = ["derive"] } termion = { version = "2.0", optional = true } termwiz = { version = "0.20.0", optional = true } diff --git a/Makefile.toml b/Makefile.toml index f4bcfe7f45..9d24a9b747 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -18,18 +18,18 @@ run_task = [ private = true dependencies = [ "style-check", + "clippy-unix", "check-unix", "test-unix", - "clippy-unix", ] [tasks.ci-windows] private = true dependencies = [ "style-check", + "clippy-windows", "check-windows", "test-windows", - "clippy-windows", ] [tasks.style-check] diff --git a/src/lib.rs b/src/lib.rs index 398946b9ac..301ccea191 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -206,7 +206,7 @@ pub mod prelude { backend::Backend, buffer::Buffer, layout::{Alignment, Constraint, Corner, Direction, Layout, Margin, Rect}, - style::{Color, Modifier, Style, Stylize}, + style::{Color, Modifier, Style, Styled, Stylize}, symbols::{self, Marker}, terminal::{Frame, Terminal, TerminalOptions, Viewport}, text::{Line, Masked, Span, Text}, diff --git a/src/style.rs b/src/style.rs index a4519efa64..0988ae5afd 100644 --- a/src/style.rs +++ b/src/style.rs @@ -15,20 +15,16 @@ //! //! # Using style shorthands //! -//! This is best for consise styling. +//! This is best for concise styling. //! ## Example //! ``` -//! use ratatui::{ -//! style::{Color, Modifier, Style, Styled, Stylize}, -//! text::Span, -//! }; +//! use ratatui::prelude::*; //! //! 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}, @@ -36,7 +32,9 @@ use std::{ }; use bitflags::bitflags; -pub use stylized::{Styled, Stylize}; + +mod stylize; +pub use stylize::{Styled, Stylize}; /// ANSI Color /// @@ -258,6 +256,17 @@ impl Default for Style { } } +impl Styled for Style { + type Item = Style; + + fn style(&self) -> Style { + *self + } + + fn set_style(self, style: Style) -> Self::Item { + self.patch(style) + } +} impl Style { pub const fn new() -> Style { Style { @@ -671,4 +680,151 @@ mod tests { .remove_modifier(Modifier::ITALIC) ) } + + #[test] + fn style_can_be_stylized() { + // foreground colors + assert_eq!(Style::new().black(), Style::new().fg(Color::Black)); + assert_eq!(Style::new().red(), Style::new().fg(Color::Red)); + assert_eq!(Style::new().green(), Style::new().fg(Color::Green)); + assert_eq!(Style::new().yellow(), Style::new().fg(Color::Yellow)); + assert_eq!(Style::new().blue(), Style::new().fg(Color::Blue)); + assert_eq!(Style::new().magenta(), Style::new().fg(Color::Magenta)); + assert_eq!(Style::new().cyan(), Style::new().fg(Color::Cyan)); + assert_eq!(Style::new().white(), Style::new().fg(Color::White)); + assert_eq!(Style::new().gray(), Style::new().fg(Color::Gray)); + assert_eq!(Style::new().dark_gray(), Style::new().fg(Color::DarkGray)); + assert_eq!(Style::new().light_red(), Style::new().fg(Color::LightRed)); + assert_eq!( + Style::new().light_green(), + Style::new().fg(Color::LightGreen) + ); + assert_eq!( + Style::new().light_yellow(), + Style::new().fg(Color::LightYellow) + ); + assert_eq!(Style::new().light_blue(), Style::new().fg(Color::LightBlue)); + assert_eq!( + Style::new().light_magenta(), + Style::new().fg(Color::LightMagenta) + ); + assert_eq!(Style::new().light_cyan(), Style::new().fg(Color::LightCyan)); + assert_eq!(Style::new().white(), Style::new().fg(Color::White)); + + // Background colors + assert_eq!(Style::new().on_black(), Style::new().bg(Color::Black)); + assert_eq!(Style::new().on_red(), Style::new().bg(Color::Red)); + assert_eq!(Style::new().on_green(), Style::new().bg(Color::Green)); + assert_eq!(Style::new().on_yellow(), Style::new().bg(Color::Yellow)); + assert_eq!(Style::new().on_blue(), Style::new().bg(Color::Blue)); + assert_eq!(Style::new().on_magenta(), Style::new().bg(Color::Magenta)); + assert_eq!(Style::new().on_cyan(), Style::new().bg(Color::Cyan)); + assert_eq!(Style::new().on_white(), Style::new().bg(Color::White)); + assert_eq!(Style::new().on_gray(), Style::new().bg(Color::Gray)); + assert_eq!( + Style::new().on_dark_gray(), + Style::new().bg(Color::DarkGray) + ); + assert_eq!( + Style::new().on_light_red(), + Style::new().bg(Color::LightRed) + ); + assert_eq!( + Style::new().on_light_green(), + Style::new().bg(Color::LightGreen) + ); + assert_eq!( + Style::new().on_light_yellow(), + Style::new().bg(Color::LightYellow) + ); + assert_eq!( + Style::new().on_light_blue(), + Style::new().bg(Color::LightBlue) + ); + assert_eq!( + Style::new().on_light_magenta(), + Style::new().bg(Color::LightMagenta) + ); + assert_eq!( + Style::new().on_light_cyan(), + Style::new().bg(Color::LightCyan) + ); + assert_eq!(Style::new().on_white(), Style::new().bg(Color::White)); + + // Add Modifiers + assert_eq!( + Style::new().bold(), + Style::new().add_modifier(Modifier::BOLD) + ); + assert_eq!(Style::new().dim(), Style::new().add_modifier(Modifier::DIM)); + assert_eq!( + Style::new().italic(), + Style::new().add_modifier(Modifier::ITALIC) + ); + assert_eq!( + Style::new().underlined(), + Style::new().add_modifier(Modifier::UNDERLINED) + ); + assert_eq!( + Style::new().slow_blink(), + Style::new().add_modifier(Modifier::SLOW_BLINK) + ); + assert_eq!( + Style::new().rapid_blink(), + Style::new().add_modifier(Modifier::RAPID_BLINK) + ); + assert_eq!( + Style::new().reversed(), + Style::new().add_modifier(Modifier::REVERSED) + ); + assert_eq!( + Style::new().hidden(), + Style::new().add_modifier(Modifier::HIDDEN) + ); + assert_eq!( + Style::new().crossed_out(), + Style::new().add_modifier(Modifier::CROSSED_OUT) + ); + + // Remove Modifiers + assert_eq!( + Style::new().not_bold(), + Style::new().remove_modifier(Modifier::BOLD) + ); + assert_eq!( + Style::new().not_dim(), + Style::new().remove_modifier(Modifier::DIM) + ); + assert_eq!( + Style::new().not_italic(), + Style::new().remove_modifier(Modifier::ITALIC) + ); + assert_eq!( + Style::new().not_underlined(), + Style::new().remove_modifier(Modifier::UNDERLINED) + ); + assert_eq!( + Style::new().not_slow_blink(), + Style::new().remove_modifier(Modifier::SLOW_BLINK) + ); + assert_eq!( + Style::new().not_rapid_blink(), + Style::new().remove_modifier(Modifier::RAPID_BLINK) + ); + assert_eq!( + Style::new().not_reversed(), + Style::new().remove_modifier(Modifier::REVERSED) + ); + assert_eq!( + Style::new().not_hidden(), + Style::new().remove_modifier(Modifier::HIDDEN) + ); + assert_eq!( + Style::new().not_crossed_out(), + Style::new().remove_modifier(Modifier::CROSSED_OUT) + ); + + // reset + assert_eq!(Style::new().reset(), Style::reset()); + } } diff --git a/src/style/stylize.rs b/src/style/stylize.rs new file mode 100644 index 0000000000..281532f8da --- /dev/null +++ b/src/style/stylize.rs @@ -0,0 +1,260 @@ +use paste::paste; + +use crate::{ + style::{Color, Modifier, Style}, + text::Span, +}; + +/// A trait for objects that have a `Style`. +/// +/// This trait enables generic code to be written that can interact with any object that has a +/// `Style`. This is used by the `Stylize` trait to allow generic code to be written that can +/// interact with any object that can be styled. +pub trait Styled { + type Item; + + fn style(&self) -> Style; + fn set_style(self, style: Style) -> Self::Item; +} + +/// Generates two methods for each color, one for setting the foreground color (`red()`, `blue()`, +/// etc) and one for setting the background color (`on_red()`, `on_blue()`, etc.). Each method sets +/// the color of the style to the corresponding color. +/// +/// ```rust,ignore +/// color!(black); +/// +/// // generates +/// +/// #[doc = "Sets the foreground color to [`black`](Color::Black)."] +/// fn black(self) -> T { +/// self.fg(Color::Black) +/// } +/// +/// #[doc = "Sets the background color to [`black`](Color::Black)."] +/// fn on_black(self) -> T { +/// self.bg(Color::Black) +/// } +/// ``` +macro_rules! color { + ( $color:ident ) => { + paste! { + #[doc = "Sets the foreground color to [`" $color "`](Color::" $color:camel ")."] + fn $color(self) -> T { + self.fg(Color::[<$color:camel>]) + } + + #[doc = "Sets the background color to [`" $color "`](Color::" $color:camel ")."] + fn [](self) -> T { + self.bg(Color::[<$color:camel>]) + } + } + }; +} + +/// Generates a method for a modifier (`bold()`, `italic()`, etc.). Each method sets the modifier +/// of the style to the corresponding modifier. +/// +/// # Examples +/// +/// ```rust,ignore +/// modifier!(bold); +/// +/// // generates +/// +/// #[doc = "Adds the [`BOLD`](Modifier::BOLD) modifier."] +/// fn bold(self) -> T { +/// self.add_modifier(Modifier::BOLD) +/// } +/// +/// #[doc = "Removes the [`BOLD`](Modifier::BOLD) modifier."] +/// fn not_bold(self) -> T { +/// self.remove_modifier(Modifier::BOLD) +/// } +/// ``` +macro_rules! modifier { + ( $modifier:ident ) => { + paste! { + #[doc = "Adds the [`" $modifier:upper "`](Modifier::" $modifier:upper ") modifier."] + fn [<$modifier>](self) -> T { + self.add_modifier(Modifier::[<$modifier:upper>]) + } + } + + paste! { + #[doc = "Removes the [`" $modifier:upper "`](Modifier::" $modifier:upper ") modifier."] + fn [](self) -> T { + self.remove_modifier(Modifier::[<$modifier:upper>]) + } + } + }; +} + +/// 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 { + fn bg(self, color: Color) -> T; + fn fg>(self, color: S) -> T; + fn reset(self) -> T; + fn add_modifier(self, modifier: Modifier) -> T; + fn remove_modifier(self, modifier: Modifier) -> T; + + color!(black); + color!(red); + color!(green); + color!(yellow); + color!(blue); + color!(magenta); + color!(cyan); + color!(gray); + color!(dark_gray); + color!(light_red); + color!(light_green); + color!(light_yellow); + color!(light_blue); + color!(light_magenta); + color!(light_cyan); + color!(white); + + modifier!(bold); + modifier!(dim); + modifier!(italic); + modifier!(underlined); + modifier!(slow_blink); + modifier!(rapid_blink); + modifier!(reversed); + modifier!(hidden); + modifier!(crossed_out); +} + +impl<'a, T, U> Stylize<'a, T> for U +where + U: Styled, +{ + fn bg(self, color: Color) -> T { + let style = self.style().bg(color); + self.set_style(style) + } + + fn fg>(self, color: S) -> T { + let style = self.style().fg(color.into()); + self.set_style(style) + } + + fn add_modifier(self, modifier: Modifier) -> T { + let style = self.style().add_modifier(modifier); + self.set_style(style) + } + + fn remove_modifier(self, modifier: Modifier) -> T { + let style = self.style().remove_modifier(modifier); + self.set_style(style) + } + + fn reset(self) -> T { + self.set_style(Style::reset()) + } +} + +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().underlined().reset(), + Span::styled("hello", Style::reset()) + ) + } + + #[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() + .underlined() + .dim() + .slow_blink() + .crossed_out() + .reversed(), + Span::styled("hello", all_modifier_black) + ); + } +} diff --git a/src/style/stylized.rs b/src/style/stylized.rs deleted file mode 100644 index 84d9356e7e..0000000000 --- a/src/style/stylized.rs +++ /dev/null @@ -1,228 +0,0 @@ -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 0f3fc5251c..7cace4d5f7 100644 --- a/src/text.rs +++ b/src/text.rs @@ -244,8 +244,9 @@ impl<'a> Styled for Span<'a> { self.style } - fn set_style(self, style: Style) -> Self { - Self::styled(self.content, style) + fn set_style(mut self, style: Style) -> Self { + self.style = style; + self } } diff --git a/src/widgets/barchart/mod.rs b/src/widgets/barchart/mod.rs index 036923ac5e..9c9ee1f080 100644 --- a/src/widgets/barchart/mod.rs +++ b/src/widgets/barchart/mod.rs @@ -1,10 +1,4 @@ -use crate::{ - buffer::Buffer, - layout::Rect, - style::Style, - symbols, - widgets::{Block, Widget}, -}; +use crate::prelude::*; mod bar; mod bar_group; @@ -25,9 +19,9 @@ pub use bar_group::BarGroup; /// .bar_width(3) /// .bar_gap(1) /// .group_gap(3) -/// .bar_style(Style::default().fg(Color::Yellow).bg(Color::Red)) -/// .value_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)) -/// .label_style(Style::default().fg(Color::White)) +/// .bar_style(Style::new().yellow().on_red()) +/// .value_style(Style::new().red().bold()) +/// .label_style(Style::new().white()) /// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)]) /// .data(BarGroup::default().bars(&[Bar::default().value(10), Bar::default().value(20)])) /// .max(4); @@ -328,16 +322,23 @@ impl<'a> Widget for BarChart<'a> { } } +impl<'a> Styled for BarChart<'a> { + type Item = BarChart<'a>; + fn style(&self) -> Style { + self.style + } + + fn set_style(self, style: Style) -> Self { + self.style(style) + } +} + #[cfg(test)] mod tests { use itertools::iproduct; use super::*; - use crate::{ - assert_buffer_eq, - style::Color, - widgets::{BorderType, Borders}, - }; + use crate::assert_buffer_eq; #[test] fn default() { @@ -417,7 +418,7 @@ mod tests { let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3)); let widget = BarChart::default() .data(&[("foo", 1), ("bar", 2)]) - .bar_style(Style::default().fg(Color::Red)); + .bar_style(Style::new().red()); widget.render(buffer.area, &mut buffer); let mut expected = Buffer::with_lines(vec![ " █ ", @@ -514,7 +515,7 @@ mod tests { let widget = BarChart::default() .data(&[("foo", 1), ("bar", 2)]) .bar_width(3) - .value_style(Style::default().fg(Color::Red)); + .value_style(Style::new().red()); widget.render(buffer.area, &mut buffer); let mut expected = Buffer::with_lines(vec![ " ███ ", @@ -531,7 +532,7 @@ mod tests { let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3)); let widget = BarChart::default() .data(&[("foo", 1), ("bar", 2)]) - .label_style(Style::default().fg(Color::Red)); + .label_style(Style::new().red()); widget.render(buffer.area, &mut buffer); let mut expected = Buffer::with_lines(vec![ " █ ", @@ -548,7 +549,7 @@ mod tests { let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3)); let widget = BarChart::default() .data(&[("foo", 1), ("bar", 2)]) - .style(Style::default().fg(Color::Red)); + .style(Style::new().red()); widget.render(buffer.area, &mut buffer); let mut expected = Buffer::with_lines(vec![ " █ ", @@ -720,4 +721,15 @@ mod tests { assert_eq!(barchart.label_height(), 0); } } + + #[test] + fn can_be_stylized() { + assert_eq!( + BarChart::default().black().on_white().bold().style, + Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::BOLD) + ) + } } diff --git a/src/widgets/block.rs b/src/widgets/block.rs index 998ae95634..7ea9b79034 100644 --- a/src/widgets/block.rs +++ b/src/widgets/block.rs @@ -514,7 +514,10 @@ impl<'a> Styled for Block<'a> { #[cfg(test)] mod tests { use super::*; - use crate::layout::Rect; + use crate::{ + layout::Rect, + style::{Color, Modifier, Stylize}, + }; #[test] fn inner_takes_into_account_the_borders() { @@ -872,4 +875,16 @@ mod tests { .style(_DEFAULT_STYLE) .padding(_DEFAULT_PADDING); } + + #[test] + fn can_be_stylized() { + assert_eq!( + Block::default().black().on_white().bold().not_dim().style, + Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::BOLD) + .remove_modifier(Modifier::DIM) + ) + } } diff --git a/src/widgets/chart.rs b/src/widgets/chart.rs index 761718f3a3..ad5f5216f0 100644 --- a/src/widgets/chart.rs +++ b/src/widgets/chart.rs @@ -5,7 +5,7 @@ use unicode_width::UnicodeWidthStr; use crate::{ buffer::Buffer, layout::{Alignment, Constraint, Rect}, - style::{Color, Style}, + style::{Color, Style, Styled}, symbols, text::{Line as TextLine, Span}, widgets::{ @@ -612,9 +612,46 @@ impl<'a> Widget for Chart<'a> { } } +impl<'a> Styled for Axis<'a> { + type Item = Axis<'a>; + + fn style(&self) -> Style { + self.style + } + + fn set_style(self, style: Style) -> Self::Item { + self.style(style) + } +} + +impl<'a> Styled for Dataset<'a> { + type Item = Dataset<'a>; + + fn style(&self) -> Style { + self.style + } + + fn set_style(self, style: Style) -> Self::Item { + self.style(style) + } +} + +impl<'a> Styled for Chart<'a> { + type Item = Chart<'a>; + + fn style(&self) -> Style { + self.style + } + + fn set_style(self, style: Style) -> Self::Item { + self.style(style) + } +} + #[cfg(test)] mod tests { use super::*; + use crate::style::{Modifier, Stylize}; struct LegendTestCase { chart_area: Rect, @@ -652,4 +689,40 @@ mod tests { assert_eq!(layout.legend_area, case.legend_area); } } + + #[test] + fn axis_can_be_stylized() { + assert_eq!( + Axis::default().black().on_white().bold().not_dim().style, + Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::BOLD) + .remove_modifier(Modifier::DIM) + ) + } + + #[test] + fn dataset_can_be_stylized() { + assert_eq!( + Dataset::default().black().on_white().bold().not_dim().style, + Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::BOLD) + .remove_modifier(Modifier::DIM) + ) + } + + #[test] + fn chart_can_be_stylized() { + assert_eq!( + Chart::new(vec![]).black().on_white().bold().not_dim().style, + Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::BOLD) + .remove_modifier(Modifier::DIM) + ) + } } diff --git a/src/widgets/gauge.rs b/src/widgets/gauge.rs index 9f5d9939b0..92cb59d07f 100644 --- a/src/widgets/gauge.rs +++ b/src/widgets/gauge.rs @@ -1,7 +1,7 @@ use crate::{ buffer::Buffer, layout::Rect, - style::{Color, Style}, + style::{Color, Style, Styled}, symbols, text::{Line, Span}, widgets::{Block, Widget}, @@ -296,9 +296,34 @@ impl<'a> Widget for LineGauge<'a> { } } +impl<'a> Styled for Gauge<'a> { + type Item = Gauge<'a>; + + fn style(&self) -> Style { + self.style + } + + fn set_style(self, style: Style) -> Self::Item { + self.style(style) + } +} + +impl<'a> Styled for LineGauge<'a> { + type Item = LineGauge<'a>; + + fn style(&self) -> Style { + self.style + } + + fn set_style(self, style: Style) -> Self::Item { + self.style(style) + } +} + #[cfg(test)] mod tests { use super::*; + use crate::style::{Modifier, Stylize}; #[test] #[should_panic] @@ -317,4 +342,33 @@ mod tests { fn gauge_invalid_ratio_lower_bound() { Gauge::default().ratio(-0.5); } + + #[test] + fn gauge_can_be_stylized() { + assert_eq!( + Gauge::default().black().on_white().bold().not_dim().style, + Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::BOLD) + .remove_modifier(Modifier::DIM) + ) + } + + #[test] + fn line_gauge_can_be_stylized() { + assert_eq!( + LineGauge::default() + .black() + .on_white() + .bold() + .not_dim() + .style, + Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::BOLD) + .remove_modifier(Modifier::DIM) + ) + } } diff --git a/src/widgets/list.rs b/src/widgets/list.rs index 32189f291e..4f2bec852b 100644 --- a/src/widgets/list.rs +++ b/src/widgets/list.rs @@ -3,7 +3,7 @@ use unicode_width::UnicodeWidthStr; use crate::{ buffer::Buffer, layout::{Corner, Rect}, - style::Style, + style::{Style, Styled}, text::Text, widgets::{Block, StatefulWidget, Widget}, }; @@ -291,6 +291,30 @@ impl<'a> Widget for List<'a> { } } +impl<'a> Styled for List<'a> { + type Item = List<'a>; + + fn style(&self) -> Style { + self.style + } + + fn set_style(self, style: Style) -> Self::Item { + self.style(style) + } +} + +impl<'a> Styled for ListItem<'a> { + type Item = ListItem<'a>; + + fn style(&self) -> Style { + self.style + } + + fn set_style(self, style: Style) -> Self::Item { + self.style(style) + } +} + #[cfg(test)] mod tests { use std::borrow::Cow; @@ -298,7 +322,7 @@ mod tests { use super::*; use crate::{ assert_buffer_eq, - style::Color, + style::{Color, Modifier, Stylize}, text::{Line, Span}, widgets::{Borders, StatefulWidget, Widget}, }; @@ -880,4 +904,28 @@ mod tests { "did not scroll the selected item into view" ); } + + #[test] + fn list_can_be_stylized() { + assert_eq!( + List::new(vec![]).black().on_white().bold().not_dim().style, + Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::BOLD) + .remove_modifier(Modifier::DIM) + ) + } + + #[test] + fn list_item_can_be_stylized() { + assert_eq!( + ListItem::new("").black().on_white().bold().not_dim().style, + Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::BOLD) + .remove_modifier(Modifier::DIM) + ) + } } diff --git a/src/widgets/paragraph.rs b/src/widgets/paragraph.rs index 856a5ce09b..b6f2f793f0 100644 --- a/src/widgets/paragraph.rs +++ b/src/widgets/paragraph.rs @@ -214,7 +214,7 @@ mod test { use super::*; use crate::{ backend::TestBackend, - style::Color, + style::{Color, Modifier, Stylize}, text::{Line, Span}, widgets::Borders, Terminal, @@ -702,4 +702,16 @@ mod test { Buffer::with_lines(vec!["こんにちは, ", "世界! 😃 "]), ); } + + #[test] + fn can_be_stylized() { + assert_eq!( + Paragraph::new("").black().on_white().bold().not_dim().style, + Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::BOLD) + .remove_modifier(Modifier::DIM) + ) + } } diff --git a/src/widgets/sparkline.rs b/src/widgets/sparkline.rs index 9c36281809..d87a4ae328 100644 --- a/src/widgets/sparkline.rs +++ b/src/widgets/sparkline.rs @@ -3,7 +3,7 @@ use std::cmp::min; use crate::{ buffer::Buffer, layout::Rect, - style::Style, + style::{Style, Styled}, symbols, widgets::{Block, Widget}, }; @@ -89,6 +89,18 @@ impl<'a> Sparkline<'a> { } } +impl<'a> Styled for Sparkline<'a> { + type Item = Sparkline<'a>; + + fn style(&self) -> Style { + self.style + } + + fn set_style(self, style: Style) -> Self::Item { + self.style(style) + } +} + impl<'a> Widget for Sparkline<'a> { fn render(mut self, area: Rect, buf: &mut Buffer) { let spark_area = match self.block.take() { @@ -155,7 +167,11 @@ impl<'a> Widget for Sparkline<'a> { #[cfg(test)] mod tests { use super::*; - use crate::{assert_buffer_eq, buffer::Cell}; + use crate::{ + assert_buffer_eq, + buffer::Cell, + style::{Color, Modifier, Stylize}, + }; // Helper function to render a sparkline to a buffer with a given width // filled with x symbols to make it easier to assert on the result @@ -206,4 +222,21 @@ mod tests { let buffer = render(widget, 12); assert_buffer_eq!(buffer, Buffer::with_lines(vec!["xxx█▇▆▅▄▃▂▁ "])); } + + #[test] + fn can_be_stylized() { + assert_eq!( + Sparkline::default() + .black() + .on_white() + .bold() + .not_dim() + .style, + Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::BOLD) + .remove_modifier(Modifier::DIM) + ) + } } diff --git a/src/widgets/table.rs b/src/widgets/table.rs index a8dcc4d691..f640c365dc 100644 --- a/src/widgets/table.rs +++ b/src/widgets/table.rs @@ -3,7 +3,7 @@ use unicode_width::UnicodeWidthStr; use crate::{ buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, - style::Style, + style::{Style, Styled}, text::Text, widgets::{Block, StatefulWidget, Widget}, }; @@ -58,6 +58,18 @@ where } } +impl<'a> Styled for Cell<'a> { + type Item = Cell<'a>; + + fn style(&self) -> Style { + self.style + } + + fn set_style(self, style: Style) -> Self::Item { + self.style(style) + } +} + /// Holds data to be displayed in a [`Table`] widget. /// /// A [`Row`] is a collection of cells. It can be created from simple strings: @@ -136,6 +148,18 @@ impl<'a> Row<'a> { } } +impl<'a> Styled for Row<'a> { + type Item = Row<'a>; + + fn style(&self) -> Style { + self.style + } + + fn set_style(self, style: Style) -> Self::Item { + self.style(style) + } +} + /// A widget to display data in formatted columns. /// /// It is a collection of [`Row`]s, themselves composed of [`Cell`]s: @@ -336,6 +360,18 @@ impl<'a> Table<'a> { } } +impl<'a> Styled for Table<'a> { + type Item = Table<'a>; + + fn style(&self) -> Style { + self.style + } + + fn set_style(self, style: Style) -> Self::Item { + self.style(style) + } +} + #[derive(Debug, Clone, Default)] pub struct TableState { offset: usize, @@ -505,11 +541,59 @@ impl<'a> Widget for Table<'a> { #[cfg(test)] mod tests { - use super::*; + use std::vec; + use super::*; + use crate::style::{Color, Modifier, Style, Stylize}; #[test] #[should_panic] fn table_invalid_percentages() { Table::new(vec![]).widths(&[Constraint::Percentage(110)]); } + + #[test] + fn cell_can_be_stylized() { + assert_eq!( + Cell::from("").black().on_white().bold().not_dim().style, + Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::BOLD) + .remove_modifier(Modifier::DIM) + ) + } + + #[test] + fn row_can_be_stylized() { + assert_eq!( + Row::new(vec![Cell::from("")]) + .black() + .on_white() + .bold() + .not_italic() + .style, + Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::BOLD) + .remove_modifier(Modifier::ITALIC) + ) + } + + #[test] + fn table_can_be_stylized() { + assert_eq!( + Table::new(vec![Row::new(vec![Cell::from("")])]) + .black() + .on_white() + .bold() + .not_crossed_out() + .style, + Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::BOLD) + .remove_modifier(Modifier::CROSSED_OUT) + ) + } } diff --git a/src/widgets/tabs.rs b/src/widgets/tabs.rs index 1079707489..ec07f39bd1 100644 --- a/src/widgets/tabs.rs +++ b/src/widgets/tabs.rs @@ -1,7 +1,7 @@ use crate::{ buffer::Buffer, layout::Rect, - style::Style, + style::{Style, Styled}, symbols, text::{Line, Span}, widgets::{Block, Widget}, @@ -83,6 +83,18 @@ impl<'a> Tabs<'a> { } } +impl<'a> Styled for Tabs<'a> { + type Item = Tabs<'a>; + + fn style(&self) -> Style { + self.style + } + + fn set_style(self, style: Style) -> Self::Item { + self.style(style) + } +} + impl<'a> Widget for Tabs<'a> { fn render(mut self, area: Rect, buf: &mut Buffer) { buf.set_style(area, self.style); @@ -130,3 +142,26 @@ impl<'a> Widget for Tabs<'a> { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::style::{Color, Modifier, Stylize}; + + #[test] + fn can_be_stylized() { + assert_eq!( + Tabs::new(vec![""]) + .black() + .on_white() + .bold() + .not_italic() + .style, + Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::BOLD) + .remove_modifier(Modifier::ITALIC) + ) + } +} diff --git a/tests/stylize.rs b/tests/stylize.rs index 950fbe4524..cec27495a2 100644 --- a/tests/stylize.rs +++ b/tests/stylize.rs @@ -1,88 +1,114 @@ +use std::io; + use ratatui::{ backend::TestBackend, buffer::Buffer, layout::Rect, - style::{Color, Stylize}, - widgets::{Block, Borders, Paragraph}, + style::{Color, Style, Stylize}, + widgets::{BarChart, Block, Borders, Paragraph}, Terminal, }; #[test] -fn paragraph_block_styles() { - let backend = TestBackend::new(10, 1); - let mut terminal = Terminal::new(backend).unwrap(); +fn barchart_can_be_stylized() { + let barchart = BarChart::default() + .on_white() + .bar_style(Style::new().red()) + .bar_width(2) + .value_style(Style::new().green()) + .label_style(Style::new().blue()) + .data(&[("A", 1), ("B", 2), ("C", 3)]) + .max(3); + let area = Rect::new(0, 0, 9, 5); + let mut terminal = Terminal::new(TestBackend::new(9, 6)).unwrap(); terminal .draw(|f| { - let paragraph = Paragraph::new("Text".cyan()); - f.render_widget( - paragraph, - Rect { - x: 0, - y: 0, - width: 10, - height: 1, - }, - ); + f.render_widget(barchart, area); }) .unwrap(); - let mut expected = Buffer::with_lines(vec!["Text "]); - for x in 0..4 { - expected.get_mut(x, 0).set_fg(Color::Cyan); + let mut expected = Buffer::with_lines(vec![ + " ██ ", + " ▅▅ ██ ", + "▂▂ ██ ██ ", + "1█ 2█ 3█ ", + "A B C ", + " ", + ]); + for y in area.y..area.height { + // background + for x in area.x..area.width { + expected.get_mut(x, y).set_bg(Color::White); + } + // bars + for x in [0, 1, 3, 4, 6, 7].iter() { + expected.get_mut(*x, y).set_fg(Color::Red); + } + } + // values + for x in 0..3 { + expected.get_mut(x * 3, 3).set_fg(Color::Green); + } + // labels + for x in 0..3 { + expected.get_mut(x * 3, 4).set_fg(Color::Blue); + expected.get_mut(x * 3 + 1, 4).set_fg(Color::Reset); } terminal.backend().assert_buffer(&expected); } #[test] -fn block_styles() { - let backend = TestBackend::new(10, 10); - let mut terminal = Terminal::new(backend).unwrap(); +fn block_can_be_stylized() -> io::Result<()> { + let block = Block::default() + .title("Title".light_blue()) + .on_cyan() + .cyan() + .borders(Borders::ALL); - 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 area = Rect::new(0, 0, 8, 3); + let mut terminal = Terminal::new(TestBackend::new(11, 4))?; + terminal.draw(|f| { + f.render_widget(block, area); + })?; let mut expected = Buffer::with_lines(vec![ - "┌Title─┐ ", - "│ │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "└──────┘ ", - " ", - " ", + "┌Title─┐ ", + "│ │ ", + "└──────┘ ", + " ", ]); - for x in 0..8 { - for y in 0..8 { + for x in area.x..area.width { + for y in area.y..area.height { 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); + Ok(()) +} + +#[test] +fn paragraph_can_be_stylized() -> io::Result<()> { + let paragraph = Paragraph::new("Text".cyan()); + + let area = Rect::new(0, 0, 10, 1); + let mut terminal = Terminal::new(TestBackend::new(10, 1))?; + terminal.draw(|f| { + f.render_widget(paragraph, area); + })?; + + 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); + Ok(()) }