The Basic Building blocks of Ratatui - Part 2

Author

Dheepak Krishnamurthy

Published

May 16, 2024

Keywords

rust, ratatui

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

In this post, we’ll discuss the Rect and Layout primitives of ratatui.

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

Layout primitives

We already saw that Rect is one of the primitives for rendering a widget.

We can create a Rect using Rect::new(x, y, width, height):

use ratatui::widgets::Widget;

let (x, y, width, height) = (0, 0, 50, 5); 
let area = ratatui::layout::Rect::new(x, y, width, height);
let mut buf = ratatui::buffer::Buffer::empty(area);
ratatui::widgets::Block::bordered().render(area, &mut buf);

show_html(buffer_to_html(&buf))

                                                
                                                
                                                

You can create inner Rects by using the Rect::inner method:

let mut buf = ratatui::buffer::Buffer::empty(area);

let (horizontal, vertical) = (5, 1);
let inner_area = area.inner(&ratatui::layout::Margin::new(horizontal, vertical));

ratatui::widgets::Block::bordered().render(inner_area, &mut buf);

show_html(buffer_to_html(&buf))
                                                  
          
                                                
          
                                                  

Ratatui also has a layout solver using the cassowary algorithm.

use ratatui::layout::{Layout, Constraint};

let [first, second] = ratatui::layout::Layout::horizontal([Constraint::Length(10), Constraint::Length(10)]).areas(area);

let mut buf = ratatui::buffer::Buffer::empty(area);
ratatui::widgets::Block::bordered().title("first").render(first, &mut buf);
ratatui::widgets::Block::bordered().title("second").render(second, &mut buf);

show_html(buffer_to_html(&buf))
firstsecond                              
                                              
                                              
                                              
                              

ratatui-macros has a couple of macros to make some of this boilerplate simpler.

let [first, second] = ratatui::layout::Layout::horizontal([Constraint::Length(10), Constraint::Length(10)]).areas(area);
// OR
let [first, second] = ratatui_macros::horizontal![==10, ==10].areas(area);

Here’s an example of combining a vertical and horizontal layout:

use ratatui_macros::{horizontal, vertical};

let (x, y, width, height) = (0, 0, 50, 6); 
let area = ratatui::layout::Rect::new(x, y, width, height);

let mut buf = ratatui::buffer::Buffer::empty(area);

let [top, middle, bottom] = vertical![*=1, *=1, *=1].areas(area);

let [first, second] = horizontal![==10, ==10].areas(top);
ratatui::widgets::Block::bordered().title("first").render(first, &mut buf);
ratatui::widgets::Block::bordered().title("second").render(second, &mut buf);

let [first, second] = horizontal![==10, ==10].flex(ratatui::layout::Flex::Center).areas(middle);
ratatui::widgets::Block::bordered().title("first").render(first, &mut buf);
ratatui::widgets::Block::bordered().title("second").render(second, &mut buf);

let [first, second] = horizontal![==10, ==10].flex(ratatui::layout::Flex::End).areas(bottom);
ratatui::widgets::Block::bordered().title("first").render(first, &mut buf);
ratatui::widgets::Block::bordered().title("second").render(second, &mut buf);

show_html(buffer_to_html(&buf))
firstsecond                              
                              
               firstsecond               
                              
                              firstsecond
                              

Conclusion

In the next post, we’ll examine how text primitives work.

Reuse

Citation

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