The Basic Building blocks of Ratatui - Part 5

Author

Dheepak Krishnamurthy

Published

May 19, 2024

Keywords

rust, ratatui

Ratatui is a crate for building terminal user interfaces in Rust.

In this post we’ll show how to build custom widgets.

Code
:dep ratatui = "0.26.2"
:dep ratatui-macros = "0.4.0"
    
fn span_to_html(s: ratatui::text::Span) -> String{
    let mut html = String::new();
    html.push_str("<span style=\"");

    // Set foreground color
    if let Some(color) = &s.style.fg {
        html.push_str(&format!("color: {};", color));
    }

    // Set background color
    if let Some(color) = &s.style.bg {
        html.push_str(&format!("background-color: {};", color));
    }

    // Add modifiers
    match s.style.add_modifier {
        ratatui::style::Modifier::BOLD => html.push_str("font-weight: bold;"),
        ratatui::style::Modifier::ITALIC => html.push_str("font-style: italic;"),
        ratatui::style::Modifier::UNDERLINED => html.push_str("text-decoration: underline;"),
        _ => {}
    }
    html.push_str("\">");
    html.push_str(&s.content);
    html.push_str("</span>");
    html
}

fn buffer_to_html(buf: &ratatui::buffer::Buffer) -> String {
    fn escape_special_html_characters(text: &str) -> String {
        text.replace("&", "&amp;")
            .replace("<", "&lt;")
            .replace(">", "&gt;")
            .replace("\"", "&quot;")
            .replace("'", "&#39;")
    }

    let mut html = String::from("<pre><code>");

    let w = buf.area.width;
    let h = buf.area.height;

    for y in 0..h {
        for x in 0..w {
            let s = buf.get(x, y).symbol();
            
            let escaped = escape_special_html_characters(s); 

            let style = buf.get(x, y).style();

            let span = ratatui::text::Span::styled(s, style);
            
            html.push_str(&span_to_html(span));
        }
        html.push('\n');
    }

    html.push_str("</code></pre>");

    html 
}
    
fn show_html<D>(content: D) where D: std::fmt::Display {
    println!(r#"EVCXR_BEGIN_CONTENT text/html
<div style="display: flex; justify-content:start; gap: 1em; margin: 1em">
{}
</div>
EVCXR_END_CONTENT"#, content);
}

Buffer

We saw from an earlier part that frame has a buffer_mut() method:

let backend = ratatui::backend::TestBackend::new(80, 5);
let mut terminal = ratatui::terminal::Terminal::new(backend).unwrap();
terminal.draw(|frame| {
    let mut buffer = frame.buffer_mut();
    dbg!(buffer);
});
[src/lib.rs:169:5] buffer = Buffer {
    area: Rect { x: 0, y: 0, width: 80, height: 5 },
    content: [
        "                                                                                ",
        "                                                                                ",
        "                                                                                ",
        "                                                                                ",
        "                                                                                ",
    ],
    styles: [
        x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
    ]
}

We saw how to draw to the Buffer using predefined widgets.

use ratatui::widgets::Widget;

let backend = ratatui::backend::TestBackend::new(80, 5);
let mut terminal = ratatui::terminal::Terminal::new(backend).unwrap();
terminal.draw(|frame| {
    let area = frame.size();
    let mut buffer = frame.buffer_mut();
    ratatui::widgets::Block::bordered().render(area, &mut buffer);
    dbg!(buffer);
});
[src/lib.rs:172:5] buffer = Buffer {
    area: Rect { x: 0, y: 0, width: 80, height: 5 },
    content: [
        "┌──────────────────────────────────────────────────────────────────────────────┐",
        "│                                                                              │",
        "│                                                                              │",
        "│                                                                              │",
        "└──────────────────────────────────────────────────────────────────────────────┘",
    ],
    styles: [
        x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
    ]
}

But it is also possible to draw to the Buffer manually:


let backend = ratatui::backend::TestBackend::new(80, 5);
let mut terminal = ratatui::terminal::Terminal::new(backend).unwrap();
terminal.draw(|frame| {
    let area = frame.size();
    let mut buffer = frame.buffer_mut();

    ratatui::widgets::Block::bordered().render(area, &mut buffer);

    let x = 15;
    let y = 2;
    let string = "The quick brown fox jumps over the lazy dog.";
    let style = ratatui::style::Style::default();
    buffer.set_string(x, y, string, style);
    
    show_html(buffer_to_html(buffer));
});

                                                                              
              The quick brown fox jumps over the lazy dog.                    
                                                                              

Widget

The Widget trait in ratatui allows you to define what you want to draw to a Buffer:

struct MyCustomWidget {
    counter: usize
}

impl Widget for MyCustomWidget {

    fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) {
            let x = 15;
            let y = 2;
            let string = format!("The quick brown fox jumps over the lazy dog - {}", self.counter);
            let style = ratatui::style::Style::default();
            buf.set_string(x, y, string, style);
    }
    
}
let backend = ratatui::backend::TestBackend::new(80, 5);
let mut terminal = ratatui::terminal::Terminal::new(backend).unwrap();
terminal.draw(|frame| {
    let area = frame.size();
    let mut buffer = frame.buffer_mut();

    ratatui::widgets::Block::bordered().render(area, &mut buffer);

    MyCustomWidget{ counter: 2 }.render(area, &mut buffer);
    
    show_html(buffer_to_html(buffer));
});

                                                                              
              The quick brown fox jumps over the lazy dog - 2                 
                                                                              

