Skip to content
kd
15 May 2024
Back to blog

The Basic Building blocks of Ratatui - Part 2

2 min read (246 words)

Table of contents

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
                              

Center a block

10 px wide

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 [middle] = horizontal![==10].flex(ratatui::layout::Flex::Center).areas(area);
ratatui::widgets::Block::bordered().title("center").render(middle, &mut buf);
show_html(buffer_to_html(&buf))
                    center                    
                                                
                                                
                                                
                                                
                                        

50% wide

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 [middle] = horizontal![==50%].flex(ratatui::layout::Flex::Center).areas(area);
ratatui::widgets::Block::bordered().title("center").render(middle, &mut buf);
show_html(buffer_to_html(&buf))
             center            
                                                
                                                
                                                
                                                
                         

MinMax(20px, 50%)

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 [sidebar, main] = horizontal![<= (area.width / 2), *=1].areas(area);
ratatui::widgets::Block::bordered().title("sidebar").render(sidebar, &mut buf);
ratatui::widgets::Block::bordered().title("main").render(main, &mut buf);
show_html(buffer_to_html(&buf))
sidebarmain
                                              
                                              
                                              
                                              

use ratatui_macros::{horizontal, vertical};
let (x, y, width, height) = (0, 0, 30, 6);
let area = ratatui::layout::Rect::new(x, y, width, height);
let mut buf = ratatui::buffer::Buffer::empty(area);
let [sidebar, main] = horizontal![>=20, *=1].areas(area);
ratatui::widgets::Block::bordered().title("sidebar").render(sidebar, &mut buf);
ratatui::widgets::Block::bordered().title("main").render(main, &mut buf);
show_html(buffer_to_html(&buf))
sidebarmain
                          
                          
                          
                          

You can combine this behavior to make dynamic layouts:

let [sidebar, main] = if area.width > 30 {
horizontal![<= (area.width / 2), *=1].areas(area)
} else {
horizontal![>=20, *=1].areas(area)
};

Or even:

let [sidebar, main] = horizontal![<= (area.width / 2), *=1].areas(area);
let [sidebar, main] = if sidebar.width < 20 {
horizontal![>=20, *=1].areas(area)
} else {
[sidebar, main]
};

Conclusion

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


Citation

@online{krishnamurthy2024thebasicbuildingblocksofratatuipart2,
  author = {Dheepak Krishnamurthy},
  title = {The Basic Building blocks of Ratatui - Part 2},
  year = {2024},
  date = {2024-05-15},
  url = {https://kdheepak.com/blog/the-basic-building-blocks-of-ratatui-part-2/},
  langid = {en},
}

For attribution, please cite this work as:

Dheepak Krishnamurthy, "The Basic Building blocks of Ratatui - Part 2", May 15, 2024 https://kdheepak.com/blog/the-basic-building-blocks-of-ratatui-part-2/


The Basic Building blocks of Ratatui - Part 3
The Basic Building blocks of Ratatui - Part 1