mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-28 23:50:44 +00:00
3631b34f53
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.
258 lines
9.4 KiB
Rust
258 lines
9.4 KiB
Rust
//! # [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<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);
|
|
}
|
|
}
|
|
}
|