Ratatui 0.26.0 highlights

https://github.com/ratatui-org/ratatui/releases/tag/v0.26.0

:warning: See the breaking changes
for this release.

FOSDEM :loudspeaker:

At the time this release is published, one of our maintainers
(Orhun Parmaksız) will be giving an introductory talk about Ratatui at
FOSDEM! The talk will be also recorded and streamed live.

See the event details
here.

If you are around in person, don’t miss the chance to get some Ratatui stickers!

The recording of the talk is available here.

Demo: Destroy Mode :boom:

We have a brand new demo which has a destroy mode! (Made for celebrating the 1000th commit of
Ratatui)

To run it:

cargo run --example demo2 --features="crossterm widget-calendar"

Press d to activate destroy mode and enjoy!

Ref Widget Implementation :jigsaw:

Many widgets can now be rendered without changing their state.

We implemented WidgetRef trait for references to widgets and changed their implementations to be
immutable. This allows us to render widgets without consuming them by passing a ref to the widget to
Frame::render_widget(). It also allows boxed widgets to be rendered.

Note: this trait is gated behind a feature flag unstable-widget-ref. The approach we take might
change for this (as there are approaches that would allow the code below to just use Widget
instead of WidgetRef).

// this might be stored in a struct
let paragraph = Paragraph::new("Hello world!");

let [left, right] = area.split(&Layout::horizontal([20, 20]));
frame.render_widget(&paragraph, left);
frame.render_widget(&paragraph, right); // we can reuse the widget

let widgets: Vec<Box<dyn WidgetRef>> = vec![Box::new(Line::raw("hello"), Span::raw("world"))];
for widget in widgets {
    widget.render_ref(area, &mut buf);
}

Layout: flex :sparkles:

We now support a new way to space the elements in a Layout: Flex! We added a Flex enum loosely
based on flexbox:

  • Flex::Start (new default)
  • Flex::Center
  • Flex::End
  • Flex::SpaceAround
  • Flex::SpaceBetween
  • Flex::Legacy (old default)

In addition to changing the default to Flex::Start, we have made a couple of changes to the
constraints.

  1. Min(v) grows to allocate excess space in all Flex modes instead of shrinking (except in
    Flex::Legacy where it retains old behavior).
  2. We added a new constraint variant Fill(1) that grows to allocate excess space, growing equally
    with Min(v).

While is a breaking change to the behavior of constraints, most users should see identical layouts
with the new Flex::Start, especially if Min() is one of the constraints. However, you want the
old behavior, you can use Flex::Legacy:

Layout::vertical([Length(25), Length(25)]).flex(Flex::Legacy)

We have also removed the unstable feature SegmentSize.

Check out the pull request for the motivation
behind this feature and more information.

We have also built a constraint-explorer TUI that will allow you to compare how constraints behave
in different Flex modes. Check out the
pull request for a video demo of the
constraint-explorer.

Color Palettes :art:

There are two brand new colors palettes ready to use, Material and Tailwind.

The
ratatui::style::palette::material
module contains the Google 2014 Material Design palette.

use ratatui::style::palette::material::BLUE_GRAY;
Line::styled("Hello", BLUE_GRAY.c500);

The
ratatui::style::palette::tailwind
module contains the default Tailwind color palette. This is useful for styling components with
colors that match the Tailwind color palette.

use ratatui::style::palette::tailwind::SLATE;
Line::styled("Hello", SLATE.c500);

See https://m2.material.io/design/color/the-color-system.html#tools-for-picking-colors and
https://tailwindcss.com/docs/customizing-colors for more information.

Alignment Convenience Functions :building_construction:

We added the following alignment convenience functions for Line, Paragraph and Text:

  • Line::from("align on left").left_aligned();
  • Line::from("centered!").centered();
  • Line::from("align on right").right_aligned();

Same applies for Paragraph and Text e.g. Paragraph::new("Hello, world!").centered().

Span on the other hand has the following new methods:

let span = Span::styled("Test Content", Style::new().green().italic());

// convert span to left-aligned line
let line = span.to_left_aligned_line();

// convert span to right-aligned line
let line = span.to_right_aligned_line();

// convert span to center-aligned line
let line = span.to_center_aligned_line();

Padding: New Constructors :building_construction:

Padding has new constructors:

  • Padding::proportional(4);: make horizontal and vertical padding seem equal
  • Padding::symmetric(5, 6);: defines left and right padding
  • Padding::left(3);: defines left padding
  • Padding::right(3);: defines right padding
  • Padding::top(3);: defines top padding
  • Padding::bottom(3);: defines bottom padding

Block: bordered :brick:

Block has a new constructor method named bordered for avoiding creating a block with no borders
and setting Borders::ALL.

So you can simplify your code as follows:

- Block::default().borders(Borders::ALL);
+ Block::bordered();

Color: New Constructors :building_construction:

Color can be constructed from u32 values now. The format is 0x00RRGGBB:

let white = Color::from_u32(0x00FFFFFF);
let black = Color::from_u32(0x00000000);

We also added from_hsl method for constructing Color::Rgb values.

let color: Color = Color::from_hsl(360.0, 100.0, 100.0);
assert_eq!(color, Color::Rgb(255, 255, 255));

let color: Color = Color::from_hsl(0.0, 0.0, 0.0);
assert_eq!(color, Color::Rgb(0, 0, 0));

HSL stands for Hue (0-360 deg), Saturation (0-100%), and Lightness (0-100%) and working with HSL
the values can be more intuitive. For example, if you want to make a red color more orange, you
can change the Hue closer toward yellow on the color wheel (i.e. increase the Hue).

Layout: Increase Cache Size :chart_with_upwards_trend:

We increase the default cache size of layout from 16 to 500.

This is a somewhat arbitrary size for the layout cache based on adding the columns and rows on my
laptop’s terminal (171+51 = 222) and doubling it for good measure and then adding a bit more to
make it a round number. This gives enough entries to store a layout for every row and every
column, twice over, which should be enough for most apps.

For those that need more, the cache size can be set with Layout::init_cache().

See the relevant discussion in this issue.

Layout: Horizontal and Vertical Constructors :building_construction:

The Layout now allows to create a vertical or horizontal layout with default values with the
following constructors:

let layout = Layout::vertical([Constraint::Length(10), Constraint::Min(5)]);
let layout = Layout::horizontal([Constraint::Length(10), Constraint::Min(5)]);

Layout: Accept Constraints :triangular_ruler:

The Layout constructors now accept any type that implements Into<Constraint> instead of just
AsRef<Constraint>. This is useful when you want to specify a fixed size for a layout, but don’t
want to explicitly create a Constraint::Length yourself.

Layout::new(Direction::Vertical, [1, 2, 3]);
Layout::horizontal([1, 2, 3]);
Layout::vertical([1, 2, 3]);
Layout::default().constraints([1, 2, 3]);

Layout: spacing :straight_ruler:

Spacing can now be added between the items of a layout.

let layout = Layout::horizontal([Length(20), Length(20), Length(20)]).spacing(2);

Rect: contains :black_square_button:

If you want to perform hit tests, this new method is for you. (e.g. did the user click in an area)

Rect::new(1, 2, 3, 4).contains(Position { x: 1, y: 2 }) // true

We also added the Position struct for storing the x and y coordinates (columns and rows).

Rect: clamp :lobster:

There is a new useful method when you want to be able to dynamically move a rectangle around, but
keep it constrained to a certain area.

For example, this can be used to implement a draggable window that can be moved around, but not
outside the terminal window.

let window_area = Rect::new(state.x, state.y, 20, 20).clamp(area);
state.x = rect.x;
state.y = rect.y;

Layout: areas and spacers methods :banana:

Now you can split a Rect into multiple sub-Rects in a more concise way:

use Constraint::*;
let layout = Layout::vertical([Length(1), Min(0)]);
let [top, main] = layout.areas();
let [above, inbetwee, below] = layout.spacers();

Rect: Rows/Colums Iterators :black_square_button:

This enables iterating over rows and columns of a Rect this simplifies looping over cells.

let area = Rect::new(0, 0, 3, 2);
let rows: Vec<Rect> = area.rows().collect();
let columns: Vec<Rect> = area.columns().collect();

Table: Accept Constraints :bar_chart:

Table constructors now accept any type that implements Into<Constraint> instead of just
AsRef<Constraint>. This is useful when you want to specify a fixed size for a table columns, but
don’t want to explicitly create a Constraint::Length yourself.

Table::new(rows, [1,2,3])
Table::default().widths([1,2,3])

Table: Accepts Iterator :bar_chart:

Previously, Table::new() accepted IntoIterator<Item=Row<'a>>. The argument change to
IntoIterator<Item: Into<Row<'a>>>, This allows more flexible types from calling scopes, though it
can some break type inference in the calling scope for empty containers.

