mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-10 07:04:17 +00:00
feat: add WidgetExt trait for debugging widgets
Importing the `WidgetExt` trait allows users to easily get a string representation of a widget with ANSI escape sequences for the terminal. This is useful for debugging and testing widgets. ```rust use ratatui::{prelude::*, widgets::widget_ext::WidgetExt}; fn main() { let greeting = Text::from(vec![ Line::styled("Hello", Color::Blue), Line::styled("World ", Color::Green), ]); println!("{}", greeting.to_ansi_string(5, 2)); } ``` Fixes: https://github.com/ratatui-org/ratatui/issues/1045
This commit is contained in:
parent
97ee102f17
commit
0d54ca06f8
6 changed files with 252 additions and 0 deletions
|
@ -148,6 +148,9 @@ unstable-rendered-line-info = []
|
|||
## the future.
|
||||
unstable-widget-ref = []
|
||||
|
||||
## Enables the `WidgetExt` trait which is experimental and may change in the future.
|
||||
unstable-widget-ext = []
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
|
||||
|
@ -325,6 +328,11 @@ name = "inline"
|
|||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "widget_ext"
|
||||
required-features = ["unstable-widget-ext"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[test]]
|
||||
name = "state_serde"
|
||||
required-features = ["serde"]
|
||||
|
|
12
examples/vhs/widget_ext.tape
Normal file
12
examples/vhs/widget_ext.tape
Normal file
|
@ -0,0 +1,12 @@
|
|||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/hello_world.tape`
|
||||
Output "target/widget_ext.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 300
|
||||
Set TypingSpeed 10ms
|
||||
Type "cargo run --example=widget_ext --features=unstable-widget-ext"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Screenshot "target/widget_ext.png"
|
||||
Sleep 1s
|
9
examples/widget_ext.rs
Normal file
9
examples/widget_ext.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
use ratatui::{prelude::*, widgets::widget_ext::WidgetExt};
|
||||
|
||||
fn main() {
|
||||
let greeting = Text::from(vec![
|
||||
Line::styled("Hello", Color::Blue),
|
||||
Line::styled("World ", Color::Green),
|
||||
]);
|
||||
println!("{}", greeting.to_ansi_string(5, 2));
|
||||
}
|
|
@ -21,6 +21,7 @@
|
|||
//! - [`Tabs`]: displays a tab bar and allows selection.
|
||||
//!
|
||||
//! [`Canvas`]: crate::widgets::canvas::Canvas
|
||||
mod ansi_string_buffer;
|
||||
mod barchart;
|
||||
pub mod block;
|
||||
mod borders;
|
||||
|
@ -37,6 +38,7 @@ mod scrollbar;
|
|||
mod sparkline;
|
||||
mod table;
|
||||
mod tabs;
|
||||
pub mod widget_ext;
|
||||
|
||||
pub use self::{
|
||||
barchart::{Bar, BarChart, BarGroup},
|
||||
|
|
171
src/widgets/ansi_string_buffer.rs
Normal file
171
src/widgets/ansi_string_buffer.rs
Normal file
|
@ -0,0 +1,171 @@
|
|||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A buffer that allows widgets to easily be rendered to a string with ANSI escape sequences.
|
||||
pub struct AnsiStringBuffer {
|
||||
pub area: Rect,
|
||||
buf: Buffer,
|
||||
}
|
||||
|
||||
impl AnsiStringBuffer {
|
||||
pub fn new(width: u16, height: u16) -> Self {
|
||||
Self {
|
||||
area: Rect::new(0, 0, width, height),
|
||||
buf: Buffer::empty(Rect::new(0, 0, width, height)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_ref(&mut self, widget: &impl WidgetRef) {
|
||||
widget.render_ref(self.area, &mut self.buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for AnsiStringBuffer {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let mut last_style = None;
|
||||
for y in 0..self.area.height {
|
||||
if y > 0 {
|
||||
f.write_str("\n")?;
|
||||
}
|
||||
for x in 0..self.area.width {
|
||||
let cell = self.buf.get(x, y);
|
||||
let style = (cell.fg, cell.bg, cell.modifier);
|
||||
if last_style.is_none() || last_style != Some(style) {
|
||||
write_cell_style(cell, f)?;
|
||||
last_style = Some(style);
|
||||
}
|
||||
f.write_str(cell.symbol())?;
|
||||
}
|
||||
}
|
||||
f.write_str("\u{1b}[0m")
|
||||
}
|
||||
}
|
||||
|
||||
fn write_cell_style(cell: &buffer::Cell, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("\u{1b}[")?;
|
||||
write_modifier(cell.modifier, f)?;
|
||||
write_fg(cell.fg, f)?;
|
||||
write_bg(cell.bg, f)?;
|
||||
f.write_str("m")
|
||||
}
|
||||
|
||||
fn write_modifier(modifier: Modifier, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
if modifier.contains(Modifier::BOLD) {
|
||||
f.write_str("1;")?;
|
||||
}
|
||||
if modifier.contains(Modifier::DIM) {
|
||||
f.write_str("2;")?;
|
||||
}
|
||||
if modifier.contains(Modifier::ITALIC) {
|
||||
f.write_str("3;")?;
|
||||
}
|
||||
if modifier.contains(Modifier::UNDERLINED) {
|
||||
f.write_str("4;")?;
|
||||
}
|
||||
if modifier.contains(Modifier::SLOW_BLINK) {
|
||||
f.write_str("5;")?;
|
||||
}
|
||||
if modifier.contains(Modifier::RAPID_BLINK) {
|
||||
f.write_str("6;")?;
|
||||
}
|
||||
if modifier.contains(Modifier::REVERSED) {
|
||||
f.write_str("7;")?;
|
||||
}
|
||||
if modifier.contains(Modifier::HIDDEN) {
|
||||
f.write_str("8;")?;
|
||||
}
|
||||
if modifier.contains(Modifier::CROSSED_OUT) {
|
||||
f.write_str("9;")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_fg(color: Color, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match color {
|
||||
Color::Reset => "39",
|
||||
Color::Black => "30",
|
||||
Color::Red => "31",
|
||||
Color::Green => "32",
|
||||
Color::Yellow => "33",
|
||||
Color::Blue => "34",
|
||||
Color::Magenta => "35",
|
||||
Color::Cyan => "36",
|
||||
Color::Gray => "37",
|
||||
Color::DarkGray => "90",
|
||||
Color::LightRed => "91",
|
||||
Color::LightGreen => "92",
|
||||
Color::LightYellow => "93",
|
||||
Color::LightBlue => "94",
|
||||
Color::LightMagenta => "95",
|
||||
Color::LightCyan => "96",
|
||||
Color::White => "97",
|
||||
_ => "",
|
||||
})?;
|
||||
if let Color::Rgb(red, green, blue) = color {
|
||||
f.write_fmt(format_args!("38;2;{red};{green};{blue}"))?;
|
||||
}
|
||||
if let Color::Indexed(i) = color {
|
||||
f.write_fmt(format_args!("38;5;{i}"))?;
|
||||
}
|
||||
f.write_str(";")
|
||||
}
|
||||
|
||||
fn write_bg(color: Color, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match color {
|
||||
Color::Reset => "49",
|
||||
Color::Black => "40",
|
||||
Color::Red => "41",
|
||||
Color::Green => "42",
|
||||
Color::Yellow => "43",
|
||||
Color::Blue => "44",
|
||||
Color::Magenta => "45",
|
||||
Color::Cyan => "46",
|
||||
Color::Gray => "47",
|
||||
Color::DarkGray => "100",
|
||||
Color::LightRed => "101",
|
||||
Color::LightGreen => "102",
|
||||
Color::LightYellow => "103",
|
||||
Color::LightBlue => "104",
|
||||
Color::LightMagenta => "105",
|
||||
Color::LightCyan => "106",
|
||||
Color::White => "107",
|
||||
_ => "",
|
||||
})?;
|
||||
if let Color::Rgb(red, green, blue) = color {
|
||||
f.write_fmt(format_args!("48;2;{red};{green};{blue}"))?;
|
||||
}
|
||||
if let Color::Indexed(i) = color {
|
||||
f.write_fmt(format_args!("48;5;{i}"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
struct Greeting;
|
||||
|
||||
impl WidgetRef for Greeting {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
let text = Text::from(vec![
|
||||
Line::styled("Hello", Color::Blue),
|
||||
Line::styled("World ", Color::Green),
|
||||
]);
|
||||
text.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_string() {
|
||||
let mut buffer = AnsiStringBuffer::new(10, 2);
|
||||
buffer.render_ref(&Greeting);
|
||||
let ansi_string = buffer.to_string();
|
||||
println!("{ansi_string}");
|
||||
assert_eq!(
|
||||
ansi_string,
|
||||
"\u{1b}[34;49mHello \n\u{1b}[32;49mWorld \u{1b}[0m"
|
||||
);
|
||||
}
|
||||
}
|
50
src/widgets/widget_ext.rs
Normal file
50
src/widgets/widget_ext.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
//! A module that provides an extension trait for widgets that provides methods that are useful for
|
||||
//! debugging.
|
||||
|
||||
use super::ansi_string_buffer::AnsiStringBuffer;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// An extension trait for widgets that provides methods that are useful for debugging.
|
||||
#[stability::unstable(
|
||||
feature = "widget-ext",
|
||||
issue = "https://github.com/ratatui-org/ratatui/issues/1045"
|
||||
)]
|
||||
pub trait WidgetExt {
|
||||
/// Returns a string representation of the widget with ANSI escape sequences for the terminal.
|
||||
fn to_ansi_string(&self, width: u16, height: u16) -> String;
|
||||
}
|
||||
|
||||
impl<W: WidgetRef> WidgetExt for W {
|
||||
fn to_ansi_string(&self, width: u16, height: u16) -> String {
|
||||
let mut buf = AnsiStringBuffer::new(width, height);
|
||||
buf.render_ref(self);
|
||||
buf.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod widget_ext_tests {
|
||||
use super::*;
|
||||
|
||||
struct Greeting;
|
||||
|
||||
impl WidgetRef for Greeting {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
let text = Text::from(vec![
|
||||
Line::styled("Hello", Color::Blue),
|
||||
Line::styled("World ", Color::Green),
|
||||
]);
|
||||
text.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widget_ext_to_ansi_string() {
|
||||
let ansi_string = Greeting.to_ansi_string(5, 2);
|
||||
println!("{ansi_string}");
|
||||
assert_eq!(
|
||||
ansi_string,
|
||||
"\u{1b}[34;49mHello\n\u{1b}[32;49mWorld\u{1b}[0m"
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue