-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: Update section on rendering 📚
- Loading branch information
Showing
3 changed files
with
286 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,283 @@ | ||
# How does Ratatui work? | ||
|
||
In this section, we are going to discuss what it means "to be a `Widget`" and what happens behind | ||
the scenes in Ratatui. | ||
|
||
The first thing to note is that Ratatui has a [`Widget`] trait. | ||
|
||
```rust | ||
pub trait Widget { | ||
/// Draws the current state of the widget in the given buffer. That is the only method required | ||
/// to implement a custom widget. | ||
fn render(self, area: Rect, buf: &mut Buffer); | ||
} | ||
``` | ||
|
||
In Ratatui, anything that implements the `Widget` trait can be rendered to the terminal. Any struct | ||
(inside Ratatui or third party crates) can provide an implementation of the `Widget` trait for said | ||
struct. | ||
|
||
For example, the `Paragraph` struct is a widget provided by Ratatui. Here's how you may use the | ||
`Paragraph` widget: | ||
|
||
```rust | ||
fn main() { | ||
let mut terminal = ratatui::terminal::Terminal::new(ratatui::backend::CrosstermBackend::new(std::io::stderr()))?; | ||
|
||
terminal.draw(|f| { | ||
f.render_widget(Paragraph::new("Hello World!"), f.size()); | ||
})?; | ||
} | ||
``` | ||
|
||
In order to use Ratatui, users are expected to create an instance of the `Terminal` struct and call | ||
the `draw` method by passing in a function. This function takes one argument of type [`Frame`]. For | ||
example, the following code is exactly equivalent to the code above: | ||
|
||
```rust | ||
fn main() { | ||
let mut terminal = ratatui::terminal::Terminal::new(ratatui::backend::CrosstermBackend::new(std::io::stderr()))?; | ||
|
||
terminal.draw(ui)?; | ||
} | ||
|
||
fn ui(frame: &mut Frame) { | ||
frame.render_widget( | ||
Paragraph::new("Hello World!"), | ||
frame.size(), | ||
); | ||
} | ||
``` | ||
|
||
The `Frame` struct implements a method called `render_widget` which takes any object that implements | ||
the `Widget` trait. This `render_widget` method calls the `Widget::render` method on the type-erased | ||
`Widget` struct. | ||
|
||
```rust | ||
pub fn render_widget<W>(&mut self, widget: W, area: Rect) | ||
where | ||
W: Widget, | ||
{ | ||
widget.render(area, self.buffer); | ||
} | ||
``` | ||
|
||
This `render` method on a widget gets the current `area` as a `Rect` and gets a reference to a | ||
`Buffer`. Every time `terminal.draw(|f| ...)` is called, a new [`Frame`] struct is constructed that | ||
contains a mutable reference to an instance of the `Buffer` struct: | ||
|
||
```rust | ||
pub struct Frame<'a> { | ||
// --snip-- | ||
/// The buffer that is used to draw the current frame | ||
buffer: &'a mut Buffer, | ||
} | ||
``` | ||
|
||
The `Buffer` represents the area in which content is going to be drawn to the terminal. A [`Buffer`] | ||
is a collection of `Cell`s: | ||
|
||
```rust | ||
pub struct Buffer { | ||
/// The area represented by this buffer | ||
pub area: Rect, | ||
/// The content of the buffer. The length of this Vec should always be equal to area.width * | ||
/// area.height | ||
pub content: Vec<Cell>, | ||
} | ||
``` | ||
|
||
In Ratatui, this `Cell` struct is the smallest renderable unit of code. A `Cell` essentially has a | ||
`String` which is the symbol that will be drawn in that `Cell` as well as `Style` information | ||
(foreground color, background color, modifiers etc). | ||
|
||
```rust | ||
// pseudo code | ||
pub struct Cell { | ||
content: String, | ||
style: Style, | ||
} | ||
``` | ||
|
||
```admonish | ||
`Cell`s are the closest analog to a "pixel" in a terminal. | ||
Most terminals render monospaced text 2 high and 1 wide. | ||
A `Cell` in Ratatui should usually contain 1 wide string content. | ||
``` | ||
|
||
Here's a simplified class diagram for reference: | ||
|
||
```mermaid | ||
classDiagram | ||
class Cell { | ||
+content: String | ||
+style: Style | ||
} | ||
class Buffer { | ||
+area: Rect | ||
+content: Vec<Cell> | ||
} | ||
class Frame { | ||
+buffer: mut Buffer | ||
+render_widget() | ||
} | ||
class Widget { | ||
<<interface>> | ||
+render(area: Rect, buf: &mut Buffer) | ||
} | ||
class Paragraph | ||
class Block | ||
class Table | ||
Buffer --> Cell | ||
Frame --> Buffer | ||
Widget <|.. Paragraph | ||
Widget <|.. Block | ||
Widget <|.. Table | ||
``` | ||
|
||
For example, when you create an instance of a `Paragraph` struct, any content you pass in is | ||
converted to a [`Text`] struct. | ||
|
||
```rust | ||
pub struct Paragraph<'a> { | ||
/// The text to display | ||
text: Text<'a>, | ||
// --snip-- | ||
} | ||
``` | ||
|
||
A `Text` struct consists of [`Line`]s which consists of [`Span`]s. | ||
|
||
```rust | ||
pub struct Text<'a> { | ||
pub lines: Vec<Line<'a>>, | ||
} | ||
|
||
pub struct Line<'a> { | ||
pub spans: Vec<Span<'a>>, | ||
pub alignment: Option<Alignment>, | ||
} | ||
|
||
pub struct Span<'a> { | ||
/// The content of the span as a Clone-on-write string. | ||
pub content: Cow<'a, str>, | ||
/// The style of the span. | ||
pub style: Style, | ||
} | ||
``` | ||
|
||
You can learn more about the text related of Ratatui features and displaying text | ||
[here](./../how-to/render/display-text.md). | ||
|
||
When you render an instance of `Paragraph`, the `Paragraph` widget is passed to a | ||
[`Paragraph`'s `render`](https://github.com/ratatui-org/ratatui/blob/e5caf170c8c304b952cbff7499fd4da17ab154ea/src/widgets/paragraph.rs#L253-L268) | ||
function along with the `Buffer`. | ||
|
||
The implementation of `render` loops through the content of the `Text` struct in the `Paragraph` | ||
widget and puts the content in the appropriate `Cell` locations using methods exposed on the | ||
`Buffer` struct. | ||
|
||
```admonish note | ||
Because any styling has to happen though the `Style` struct (i.e. methods that apply | ||
`Style`s to `Cell`s) you can't use ansi escape sequences directly in the string content. | ||
As a workaround, what you can do however is convert the ansi escape sequences into a | ||
`ratatui::text::Text` struct that contains the appropriate `Style`s. There's a crate for this that | ||
might help: <https://crates.io/crates/ansi-to-tui>. And this `Text` struct that you create can be | ||
passed into the `Paragraph` widget to get the desired effect. | ||
Even if this particular crate doesn't work for you this approach is probably what you want to use if | ||
you want to render ansi escape sequences in Ratatui. | ||
``` | ||
|
||
As another example, here's a snippet of the [`render` method for `Block`]: | ||
|
||
```rust | ||
fn render(self, area: Rect, buf: &mut Buffer) { | ||
buf.set_style(area, self.style); | ||
let symbols = self.border_set; | ||
|
||
// Sides | ||
if self.borders.intersects(Borders::LEFT) { | ||
for y in area.top()..area.bottom() { | ||
buf.get_mut(area.left(), y) | ||
.set_symbol(symbols.vertical_left) | ||
.set_style(self.border_style); | ||
} | ||
} | ||
// --snip-- | ||
|
||
// Corners | ||
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) { | ||
buf.get_mut(area.right() - 1, area.bottom() - 1) | ||
.set_symbol(symbols.bottom_right) | ||
.set_style(self.border_style); | ||
} | ||
// --snip-- | ||
} | ||
``` | ||
|
||
The important thing to note is that all widgets render to the same `Buffer` within a single | ||
`terminal.draw(|f| ...)` call. Widgets may overwrite the same `Cell`, so the order in which widgets | ||
are rendered is important. For example, in this `draw` example below, `content1` will be overwritten | ||
by `content2` which will be overwritten by `content3` in the `Buffer` struct: | ||
|
||
```rust | ||
terminal.draw(|f| { | ||
f.render_widget(Paragraph::new("content1"), f.size()); | ||
f.render_widget(Paragraph::new("content2"), f.size()); | ||
f.render_widget(Paragraph::new("content3"), f.size()); | ||
}) | ||
``` | ||
|
||
After all the `Widget`s are rendered to the same `Buffer` struct, i.e. after | ||
`terminal.draw(|f| ...)` returns, [`terminal.flush()`] is called. This is where the content is | ||
actually written out to the terminal. | ||
|
||
Ratatui uses a double buffer technique. In `flush()`, the difference between the previous and the | ||
current buffer is obtained and that is passed it to the current backend for drawing to your actual | ||
terminal. | ||
|
||
```rust | ||
pub fn flush(&mut self) -> io::Result<()> { | ||
let previous_buffer = &self.buffers[1 - self.current]; | ||
let current_buffer = &self.buffers[self.current]; | ||
let updates = previous_buffer.diff(current_buffer); | ||
if let Some((col, row, _)) = updates.last() { | ||
self.last_known_cursor_pos = (*col, *row); | ||
} | ||
self.backend.draw(updates.into_iter()) | ||
} | ||
``` | ||
|
||
After `flush()`, the buffers are swapped and the next time `terminal.draw(|f| ...)` is called a | ||
`Frame` is constructed with the other `Buffer` struct. | ||
|
||
This was a sneak peak into what happens under the hood in Ratatui. If you have any questions, feel | ||
free to open issues or discussions on [the main repository](https://github.com/ratatui-org/ratatui). | ||
|
||
[`Cell`]: | ||
https://github.com/ratatui-org/ratatui/blob/e5caf170c8c304b952cbff7499fd4da17ab154ea/src/buffer.rs#L15-L26 | ||
[`Buffer`]: | ||
https://github.com/ratatui-org/ratatui/blob/e5caf170c8c304b952cbff7499fd4da17ab154ea/src/buffer.rs#L149-L157 | ||
[`Text`]: | ||
https://github.com/ratatui-org/ratatui/blob/e5caf170c8c304b952cbff7499fd4da17ab154ea/src/text/text.rs#L30-L33 | ||
[`Line`]: | ||
https://github.com/ratatui-org/ratatui/blob/e5caf170c8c304b952cbff7499fd4da17ab154ea/src/text/line.rs#L6-L10 | ||
[`Span`]: | ||
https://github.com/ratatui-org/ratatui/blob/e5caf170c8c304b952cbff7499fd4da17ab154ea/src/text/span.rs#L55-L61 | ||
[`render` method for `Block`]: | ||
https://github.com/ratatui-org/ratatui/blob/e5caf170c8c304b952cbff7499fd4da17ab154ea/src/widgets/block.rs#L752-L760 | ||
[`Frame`]: | ||
https://github.com/ratatui-org/ratatui/blob/e5caf170c8c304b952cbff7499fd4da17ab154ea/src/terminal.rs#L566-L578 | ||
[`Widget`]: | ||
https://github.com/ratatui-org/ratatui/blob/e5caf170c8c304b952cbff7499fd4da17ab154ea/src/widgets.rs#L107-L112 | ||
[`terminal.flush()`]: | ||
https://github.com/ratatui-org/ratatui/blob/e5caf170c8c304b952cbff7499fd4da17ab154ea/src/terminal.rs#L253-L263 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters