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("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'") }
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))┌first───┐┌second──┐
│ ││ │
│ ││ │
│ ││ │
└────────┘└────────┘
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);// ORlet [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))┌first───┐┌second──┐
└────────┘└────────┘
┌first───┐┌second──┐
└────────┘└────────┘
┌first───┐┌second──┐
└────────┘└────────┘
Popular layouts
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─────────────────┐
│ │
│ │
│ │
│ │
└───────────────────────┘
Sidebar
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))┌sidebar────────────────┐┌main───────────────────┐
│ ││ │
│ ││ │
│ ││ │
│ ││ │
└───────────────────────┘└───────────────────────┘
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))┌sidebar───────────┐┌main────┐
│ ││ │
│ ││ │
│ ││ │
│ ││ │
└──────────────────┘└────────┘
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.