When trait in Rust

When trait in Rust
Photo by Ruhan Shete / Unsplash

I will start this article by demonstrating the problem that I was facing.

I am developing a game in Ratatui, a really nice library for creating UIs in the terminal (TUI).

Ratatui offers a builder pattern for widgets.

Step 1: Building a simple widget

// area: Rect, buf: &mut Buffer
Paragraph::new("Hello There!")
    .block(Block::default().borders(Borders::ALL)
        .title("Game Menu").title_alignment(Center)
        .white())
    .light_green()
    .alignment(Center)
    .render(area, buf);

The resulting render:

As you can see, the text is drawn in green, centered, with a white box around it and a title that is also centered.

Now let's do something more complicated: I want to display this widget differently based on whether it is currently "active". Let's replace all the colors with something darker, if the menu is not active.

Step 2: Dynamic Colors

let title_color = if menu_is_active {
    Color::White
} else {
    Color::DarkGray
};
let text_color = if menu_is_active {
    Color::LightCyan
} else {
    Color::DarkGray
};

Paragraph::new("Hello There!")
    .block(Block::default().borders(Borders::ALL)
        .title("Game Menu").title_alignment(Center)
        .style(Style::default().fg(title_color)))
    .style(Style::default().fg(text_color))
    .alignment(Center)
    .render(area, buf);

Active vs Inactive:

It works, but the code blew up quite a bit.

Step 3: Using the dim() method

As it turns out, there is already a dim() method for widgets, that automatically takes care of dimming all the colors inside of it.

With this, I can get rid of the color logic. I can store the widget in an intermediate variable, and then apply a conditional action on it.

let widget = Paragraph::new("Hello There!")
  .block(Block::default().borders(Borders::ALL)
      .title("Game Menu").title_alignment(Center)
      .white())
  .light_green()
  .alignment(Center);
    
let widget = if menu_is_active { widget.dim() } else { widget };

widget.render(area, buf);

But this code ruins the fluid, elegant code style we had originally, because I need to perform a conditional transformation on the widget.

Step 4: When Trait

Can we somehow turn an if expression into a chainable method? Yes we can!

I added this trait to my utils file:

pub trait When {
    fn when(self, condition: bool, action: impl FnOnce(Self) -> Self) -> Self where Self: Sized;
}

impl<T> When for T {
    fn when(self, condition: bool, action: impl FnOnce(T) -> T) -> Self {
        if condition {
            action(self)
        } else {
            self
        }
    }
}

And now I can write my widget like this:

use crate::utils::When; // make the trait available

Paragraph::new("Hello There!")
    .block(Block::default().borders(Borders::ALL)
        .title("Game Menu").title_alignment(Center)
        .white())
    .light_green()
    .alignment(Center)
    .when(! menu_is_active, |w| w.dim()) // <---- the good stuff
    .render(area, buf);

Isn't that glorious?

Let's add another when() and give our widget a Double border when it is active:

Paragraph::new("Hello There!")
    .block(Block::default().borders(Borders::ALL)
        .when(menu_is_active, |b| b.border_type(Double)) // <---- the good stuff
        .title("Game Menu").title_alignment(Center)
        .white())
    .light_green()
    .alignment(Center)
    .when(! menu_is_active, |w| w.dim()) // <---- the good stuff
    .render(area, buf);

The result:

Read more