widget_impl/
widget_impl.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
//! # [Ratatui] Widgets implementation examples
//!
//! This example demonstrates various ways to implement widget traits in Ratatui on a type, a
//! reference, and a mutable reference. It also shows how to use the `WidgetRef` trait to render
//! boxed widgets.
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::time::{Duration, Instant};

use color_eyre::Result;
use crossterm::event::{self, Event, KeyCode};
use ratatui::{
    buffer::Buffer,
    layout::{Constraint, Layout, Position, Rect, Size},
    style::{Color, Style},
    widgets::{Widget, WidgetRef},
    DefaultTerminal,
};

fn main() -> Result<()> {
    color_eyre::install()?;
    let terminal = ratatui::init();
    let result = App::default().run(terminal);
    ratatui::restore();
    result
}

#[derive(Default)]
struct App {
    should_quit: bool,
    timer: Timer,
    #[cfg(feature = "unstable-widget-ref")]
    boxed_squares: BoxedSquares,
    green_square: RightAlignedSquare,
}

impl App {
    fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
        while !self.should_quit {
            self.draw(&mut terminal)?;
            self.handle_events()?;
        }
        Ok(())
    }

    fn draw(&mut self, tui: &mut DefaultTerminal) -> Result<()> {
        tui.draw(|frame| frame.render_widget(self, frame.area()))?;
        Ok(())
    }

    fn handle_events(&mut self) -> Result<()> {
        // Handle events at least 50 frames per second (gifs are usually 50fps)
        let timeout = Duration::from_secs_f64(1.0 / 50.0);
        if !event::poll(timeout)? {
            return Ok(());
        }
        if let Event::Key(key) = event::read()? {
            match key.code {
                KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true,
                _ => {}
            }
        }
        Ok(())
    }
}

/// Implement the `Widget` trait on a mutable reference to the `App` type.
///
/// This allows the `App` type to be rendered as a widget. The `App` type owns several other widgets
/// that are rendered as part of the app. The `Widget` trait is implemented on a mutable reference
/// to the `App` type, which allows this to be rendered without consuming the `App` type, and allows
/// the sub-widgets to be mutable.
impl Widget for &mut App {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let constraints = Constraint::from_lengths([1, 1, 2, 1]);
        let [greeting, timer, squares, position] = Layout::vertical(constraints).areas(area);

        // render an ephemeral greeting widget
        Greeting::new("Ratatui!").render(greeting, buf);

        // render a reference to the timer widget
        self.timer.render(timer, buf);

        // render a boxed widget containing red and blue squares
        #[cfg(feature = "unstable-widget-ref")]
        self.boxed_squares.render(squares, buf);

        // render a mutable reference to the green square widget
        self.green_square.render(squares, buf);
        // Display the dynamically updated position of the green square
        let square_position = format!("Green square is at {}", self.green_square.last_position);
        square_position.render(position, buf);
    }
}

/// An ephemeral greeting widget.
///
/// This widget is implemented on the type itself, which means that it is consumed when it is
/// rendered. This is useful for widgets that are cheap to create, don't need to be reused, and
/// don't need to store any state between renders. This is the simplest way to implement a widget in
/// Ratatui, but in most cases, it is better to implement the `Widget` trait on a reference to the
/// type, as shown in the other examples below.
///
/// This was the way most widgets were implemented in Ratatui before `Widget` was implemented on
/// references in [PR #903] (merged in Ratatui 0.26.0).
///
/// [PR #903]: https://github.com/ratatui-org/ratatui/pull/903
struct Greeting {
    name: String,
}

impl Greeting {
    fn new(name: &str) -> Self {
        Self {
            name: name.to_string(),
        }
    }
}

impl Widget for Greeting {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let greeting = format!("Hello, {}!", self.name);
        greeting.render(area, buf);
    }
}

/// A timer widget that displays the elapsed time since the timer was started.
#[derive(Debug)]
struct Timer {
    start: Instant,
}

impl Default for Timer {
    fn default() -> Self {
        Self {
            start: Instant::now(),
        }
    }
}

