From 3631b34f538a14840d633de57a0beb59c83bd649 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Fri, 23 Aug 2024 14:30:23 -0700 Subject: [PATCH] docs(examples): add widget implementation example (#1147) This new example documents the various ways to implement widgets in Ratatui. It demonstrates how to implement the `Widget` trait on a type, a reference, and a mutable reference. It also shows how to use the `WidgetRef` trait to render boxed widgets. --- Cargo.toml | 5 + examples/widget_impl.rs | 258 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 examples/widget_impl.rs diff --git a/Cargo.toml b/Cargo.toml index 979c52eb..cc355518 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -365,6 +365,11 @@ name = "user_input" required-features = ["crossterm"] doc-scrape-examples = true +[[example]] +name = "widget_impl" +required-features = ["crossterm", "unstable-widget-ref"] +doc-scrape-examples = true + [[test]] name = "state_serde" required-features = ["serde"] diff --git a/examples/widget_impl.rs b/examples/widget_impl.rs new file mode 100644 index 00000000..4fe72470 --- /dev/null +++ b/examples/widget_impl.rs @@ -0,0 +1,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 mutatable. +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>, +} + +impl Default for BoxedSquares { + fn default() -> Self { + let red_square: Box = Box::new(RedSquare); + let blue_square: Box = 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. +/// +fn fill>(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); + } + } +}