Skip to content

Commit

Permalink
docs: Update section on rendering 📚
Browse files Browse the repository at this point in the history
  • Loading branch information
kdheepak committed Oct 7, 2023
1 parent a1b9d61 commit 225c2e3
Show file tree
Hide file tree
Showing 3 changed files with 286 additions and 3 deletions.
2 changes: 1 addition & 1 deletion src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
- [Mouse Capture](./concepts/backends/mouse-capture.md)
- [Layout](./concepts/layout/README.md)
- [Rendering](./concepts/rendering.md)
- [Under the hood](./concepts/rendering-under-the-hood.md)
- [Event Handling](./concepts/event_handling.md)
- [Key Binding]()
- [Application Patterns](./concepts/application-patterns/README.md)
- [The Elm Architecture](./concepts/application-patterns/the-elm-architecture.md)
- [Component Architecture](./concepts/application-patterns/component-architecture.md)
Expand Down
283 changes: 283 additions & 0 deletions src/concepts/rendering-under-the-hood.md
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
4 changes: 2 additions & 2 deletions src/concepts/rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,14 @@ This 4 minute talk about `IMGUI` is also tangentially relevant.
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen></iframe>

## Advantages of Immediate Mode Rendering
### Advantages of Immediate Mode Rendering

- **Simplicity**: Without a persistent widget state, your UI logic becomes a direct reflection of
your application state. You don't have to sync them or worry about past widget states.
- **Flexibility**: You can change your UI layout or logic any time, as nothing is set in stone. Want
to hide a widget conditionally? Just don't draw it based on some condition.

## Disadvantages of Immediate Mode Rendering
### Disadvantages of Immediate Mode Rendering

- **Render loop management**: In Immediate mode rendering, the onus of rendering lies solely on the
programmer. Every visual update necessitates a call to `Backend.draw()`. Hence, if the rendering
Expand Down

0 comments on commit 225c2e3

Please sign in to comment.