/// This implements `Widget` on a reference to the type, which means that it can be reused and
/// doesn't need to be consumed when it is rendered. This is useful for widgets that need to store
/// state and be updated over time.
///
/// This approach was probably always available in Ratatui, but it wasn't widely used until `Widget`
/// was implemented on references in [PR #903] (merged in Ratatui 0.26.0). This is because all the
/// built-in widgets previously would consume themselves when rendered.
impl Widget for &Timer {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let elapsed = self.start.elapsed().as_secs_f32();
        let message = format!("Elapsed: {elapsed:.1?}s");
        message.render(area, buf);
    }
}

/// A widget that contains a list of several different widgets.
struct BoxedSquares {
    squares: Vec<Box<dyn WidgetRef>>,
}

impl Default for BoxedSquares {
    fn default() -> Self {
        let red_square: Box<dyn WidgetRef> = Box::new(RedSquare);
        let blue_square: Box<dyn WidgetRef> = Box::new(BlueSquare);
        Self {
            squares: vec![red_square, blue_square],
        }
    }
}

/// A widget that renders a red square.
struct RedSquare;

/// A widget that renders a blue square.
struct BlueSquare;

/// This implements the `Widget` trait on a reference to the type. It contains a list of boxed
/// widgets that implement the `WidgetRef` trait. This is useful for widgets that contain a list of
/// other widgets that can be different types.
impl Widget for &BoxedSquares {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let constraints = vec![Constraint::Length(4); self.squares.len()];
        let areas = Layout::horizontal(constraints).split(area);
        for (widget, area) in self.squares.iter().zip(areas.iter()) {
            widget.render_ref(*area, buf);
        }
    }
}

/// `RedSquare` and `BlueSquare` are widgets that render a red and blue square, respectively. They
/// implement the `WidgetRef` trait instead of the `Widget` trait, which which allows them to be
/// rendered as boxed widgets. It's not possible to use Widget for this as a dynamic reference to a
/// widget cannot generally be moved out of the box.
impl WidgetRef for RedSquare {
    fn render_ref(&self, area: Rect, buf: &mut Buffer) {
        fill(area, buf, "█", Color::Red);
    }
}

impl WidgetRef for BlueSquare {
    fn render_ref(&self, area: Rect, buf: &mut Buffer) {
        fill(area, buf, "█", Color::Blue);
    }
}

/// A widget that renders a green square aligned to the right of the area.
#[derive(Default)]
struct RightAlignedSquare {
    last_position: Position,
}

/// This widget is implemented on a mutable reference to the type, which means that it can store
/// state and update it when it is rendered. This is useful for widgets that need to store the
/// result of some calculation that can only be done when the widget is rendered.
///
/// The x and y coordinates of the square are stored in the widget and updated when the widget is
/// rendered. This allows the square to be aligned to the right of the area. These coordinates could
/// be used to perform hit testing (e.g. checking if a mouse click is inside the square). This app
/// just displays the coordinates as a string.
///
/// This approach was probably always available in Ratatui, but it wasn't widely used either. This
/// is an alternative to implementing the `StatefulWidget` trait, for situations where you want to
/// store the state in the widget itself instead of a separate struct.
impl Widget for &mut RightAlignedSquare {
    /// Render a green square aligned to the right of the area and store the position.
    fn render(self, area: Rect, buf: &mut Buffer) {
        const WIDTH: u16 = 4;
        let x = area.right() - WIDTH; // Align to the right
        self.last_position = Position { x, y: area.y };
        let size = Size::new(WIDTH, area.height);
        let area = Rect::from((self.last_position, size));
        fill(area, buf, "█", Color::Green);
    }
}

/// Fill the area with the specified symbol and style.
///
/// This probably should be a method on the `Buffer` type, but it is defined here for simplicity.
/// <https://github.com/ratatui-org/ratatui/issues/1146>
fn fill<S: Into<Style>>(area: Rect, buf: &mut Buffer, symbol: &str, style: S) {
    let style = style.into();
    for y in area.top()..area.bottom() {
        for x in area.left()..area.right() {
            buf[(x, y)].set_symbol(symbol).set_style(style);
        }
    }
}