Here’s an example of filling the entire buffer with the symbol :

struct Hatcher;

impl Widget for Hatcher {
    fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) {
        for x in area.left()..area.right() {
            for y in area.top()..area.bottom() {
                buf.set_string(x, y, "▒", ratatui::style::Style::default());
            }
        };
    }
    
}

Here’s the output of a Hatcher being rendered:

let backend = ratatui::backend::TestBackend::new(80, 5);
let mut terminal = ratatui::terminal::Terminal::new(backend).unwrap();
terminal.draw(|frame| {
    let area = frame.size();
    let mut buffer = frame.buffer_mut();

    Hatcher.render(area, &mut buffer);

    show_html(buffer_to_html(buffer));
});





Rendering multiple widgets renders to the buffer in the order they are called, and hence latter render calls can overwrite earlier renders.

let backend = ratatui::backend::TestBackend::new(80, 5);
let mut terminal = ratatui::terminal::Terminal::new(backend).unwrap();
terminal.draw(|frame| {
    let area = frame.size();
    let mut buffer = frame.buffer_mut();

    Hatcher.render(area, &mut buffer);

    MyCustomWidget{ counter: 2 }.render(area, &mut buffer);

    show_html(buffer_to_html(buffer));
});


The quick brown fox jumps over the lazy dog - 2


StatefulWidget

Sometimes you want to store information that is only known during render for later use. For this, you can use a StatefulWidget:

use ratatui::widgets::StatefulWidget;

#[derive(Default, Debug)]
struct Data {
    area: (usize, usize)
}

struct MyCustomWidgetWithState;

impl StatefulWidget for MyCustomWidgetWithState {
    type State = Data;
    
    fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer, state: &mut Self::State) {
        state.area = (area.width as usize, area.height as usize);
    }
}
let backend = ratatui::backend::TestBackend::new(80, 5);
let mut terminal = ratatui::terminal::Terminal::new(backend).unwrap();

let mut data = Data::default();

println!("before: {:?}", &data);

terminal.draw(|frame| {
    let area = frame.size();
    let mut buffer = frame.buffer_mut();

    MyCustomWidgetWithState.render(area, &mut buffer, &mut data);
});

println!("after: {:?}", &data);
before: Data { area: (0, 0) }
after: Data { area: (80, 5) }

Conclusion

In these series of posts, we examined how Ratatui works under the hood. For more information, check out the official tutorials and documentation.

Reuse

Citation

BibTeX citation:
@online{krishnamurthy2024,
  author = {Krishnamurthy, Dheepak},
  title = {The {Basic} {Building} Blocks of {Ratatui} - {Part} 5},
  date = {2024-05-19},
  url = {https://kdheepak.com/blog/the-basic-building-blocks-of-ratatui-part-5/},
  langid = {en}
}
For attribution, please cite this work as:
D. Krishnamurthy, “The Basic Building blocks of Ratatui - Part 5,” May 19, 2024. https://kdheepak.com/blog/the-basic-building-blocks-of-ratatui-part-5/.