feat(stylize): allow all widgets to be styled (#289)

* feat(stylize): allow all widgets to be styled

- Add styled impl to:
  - Barchart
  - Chart (including Axis and Dataset),
  - Guage and LineGuage
  - List and ListItem
  - Sparkline
  - Table, Row, and Cell
  - Tabs
  - Style
- Allow modifiers to be removed (e.g. .not_italic())
- Allow .bg() to recieve Into<Color>
- Made shorthand methods consistent with modifier names (e.g. dim() not
  dimmed() and underlined() not underline())
- Simplify integration tests
- Add doc comments
- Simplified stylize macros with https://crates.io/crates/paste

* build: run clippy before tests

Runny clippy first means that we fail fast when there is an issue that
can easily be fixed rather than having to wait 30-40s for the failure
This commit is contained in:
Josh McKinney 2023-07-14 01:37:30 -07:00 committed by GitHub
parent 6f6c355c5c
commit 9f1f59a51c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 904 additions and 322 deletions

View file

@ -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 }

View file

@ -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]

View file

@ -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},

View file

@ -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());
}
}

260
src/style/stylize.rs Normal file
View file

@ -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 [<on_ $color>](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 [<not_ $modifier>](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<S: Into<Color>>(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<Item = T>,
{
fn bg(self, color: Color) -> T {
let style = self.style().bg(color);
self.set_style(style)
}
fn fg<S: Into<Color>>(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)
);
}
}

View file

@ -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<S: Into<Color>>(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<Item = T>,
{
fn fg<S: Into<Color>>(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)
);
}
}

View file

@ -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
}
}

View file

@ -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)
)
}
}

View file

@ -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)
)
}
}

View file

@ -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)
)
}
}

View file

@ -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)
)
}
}

View file

@ -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)
)
}
}

View file

@ -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)
)
}
}

View file

@ -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)
)
}
}

View file

@ -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)
)
}
}

View file

@ -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)
)
}
}

View file

@ -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(())
}