From bc10af5931d1c1ec58a4181c01807ed3c52051c6 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Thu, 26 Sep 2024 10:38:23 -0700 Subject: [PATCH] chore(style): make Debug output for Text/Line/Span/Style more concise (#1383) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Given: ```rust Text::from_iter([ Line::from("without line fields"), Line::from("with line fields").bold().centered(), Line::from_iter([ Span::from("without span fields"), Span::from("with span fields") .green() .on_black() .italic() .not_dim(), ]), ]) ``` Debug: ``` Text [Line [Span("without line fields")], Line { style: Style::new().add_modifier(Modifier::BOLD), alignment: Some(Center), spans: [Span("with line fields")] }, Line [Span("without span fields"), Span { style: Style::new().green().on_black().add_modifier(Modifier::ITALIC).remove_modifier(Modifier::DIM), content: "with span fields" }]] ``` Fixes: https://github.com/ratatui/ratatui/issues/1382 --------- Co-authored-by: Orhun Parmaksız Co-authored-by: Orhun Parmaksız --- src/style.rs | 62 +++++++++++++++++- src/style/color.rs | 6 ++ src/style/stylize.rs | 150 +++++++++++++++++++++++++++++++++++++++++++ src/text/line.rs | 25 ++++++-- src/text/span.rs | 18 +++++- src/text/text.rs | 70 ++++++++++++++++++-- 6 files changed, 318 insertions(+), 13 deletions(-) diff --git a/src/style.rs b/src/style.rs index fa083b11..1beedcca 100644 --- a/src/style.rs +++ b/src/style.rs @@ -72,6 +72,7 @@ use std::fmt; use bitflags::bitflags; pub use color::{Color, ParseColorError}; +use stylize::ColorDebugKind; pub use stylize::{Styled, Stylize}; mod color; @@ -223,7 +224,7 @@ impl fmt::Debug for Modifier { /// buffer[(0, 0)].style(), /// ); /// ``` -#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)] +#[derive(Default, Clone, Copy, Eq, PartialEq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Style { pub fg: Option, @@ -234,6 +235,55 @@ pub struct Style { pub sub_modifier: Modifier, } +/// A custom debug implementation that prints only the fields that are not the default, and unwraps +/// the `Option`s. +impl fmt::Debug for Style { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("Style::new()")?; + if let Some(fg) = self.fg { + fg.stylize_debug(ColorDebugKind::Foreground).fmt(f)?; + } + if let Some(bg) = self.bg { + bg.stylize_debug(ColorDebugKind::Background).fmt(f)?; + } + #[cfg(feature = "underline-color")] + if let Some(underline_color) = self.underline_color { + underline_color + .stylize_debug(ColorDebugKind::Underline) + .fmt(f)?; + } + for modifier in self.add_modifier.iter() { + match modifier { + Modifier::BOLD => f.write_str(".bold()")?, + Modifier::DIM => f.write_str(".dim()")?, + Modifier::ITALIC => f.write_str(".italic()")?, + Modifier::UNDERLINED => f.write_str(".underlined()")?, + Modifier::SLOW_BLINK => f.write_str(".slow_blink()")?, + Modifier::RAPID_BLINK => f.write_str(".rapid_blink()")?, + Modifier::REVERSED => f.write_str(".reversed()")?, + Modifier::HIDDEN => f.write_str(".hidden()")?, + Modifier::CROSSED_OUT => f.write_str(".crossed_out()")?, + _ => f.write_fmt(format_args!(".add_modifier(Modifier::{modifier:?})"))?, + } + } + for modifier in self.sub_modifier.iter() { + match modifier { + Modifier::BOLD => f.write_str(".not_bold()")?, + Modifier::DIM => f.write_str(".not_dim()")?, + Modifier::ITALIC => f.write_str(".not_italic()")?, + Modifier::UNDERLINED => f.write_str(".not_underlined()")?, + Modifier::SLOW_BLINK => f.write_str(".not_slow_blink()")?, + Modifier::RAPID_BLINK => f.write_str(".not_rapid_blink()")?, + Modifier::REVERSED => f.write_str(".not_reversed()")?, + Modifier::HIDDEN => f.write_str(".not_hidden()")?, + Modifier::CROSSED_OUT => f.write_str(".not_crossed_out()")?, + _ => f.write_fmt(format_args!(".remove_modifier(Modifier::{modifier:?})"))?, + } + } + Ok(()) + } +} + impl Styled for Style { type Item = Self; @@ -549,6 +599,16 @@ mod tests { use super::*; + #[rstest] + #[case(Style::new(), "Style::new()")] + #[case(Style::new().red(), "Style::new().red()")] + #[case(Style::new().on_blue(), "Style::new().on_blue()")] + #[case(Style::new().bold(), "Style::new().bold()")] + #[case(Style::new().not_italic(), "Style::new().not_italic()")] + fn debug(#[case] style: Style, #[case] expected: &'static str) { + assert_eq!(format!("{style:?}"), expected); + } + #[test] fn combined_patch_gives_same_result_as_individual_patch() { let styles = [ diff --git a/src/style/color.rs b/src/style/color.rs index cb4ac9a7..8ef64132 100644 --- a/src/style/color.rs +++ b/src/style/color.rs @@ -2,6 +2,8 @@ use std::{fmt, str::FromStr}; +use crate::style::stylize::{ColorDebug, ColorDebugKind}; + /// ANSI Color /// /// All colors from the [ANSI color table] are supported (though some names are not exactly the @@ -361,6 +363,10 @@ impl fmt::Display for Color { } impl Color { + pub(crate) const fn stylize_debug(self, kind: ColorDebugKind) -> ColorDebug { + ColorDebug { kind, color: self } + } + /// Converts a HSL representation to a `Color::Rgb` instance. /// /// The `from_hsl` function converts the Hue, Saturation and Lightness values to a diff --git a/src/style/stylize.rs b/src/style/stylize.rs index 4be15c73..e44f6840 100644 --- a/src/style/stylize.rs +++ b/src/style/stylize.rs @@ -1,3 +1,5 @@ +use std::fmt; + use paste::paste; use crate::{ @@ -23,6 +25,75 @@ pub trait Styled { fn set_style>(self, style: S) -> Self::Item; } +/// A helper struct to make it easy to debug using the `Stylize` method names +pub(crate) struct ColorDebug { + pub kind: ColorDebugKind, + pub color: Color, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub(crate) enum ColorDebugKind { + Foreground, + Background, + #[cfg(feature = "underline-color")] + Underline, +} + +impl fmt::Debug for ColorDebug { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + #[cfg(feature = "underline-color")] + let is_underline = self.kind == ColorDebugKind::Underline; + #[cfg(not(feature = "underline-color"))] + let is_underline = false; + if is_underline + || matches!( + self.color, + Color::Reset | Color::Indexed(_) | Color::Rgb(_, _, _) + ) + { + match self.kind { + ColorDebugKind::Foreground => write!(f, ".fg(")?, + ColorDebugKind::Background => write!(f, ".bg(")?, + #[cfg(feature = "underline-color")] + ColorDebugKind::Underline => write!(f, ".underline_color(")?, + } + write!(f, "Color::{:?}", self.color)?; + write!(f, ")")?; + return Ok(()); + } + + match self.kind { + ColorDebugKind::Foreground => write!(f, ".")?, + ColorDebugKind::Background => write!(f, ".on_")?, + // TODO: .underline_color_xxx is not implemented on Stylize yet, but it should be + #[cfg(feature = "underline-color")] + ColorDebugKind::Underline => { + unreachable!("covered by the first part of the if statement") + } + } + match self.color { + Color::Black => write!(f, "black")?, + Color::Red => write!(f, "red")?, + Color::Green => write!(f, "green")?, + Color::Yellow => write!(f, "yellow")?, + Color::Blue => write!(f, "blue")?, + Color::Magenta => write!(f, "magenta")?, + Color::Cyan => write!(f, "cyan")?, + Color::Gray => write!(f, "gray")?, + Color::DarkGray => write!(f, "dark_gray")?, + Color::LightRed => write!(f, "light_red")?, + Color::LightGreen => write!(f, "light_green")?, + Color::LightYellow => write!(f, "light_yellow")?, + Color::LightBlue => write!(f, "light_blue")?, + Color::LightMagenta => write!(f, "light_magenta")?, + Color::LightCyan => write!(f, "light_cyan")?, + Color::White => write!(f, "white")?, + _ => unreachable!("covered by the first part of the if statement"), + } + write!(f, "()") + } +} + /// 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. @@ -231,6 +302,7 @@ impl Styled for String { #[cfg(test)] mod tests { use itertools::Itertools; + use rstest::rstest; use super::*; @@ -423,4 +495,82 @@ mod tests { Span::styled("hello", all_modifier_black) ); } + + #[rstest] + #[case(ColorDebugKind::Foreground, Color::Black, ".black()")] + #[case(ColorDebugKind::Foreground, Color::Red, ".red()")] + #[case(ColorDebugKind::Foreground, Color::Green, ".green()")] + #[case(ColorDebugKind::Foreground, Color::Yellow, ".yellow()")] + #[case(ColorDebugKind::Foreground, Color::Blue, ".blue()")] + #[case(ColorDebugKind::Foreground, Color::Magenta, ".magenta()")] + #[case(ColorDebugKind::Foreground, Color::Cyan, ".cyan()")] + #[case(ColorDebugKind::Foreground, Color::Gray, ".gray()")] + #[case(ColorDebugKind::Foreground, Color::DarkGray, ".dark_gray()")] + #[case(ColorDebugKind::Foreground, Color::LightRed, ".light_red()")] + #[case(ColorDebugKind::Foreground, Color::LightGreen, ".light_green()")] + #[case(ColorDebugKind::Foreground, Color::LightYellow, ".light_yellow()")] + #[case(ColorDebugKind::Foreground, Color::LightBlue, ".light_blue()")] + #[case(ColorDebugKind::Foreground, Color::LightMagenta, ".light_magenta()")] + #[case(ColorDebugKind::Foreground, Color::LightCyan, ".light_cyan()")] + #[case(ColorDebugKind::Foreground, Color::White, ".white()")] + #[case( + ColorDebugKind::Foreground, + Color::Indexed(10), + ".fg(Color::Indexed(10))" + )] + #[case( + ColorDebugKind::Foreground, + Color::Rgb(255, 0, 0), + ".fg(Color::Rgb(255, 0, 0))" + )] + #[case(ColorDebugKind::Background, Color::Black, ".on_black()")] + #[case(ColorDebugKind::Background, Color::Red, ".on_red()")] + #[case(ColorDebugKind::Background, Color::Green, ".on_green()")] + #[case(ColorDebugKind::Background, Color::Yellow, ".on_yellow()")] + #[case(ColorDebugKind::Background, Color::Blue, ".on_blue()")] + #[case(ColorDebugKind::Background, Color::Magenta, ".on_magenta()")] + #[case(ColorDebugKind::Background, Color::Cyan, ".on_cyan()")] + #[case(ColorDebugKind::Background, Color::Gray, ".on_gray()")] + #[case(ColorDebugKind::Background, Color::DarkGray, ".on_dark_gray()")] + #[case(ColorDebugKind::Background, Color::LightRed, ".on_light_red()")] + #[case(ColorDebugKind::Background, Color::LightGreen, ".on_light_green()")] + #[case(ColorDebugKind::Background, Color::LightYellow, ".on_light_yellow()")] + #[case(ColorDebugKind::Background, Color::LightBlue, ".on_light_blue()")] + #[case(ColorDebugKind::Background, Color::LightMagenta, ".on_light_magenta()")] + #[case(ColorDebugKind::Background, Color::LightCyan, ".on_light_cyan()")] + #[case(ColorDebugKind::Background, Color::White, ".on_white()")] + #[case( + ColorDebugKind::Background, + Color::Indexed(10), + ".bg(Color::Indexed(10))" + )] + #[case( + ColorDebugKind::Background, + Color::Rgb(255, 0, 0), + ".bg(Color::Rgb(255, 0, 0))" + )] + #[cfg(feature = "underline-color")] + #[case( + ColorDebugKind::Underline, + Color::Black, + ".underline_color(Color::Black)" + )] + #[cfg(feature = "underline-color")] + #[case(ColorDebugKind::Underline, Color::Red, ".underline_color(Color::Red)")] + #[cfg(feature = "underline-color")] + #[case( + ColorDebugKind::Underline, + Color::Green, + ".underline_color(Color::Green)" + )] + #[cfg(feature = "underline-color")] + #[case( + ColorDebugKind::Underline, + Color::Yellow, + ".underline_color(Color::Yellow)" + )] + fn stylize_debug(#[case] kind: ColorDebugKind, #[case] color: Color, #[case] expected: &str) { + let debug = color.stylize_debug(kind); + assert_eq!(format!("{debug:?}"), expected); + } } diff --git a/src/text/line.rs b/src/text/line.rs index 3082635d..ffa7700f 100644 --- a/src/text/line.rs +++ b/src/text/line.rs @@ -149,16 +149,33 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme}; /// ``` /// /// [`Paragraph`]: crate::widgets::Paragraph -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +#[derive(Default, Clone, Eq, PartialEq, Hash)] pub struct Line<'a> { - /// The spans that make up this line of text. - pub spans: Vec>, - /// The style of this line of text. pub style: Style, /// The alignment of this line of text. pub alignment: Option, + + /// The spans that make up this line of text. + pub spans: Vec>, +} + +impl fmt::Debug for Line<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.style == Style::default() && self.alignment.is_none() { + f.write_str("Line ")?; + return f.debug_list().entries(&self.spans).finish(); + } + let mut debug = f.debug_struct("Line"); + if self.style != Style::default() { + debug.field("style", &self.style); + } + if let Some(alignment) = self.alignment { + debug.field("alignment", &format!("Alignment::{alignment}")); + } + debug.field("spans", &self.spans).finish() + } } fn cow_to_spans<'a>(content: impl Into>) -> Vec> { diff --git a/src/text/span.rs b/src/text/span.rs index 77795c18..627b86dd 100644 --- a/src/text/span.rs +++ b/src/text/span.rs @@ -88,12 +88,24 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme}; /// [`Paragraph`]: crate::widgets::Paragraph /// [`Stylize`]: crate::style::Stylize /// [`Cow`]: std::borrow::Cow -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +#[derive(Default, Clone, Eq, PartialEq, Hash)] pub struct Span<'a> { - /// The content of the span as a Clone-on-write string. - pub content: Cow<'a, str>, /// The style of the span. pub style: Style, + /// The content of the span as a Clone-on-write string. + pub content: Cow<'a, str>, +} + +impl fmt::Debug for Span<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.style == Style::default() { + return write!(f, "Span({:?})", self.content); + } + f.debug_struct("Span") + .field("style", &self.style) + .field("content", &self.content) + .finish() + } } impl<'a> Span<'a> { diff --git a/src/text/text.rs b/src/text/text.rs index e7da458f..b8e3323e 100644 --- a/src/text/text.rs +++ b/src/text/text.rs @@ -163,14 +163,32 @@ use crate::{prelude::*, style::Styled}; /// ``` /// /// [`Paragraph`]: crate::widgets::Paragraph -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +#[derive(Default, Clone, Eq, PartialEq, Hash)] pub struct Text<'a> { - /// The lines that make up this piece of text. - pub lines: Vec>, - /// The style of this text. - pub style: Style, /// The alignment of this text. pub alignment: Option, + /// The style of this text. + pub style: Style, + /// The lines that make up this piece of text. + pub lines: Vec>, +} + +impl fmt::Debug for Text<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.style == Style::default() && self.alignment.is_none() { + f.write_str("Text ")?; + f.debug_list().entries(&self.lines).finish() + } else { + let mut debug = f.debug_struct("Text"); + if self.style != Style::default() { + debug.field("style", &self.style); + } + if let Some(alignment) = self.alignment { + debug.field("alignment", &format!("Alignment::{alignment}")); + } + debug.field("lines", &self.lines).finish() + } + } } impl<'a> Text<'a> { @@ -1232,4 +1250,46 @@ mod tests { assert_eq!(result, "Hello world!"); } } + + mod debug { + use super::*; + + #[test] + #[ignore = "This is just showing the debug output of the assertions"] + fn no_style() { + let text = Text::from("single unstyled line"); + assert_eq!(text, Text::default()); + } + + #[test] + #[ignore = "This is just showing the debug output of the assertions"] + fn text_style() { + let text = Text::from("single styled line") + .red() + .on_black() + .bold() + .not_italic(); + assert_eq!(text, Text::default()); + } + + #[test] + #[ignore = "This is just showing the debug output of the assertions"] + fn line_style() { + let text = Text::from(vec![ + Line::from("first line").red().alignment(Alignment::Right), + Line::from("second line").on_black(), + ]); + assert_eq!(text, Text::default()); + } + + #[test] + #[ignore = "This is just showing the debug output of the assertions"] + fn span_style() { + let text = Text::from(Line::from(vec![ + Span::from("first span").red(), + Span::from("second span").on_black(), + ])); + assert_eq!(text, Text::default()); + } + } }