diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 8b5d5f164..170dd987e 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -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) diff --git a/src/concepts/rendering-under-the-hood.md b/src/concepts/rendering-under-the-hood.md new file mode 100644 index 000000000..deb9ce90a --- /dev/null +++ b/src/concepts/rendering-under-the-hood.md @@ -0,0 +1,284 @@ +# How does Ratatui work? + +You may have read in previous sections that Ratatui is a immediate mode rendering library. But what +does that really mean? And how is it implemented? 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(&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, +} +``` + +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 + } + + class Frame { + +buffer: mut Buffer + +render_widget() + } + + class Widget { + <> + +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>, +} + +pub struct Line<'a> { + pub spans: Vec>, + pub alignment: Option, +} + +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: . 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 diff --git a/src/concepts/rendering.md b/src/concepts/rendering.md index 39f248359..2e0da15d8 100644 --- a/src/concepts/rendering.md +++ b/src/concepts/rendering.md @@ -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> -## 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