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 6, 2023
1 parent a1b9d61 commit ad27254
Showing 1 changed file with 230 additions and 2 deletions.
232 changes: 230 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 All @@ -77,3 +77,231 @@ This 4 minute talk about `IMGUI` is also tangentially relevant.
- **Architecture design considerations**: With `ratatui`, out of the box, there's little to no help
in organizing large applications. Ultimately, the decision on structure and discipline rests with
the developer to be principled.

## How does Ratatui work?

The smallest unit of data in Ratatui is a [`Cell`].

```rust
pub struct Cell {
pub symbol: String,
pub fg: Color,
pub bg: Color,
#[cfg(feature = "crossterm")]
pub underline_color: Color,
pub modifier: Modifier,
pub skip: bool,
}
```

You can see that a `Cell` 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).

And 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>,
}
```

`Buffer` represents the area in which the `Cell`s are going to be drawn.

When `terminal.draw(|f| ...)` is called, a [`Frame`] struct is constructed that contains a reference
to a `Buffer`:

```rust
pub struct Frame<'a> {
// --snip--
/// The buffer that is used to draw the current frame
buffer: &'a mut Buffer,
}
```

Structs that implement the [`Widget`] trait implement a `render` method that gets this `Buffer` from
the `Frame` struct:

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

Here's a simplified class diagram for reference:

```mermaid
classDiagram
class Cell {
+symbol: String
+fg: Color
+bg: Color
+modifier: Modifier
+underline_color: Color (conditional)
}
class Buffer {
+area: Rect
+content: Vec<Cell>
}
class Frame {
+buffer: mut Buffer
}
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,
}
```

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 the first `draw` call, `content1` will be overwritten by
`content2` which will be overwritten by `content3` in the `Buffer` struct:

```rust
terminal.draw(|f| {
f.render_widget(Paragraph("content1"), f.size());
f.render_widget(Paragraph("content2"), f.size());
f.render_widget(Paragraph("content3"), f.size());
})
```

Finally, 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.

[`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

0 comments on commit ad27254

Please sign in to comment.