- let table = Table::new(vec![], widths);
// becomes
+ let table = Table::default().widths(widths);

This also means that any iterator whose item is convertible into Row can now be collected into a
Table.

Table: Accept Text as highlight symbol :bar_chart:

You can now use multi-line symbols for highlighting items in a table.

let table = Table::new(rows, widths)
  .highlight_symbol(Text::from(vec![
      "".into(),
      " █ ".into(),
      " █ ".into(),
      "".into(),
  ]));
See the demo

Made with VHS

Table: footer :bar_chart:

Table now has a footer method for setting the rows that will be displayed at the bottom.

let footer = Row::new(vec![
    Cell::from("Footer Cell 1"),
    Cell::from("Footer Cell 2"),
]);
let table = Table::default().footer(footer);

Along with that, there is a new top_margin method of Row:

let row = Row::default().top_margin(1);

Widget Implementation :jigsaw:

Line and Span now implements Widget which means it can be used as a child of other widgets.

You can also use Line::render() to render it rather than calling buffer.set_line().

frame.render_widget(Line::raw("Hello, world!"), area);
// or
Line::raw("Hello, world!").render(frame, area);

Same applies to Span and you can use Span::render() to render it rather than calling
buffer.set_span().

Line: styled :art:

Previously the style of a Line was stored in the Spans that make up the line. Now the Line
itself has a style field, which can be set with the Line::styled method.

let style = Style::new().yellow();
let content = "Hello, world!";
let line = Line::styled(content, style);

Any code that creates Lines using the struct initializer instead of constructors will fail to
compile due to the added field. This can be easily fixed by adding ..Default::default() to the
field list or by using a constructor method (Line::styled(), Line::raw()) or conversion method
(Line::from()).

  let line = Line {
      spans: vec!["".into()],
      alignment: Alignment::Left,
+     ..Default::default()
  };

Style: Accept Into :art:

All style related methods now accept S: Into<Style> instead of Style. Color and Modifier
implement Into<Style> so this is allows for more ergonomic usage. E.g.:

Line::styled("hello", Style::new().red());
Line::styled("world", Style::new().bold());

// can now be simplified to

Line::styled("hello", Color::Red);
Line::styled("world", Modifier::BOLD);

This means that if you are already passing an ambiguous type that implements Into<Style> you will
need to remove the .into() call.

Tabs: Accept Iterator of Line :bookmark_tabs:

Previously Tabs::new required a Vec, it can now accept any object that implements IntoIterator
with an item type implementing Into<Line>.

Calls to Tabs::new() whose argument is collected from an iterator will no longer compile.

For example, Tabs::new(["a","b"].into_iter().collect()) will no longer compile, because the return
type of .collect() can no longer be inferred to be a Vec<_>.

- let table = Tabs::new((0.3).map(|i| format!("{i}")).collect());
// becomes
+ let table = Tabs::new((0.3).map(|i| format!("{i}")));

New Border Sets :black_square_button:

We added the McGugan
border set, which allows for tighter borders.

Wide border set based on McGugan box technique:

▁▁▁▁▁▁▁
▏xxxxx▕
▏xxxxx▕
▔▔▔▔▔▔▔

Tall border set based on McGugan box technique:

▕▔▔▏
▕xx▏
▕xx▏
▕▁▁▏

Terminal: Frame Count :1234:

You can now get the current frame count!

let mut frame = terminal.get_frame();
let current_count = frame.count();
println!("Current frame count: {}", current_count);

This count is particularly useful when dealing with dynamic content or animations where the state
of the display changes over time. By tracking the frame count, developers can synchronize updates
or changes to the content with the rendering process.

Buffer: SSO :ant:

We now apply SSO (small string optimization) technique to text buffer in buffer::Cell i.e. use
CompactString instead of String to store the Cell::symbol field. This saves reduces the size
of memory allocations at runtime.

See the related discussion here.

Other :briefcase:

  • Rename Constraint::Proportional to Constraint::Fill
    (#880)
  • Add Rect → Size conversion methods (#789)
  • Implement Display for Text, Line, Span
    (#826)
  • Support de/serialization of TableState, ListState, and ScrollbarState via serde feature
  • Implement FromIterator for Row (#755)
  • Add From for termwiz style (#726)
  • Add style and alignment to Text (#807)
  • Collect iterator of ListItem into List
    (#775)
  • Remove deprecated Block::title_on_bottom
    (#757)
  • Make patch_style and reset_style chainable
    (#754)