mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-25 14:10:31 +00:00
feat(text): improve concise debug view for Span,Line,Text,Style (#1410)
Improves https://github.com/ratatui/ratatui/pull/1383 The following now round trips when formatted for debug. This will make it easier to use insta when testing text related views of widgets. ```rust Text::from_iter([ Line::from("Hello, world!"), Line::from("How are you?").bold().left_aligned(), Line::from_iter([ Span::from("I'm "), Span::from("doing ").italic(), Span::from("great!").bold(), ]), ]).on_blue().italic().centered() ```
This commit is contained in:
parent
c32baa7cd8
commit
23c0d52c29
4 changed files with 216 additions and 107 deletions
93
src/style.rs
93
src/style.rs
|
@ -260,46 +260,7 @@ pub struct Style {
|
|||
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:?})"))?,
|
||||
}
|
||||
}
|
||||
self.fmt_stylize(f)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -489,6 +450,54 @@ impl Style {
|
|||
|
||||
self
|
||||
}
|
||||
|
||||
/// Formats the style in a way that can be copy-pasted into code using the style shorthands.
|
||||
///
|
||||
/// This is useful for debugging and for generating code snippets.
|
||||
pub(crate) fn fmt_stylize(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use fmt::Debug;
|
||||
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 From<Color> for Style {
|
||||
|
@ -638,6 +647,10 @@ mod tests {
|
|||
#[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()")]
|
||||
#[case(
|
||||
Style::new().red().on_blue().bold().italic().not_dim().not_hidden(),
|
||||
"Style::new().red().on_blue().bold().italic().not_dim().not_hidden()"
|
||||
)]
|
||||
fn debug(#[case] style: Style, #[case] expected: &'static str) {
|
||||
assert_eq!(format!("{style:?}"), expected);
|
||||
}
|
||||
|
|
|
@ -193,18 +193,28 @@ pub struct Line<'a> {
|
|||
|
||||
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();
|
||||
if self.spans.is_empty() {
|
||||
f.write_str("Line::default()")?;
|
||||
} else if self.spans.len() == 1 && self.spans[0].style == Style::default() {
|
||||
f.write_str(r#"Line::from(""#)?;
|
||||
f.write_str(&self.spans[0].content)?;
|
||||
f.write_str(r#"")"#)?;
|
||||
} else if self.spans.len() == 1 {
|
||||
f.write_str("Line::from(")?;
|
||||
self.spans[0].fmt(f)?;
|
||||
f.write_str(")")?;
|
||||
} else {
|
||||
f.write_str("Line::from_iter(")?;
|
||||
f.debug_list().entries(&self.spans).finish()?;
|
||||
f.write_str(")")?;
|
||||
}
|
||||
let mut debug = f.debug_struct("Line");
|
||||
if self.style != Style::default() {
|
||||
debug.field("style", &self.style);
|
||||
self.style.fmt_stylize(f)?;
|
||||
match self.alignment {
|
||||
Some(Alignment::Left) => write!(f, ".left_aligned()"),
|
||||
Some(Alignment::Center) => write!(f, ".centered()"),
|
||||
Some(Alignment::Right) => write!(f, ".right_aligned()"),
|
||||
None => Ok(()),
|
||||
}
|
||||
if let Some(alignment) = self.alignment {
|
||||
debug.field("alignment", &format!("Alignment::{alignment}"));
|
||||
}
|
||||
debug.field("spans", &self.spans).finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1590,4 +1600,49 @@ mod tests {
|
|||
assert_eq!(result, "Hello world!");
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::empty(Line::default(), "Line::default()")]
|
||||
#[case::raw(Line::raw("Hello, world!"), r#"Line::from("Hello, world!")"#)]
|
||||
#[case::styled(
|
||||
Line::styled("Hello, world!", Color::Yellow),
|
||||
r#"Line::from("Hello, world!").yellow()"#
|
||||
)]
|
||||
#[case::styled_complex(
|
||||
Line::from(String::from("Hello, world!")).green().on_blue().bold().italic().not_dim(),
|
||||
r#"Line::from("Hello, world!").green().on_blue().bold().italic().not_dim()"#
|
||||
)]
|
||||
#[case::styled_span(
|
||||
Line::from(Span::styled("Hello, world!", Color::Yellow)),
|
||||
r#"Line::from(Span::from("Hello, world!").yellow())"#
|
||||
)]
|
||||
#[case::styled_line_and_span(
|
||||
Line::from(vec![
|
||||
Span::styled("Hello", Color::Yellow),
|
||||
Span::styled(" world!", Color::Green),
|
||||
]).italic(),
|
||||
r#"Line::from_iter([Span::from("Hello").yellow(), Span::from(" world!").green()]).italic()"#
|
||||
)]
|
||||
#[case::spans_vec(
|
||||
Line::from(vec![
|
||||
Span::styled("Hello", Color::Blue),
|
||||
Span::styled(" world!", Color::Green),
|
||||
]),
|
||||
r#"Line::from_iter([Span::from("Hello").blue(), Span::from(" world!").green()])"#,
|
||||
)]
|
||||
#[case::left_aligned(
|
||||
Line::from("Hello, world!").left_aligned(),
|
||||
r#"Line::from("Hello, world!").left_aligned()"#
|
||||
)]
|
||||
#[case::centered(
|
||||
Line::from("Hello, world!").centered(),
|
||||
r#"Line::from("Hello, world!").centered()"#
|
||||
)]
|
||||
#[case::right_aligned(
|
||||
Line::from("Hello, world!").right_aligned(),
|
||||
r#"Line::from("Hello, world!").right_aligned()"#
|
||||
)]
|
||||
fn debug(#[case] line: Line, #[case] expected: &str) {
|
||||
assert_eq!(format!("{line:?}"), expected);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,13 +107,15 @@ pub struct Span<'a> {
|
|||
|
||||
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);
|
||||
if self.content.is_empty() {
|
||||
write!(f, "Span::default()")?;
|
||||
} else {
|
||||
write!(f, "Span::from({:?})", self.content)?;
|
||||
}
|
||||
f.debug_struct("Span")
|
||||
.field("style", &self.style)
|
||||
.field("content", &self.content)
|
||||
.finish()
|
||||
if self.style != Style::default() {
|
||||
self.style.fmt_stylize(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -505,7 +507,7 @@ impl fmt::Display for Span<'_> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::fixture;
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::{buffer::Cell, layout::Alignment, style::Stylize};
|
||||
|
@ -884,4 +886,16 @@ mod tests {
|
|||
Line::from(vec![Span::raw("test"), Span::raw("content")])
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::default(Span::default(), "Span::default()")]
|
||||
#[case::raw(Span::raw("test"), r#"Span::from("test")"#)]
|
||||
#[case::styled(Span::styled("test", Style::new().green()), r#"Span::from("test").green()"#)]
|
||||
#[case::styled_italic(
|
||||
Span::styled("test", Style::new().green().italic()),
|
||||
r#"Span::from("test").green().italic()"#
|
||||
)]
|
||||
fn debug(#[case] span: Span, #[case] expected: &str) {
|
||||
assert_eq!(format!("{span:?}"), expected);
|
||||
}
|
||||
}
|
||||
|
|
127
src/text/text.rs
127
src/text/text.rs
|
@ -202,19 +202,23 @@ pub struct Text<'a> {
|
|||
|
||||
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()
|
||||
if self.lines.is_empty() {
|
||||
f.write_str("Text::default()")?;
|
||||
} else if self.lines.len() == 1 {
|
||||
write!(f, "Text::from({:?})", self.lines[0])?;
|
||||
} 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()
|
||||
f.write_str("Text::from_iter(")?;
|
||||
f.debug_list().entries(self.lines.iter()).finish()?;
|
||||
f.write_str(")")?;
|
||||
}
|
||||
self.style.fmt_stylize(f)?;
|
||||
match self.alignment {
|
||||
Some(Alignment::Left) => f.write_str(".left_aligned()")?,
|
||||
Some(Alignment::Center) => f.write_str(".centered()")?,
|
||||
Some(Alignment::Right) => f.write_str(".right_aligned()")?,
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1315,45 +1319,68 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
mod debug {
|
||||
use super::*;
|
||||
#[rstest]
|
||||
#[case::default(Text::default(), "Text::default()")]
|
||||
// TODO jm: these could be improved to inspect the line / span if there's only one. e.g.
|
||||
// Text::from("Hello, world!") and Text::from("Hello, world!".blue()) but the current
|
||||
// implementation is good enough for now.
|
||||
#[case::raw(
|
||||
Text::raw("Hello, world!"),
|
||||
r#"Text::from(Line::from("Hello, world!"))"#
|
||||
)]
|
||||
#[case::styled(
|
||||
Text::styled("Hello, world!", Color::Yellow),
|
||||
r#"Text::from(Line::from("Hello, world!")).yellow()"#
|
||||
)]
|
||||
#[case::complex_styled(
|
||||
Text::from("Hello, world!").yellow().on_blue().bold().italic().not_dim().not_hidden(),
|
||||
r#"Text::from(Line::from("Hello, world!")).yellow().on_blue().bold().italic().not_dim().not_hidden()"#
|
||||
)]
|
||||
#[case::alignment(
|
||||
Text::from("Hello, world!").centered(),
|
||||
r#"Text::from(Line::from("Hello, world!")).centered()"#
|
||||
)]
|
||||
#[case::styled_alignment(
|
||||
Text::styled("Hello, world!", Color::Yellow).centered(),
|
||||
r#"Text::from(Line::from("Hello, world!")).yellow().centered()"#
|
||||
)]
|
||||
#[case::multiple_lines(
|
||||
Text::from(vec![
|
||||
Line::from("Hello, world!"),
|
||||
Line::from("How are you?")
|
||||
]),
|
||||
r#"Text::from_iter([Line::from("Hello, world!"), Line::from("How are you?")])"#
|
||||
)]
|
||||
fn debug(#[case] text: Text, #[case] expected: &str) {
|
||||
assert_eq!(format!("{text:?}"), expected);
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
#[test]
|
||||
fn debug_alternate() {
|
||||
let text = Text::from_iter([
|
||||
Line::from("Hello, world!"),
|
||||
Line::from("How are you?").bold().left_aligned(),
|
||||
Line::from_iter([
|
||||
Span::from("I'm "),
|
||||
Span::from("doing ").italic(),
|
||||
Span::from("great!").bold(),
|
||||
]),
|
||||
])
|
||||
.on_blue()
|
||||
.italic()
|
||||
.centered();
|
||||
assert_eq!(
|
||||
format!("{text:#?}"),
|
||||
indoc::indoc! {r#"
|
||||
Text::from_iter([
|
||||
Line::from("Hello, world!"),
|
||||
Line::from("How are you?").bold().left_aligned(),
|
||||
Line::from_iter([
|
||||
Span::from("I'm "),
|
||||
Span::from("doing ").italic(),
|
||||
Span::from("great!").bold(),
|
||||
]),
|
||||
]).on_blue().italic().centered()"#}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue