//! # [Ratatui] `Colors_RGB` example
//! 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/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
// This example shows the full range of RGB colors that can be displayed in the terminal.
// Requires a terminal that supports 24-bit color (true color) and unicode.
// This example also demonstrates how implementing the Widget trait on a mutable reference
// allows the widget to update its state while it is being rendered. This allows the fps
// widget to update the fps calculation and the colors widget to update a cached version of
// the colors to render instead of recalculating them every frame.
// This is an alternative to using the `StatefulWidget` trait and a separate state struct. It
// is useful when the state is only used by the widget and doesn't need to be shared with
use std::time::{Duration, Instant};
use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Layout, Position, Rect},
fn main() -> Result<()> {
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
#[derive(Debug, Default)]
/// The current state of the app (running or quit)
/// A widget that displays the current frames per second
/// A widget that displays the full range of RGB colors that can be displayed in the terminal.
colors_widget: ColorsWidget,
#[derive(Debug, Default, PartialEq, Eq)]
/// The user has requested the app to quit
/// A widget that displays the current frames per second
/// The number of elapsed frames that have passed - used to calculate the fps
/// The last instant that the fps was calculated
/// The current frames per second
/// A widget that displays the full range of RGB colors that can be displayed in the terminal.
/// This widget is animated and will change colors over time.
#[derive(Debug, Default)]
/// The colors to render - should be double the height of the area as we render two rows of
/// pixels for each row of the widget using the half block character. This is computed any time
/// the size of the widget changes.
/// the number of elapsed frames that have passed - used to animate the colors by shifting the
/// x index by the frame number
/// This is the main event loop for the app.
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
while self.is_running() {
terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?;
const fn is_running(&self) -> bool {
matches!(self.state, AppState::Running)
/// Handle any events that have occurred since the last time the app was rendered.
/// Currently, this only handles the q key to quit the app.
fn handle_events(&mut self) -> Result<()> {
// Ensure that the app only blocks for a period that allows the app to render at
// approximately 60 FPS (this doesn't account for the time to render the frame, and will
// also update the app immediately any time an event occurs)
let timeout = Duration::from_secs_f32(1.0 / 60.0);
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
self.state = AppState::Quit;
/// Implement the Widget trait for &mut App so that it can be rendered
/// This is implemented on a mutable reference so that the app can update its state while it is
/// being rendered. This allows the fps widget to update the fps calculation and the colors widget
/// to update the colors to render.
impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) {
use Constraint::{Length, Min};
let [top, colors] = Layout::vertical([Length(1), Min(0)]).areas(area);
let [title, fps] = Layout::horizontal([Min(0), Length(8)]).areas(top);
Text::from("colors_rgb example. Press q to quit")
self.fps_widget.render(fps, buf);
self.colors_widget.render(colors, buf);
/// Default impl for `FpsWidget`
/// Manual impl is required because we need to initialize the `last_instant` field to the current
impl Default for FpsWidget {
last_instant: Instant::now(),
/// Widget impl for `FpsWidget`
/// This is implemented on a mutable reference so that we can update the frame count and fps
/// calculation while rendering.
impl Widget for &mut FpsWidget {
fn render(self, area: Rect, buf: &mut Buffer) {
if let Some(fps) = self.fps {
let text = format!("{fps:.1} fps");
Text::from(text).render(area, buf);
/// Update the fps calculation.
/// This updates the fps once a second, but only if the widget has rendered at least 2 frames
/// since the last calculation. This avoids noise in the fps calculation when rendering on slow
/// machines that can't render at least 2 frames per second.
#[allow(clippy::cast_precision_loss)]
fn calculate_fps(&mut self) {
let elapsed = self.last_instant.elapsed();
if elapsed > Duration::from_secs(1) && self.frame_count > 2 {
self.fps = Some(self.frame_count as f32 / elapsed.as_secs_f32());
self.last_instant = Instant::now();
/// Widget impl for `ColorsWidget`
/// This is implemented on a mutable reference so that we can update the frame count and store a
/// cached version of the colors to render instead of recalculating them every frame.
impl Widget for &mut ColorsWidget {
fn render(self, area: Rect, buf: &mut Buffer) {
let colors = &self.colors;
for (xi, x) in (area.left()..area.right()).enumerate() {
// animate the colors by shifting the x index by the frame number
let xi = (xi + self.frame_count) % (area.width as usize);
for (yi, y) in (area.top()..area.bottom()).enumerate() {
// render a half block character for each row of pixels with the foreground color
// set to the color of the pixel and the background color set to the color of the
let fg = colors[yi * 2][xi];
let bg = colors[yi * 2 + 1][xi];
buf[Position::new(x, y)].set_char('▀').set_fg(fg).set_bg(bg);
/// Setup the colors to render.
/// This is called once per frame to setup the colors to render. It caches the colors so that
/// they don't need to be recalculated every frame.
#[allow(clippy::cast_precision_loss)]
fn setup_colors(&mut self, size: Rect) {
let Rect { width, height, .. } = size;
// double the height because each screen row has two rows of half block pixels
let height = height as usize * 2;
let width = width as usize;
// only update the colors if the size has changed since the last time we rendered
if self.colors.len() == height && self.colors[0].len() == width {
self.colors = Vec::with_capacity(height);
let mut row = Vec::with_capacity(width);
let hue = x as f32 * 360.0 / width as f32;
let value = (height - y) as f32 / height as f32;
let saturation = Okhsv::max_saturation();
let color = Okhsv::new(hue, saturation, value);
let color = Srgb::<f32>::from_color_unclamped(color);
let color: Srgb<u8> = color.into_format();
let color = Color::Rgb(color.red, color.green, color.blue);