//! # [Ratatui] Constraint explorer 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
use itertools::Itertools;
crossterm::event::{self, Event, KeyCode, KeyEventKind},
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
palette::tailwind::{BLUE, SKY, SLATE, STONE},
text::{Line, Span, Text},
widgets::{Block, Paragraph, Widget, Wrap},
use strum::{Display, EnumIter, FromRepr};
fn main() -> Result<()> {
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
constraints: Vec<Constraint>,
#[derive(Debug, Default, PartialEq, Eq)]
/// A variant of [`Constraint`] that can be rendered as a tab.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, EnumIter, FromRepr, Display)]
/// A widget that renders a [`Constraint`] as a block. E.g.:
/// A widget that renders a spacer with a label indicating the width of the spacer. E.g.:
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
self.insert_test_defaults();
while self.is_running() {
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
// TODO remove these - these are just for testing
fn insert_test_defaults(&mut self) {
fn is_running(&self) -> bool {
self.mode == AppMode::Running
fn handle_events(&mut self) -> Result<()> {
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.exit(),
KeyCode::Char('1') => self.swap_constraint(ConstraintName::Min),
KeyCode::Char('2') => self.swap_constraint(ConstraintName::Max),
KeyCode::Char('3') => self.swap_constraint(ConstraintName::Length),
KeyCode::Char('4') => self.swap_constraint(ConstraintName::Percentage),
KeyCode::Char('5') => self.swap_constraint(ConstraintName::Ratio),
KeyCode::Char('6') => self.swap_constraint(ConstraintName::Fill),
KeyCode::Char('+') => self.increment_spacing(),
KeyCode::Char('-') => self.decrement_spacing(),
KeyCode::Char('x') => self.delete_block(),
KeyCode::Char('a') => self.insert_block(),
KeyCode::Char('k') | KeyCode::Up => self.increment_value(),
KeyCode::Char('j') | KeyCode::Down => self.decrement_value(),
KeyCode::Char('h') | KeyCode::Left => self.prev_block(),
KeyCode::Char('l') | KeyCode::Right => self.next_block(),
fn increment_value(&mut self) {
let Some(constraint) = self.constraints.get_mut(self.selected_index) else {
| Constraint::Percentage(v) => *v = v.saturating_add(1),
Constraint::Ratio(_n, d) => *d = d.saturating_add(1),
fn decrement_value(&mut self) {
let Some(constraint) = self.constraints.get_mut(self.selected_index) else {
| Constraint::Percentage(v) => *v = v.saturating_sub(1),
Constraint::Ratio(_n, d) => *d = d.saturating_sub(1),
/// select the next block with wrap around
fn next_block(&mut self) {
if self.constraints.is_empty() {
let len = self.constraints.len();
self.selected_index = (self.selected_index + 1) % len;
/// select the previous block with wrap around
fn prev_block(&mut self) {
if self.constraints.is_empty() {
let len = self.constraints.len();
self.selected_index = (self.selected_index + self.constraints.len() - 1) % len;
/// delete the selected block
fn delete_block(&mut self) {
if self.constraints.is_empty() {
self.constraints.remove(self.selected_index);
self.selected_index = self.selected_index.saturating_sub(1);
/// insert a block after the selected block
fn insert_block(&mut self) {
.min(self.constraints.len());
let constraint = Constraint::Length(self.value);
self.constraints.insert(index, constraint);
self.selected_index = index;
fn increment_spacing(&mut self) {
self.spacing = self.spacing.saturating_add(1);
fn decrement_spacing(&mut self) {
self.spacing = self.spacing.saturating_sub(1);
self.mode = AppMode::Quit;
fn swap_constraint(&mut self, name: ConstraintName) {
if self.constraints.is_empty() {
let constraint = match name {
ConstraintName::Length => Length(self.value),
ConstraintName::Percentage => Percentage(self.value),
ConstraintName::Min => Min(self.value),
ConstraintName::Max => Max(self.value),
ConstraintName::Fill => Fill(self.value),
ConstraintName::Ratio => Ratio(1, u32::from(self.value) / 4), // for balance
self.constraints[self.selected_index] = constraint;
impl From<Constraint> for ConstraintName {
fn from(constraint: Constraint) -> Self {
Length(_) => Self::Length,
Percentage(_) => Self::Percentage,
Ratio(_, _) => Self::Ratio,
fn render(self, area: Rect, buf: &mut Buffer) {
let [header_area, instructions_area, swap_legend_area, _, blocks_area] =
Length(2), // instructions
Length(1), // swap key legend
App::header().render(header_area, buf);
App::instructions().render(instructions_area, buf);
App::swap_legend().render(swap_legend_area, buf);
self.render_layout_blocks(blocks_area, buf);
const HEADER_COLOR: Color = SLATE.c200;
const TEXT_COLOR: Color = SLATE.c400;
const AXIS_COLOR: Color = SLATE.c500;
fn header() -> impl Widget {
let text = "Constraint Explorer";
text.bold().fg(Self::HEADER_COLOR).into_centered_line()
fn instructions() -> impl Widget {
let text = "◄ ►: select, ▲ ▼: edit, 1-6: swap, a: add, x: delete, q: quit, + -: spacing";
.wrap(Wrap { trim: false })
fn swap_legend() -> impl Widget {
#[allow(unstable_name_collisions)]
ConstraintName::Percentage,
format!(" {i}: {name} ", i = i + 1)
.intersperse(Span::from(" "))
.wrap(Wrap { trim: false })
/// A bar like `<----- 80 px (gap: 2 px) ----->`
/// Only shows the gap when spacing is not zero
fn axis(&self, width: u16) -> impl Widget {
let label = if self.spacing != 0 {
format!("{} px (gap: {} px)", width, self.spacing)
let bar_width = width.saturating_sub(2) as usize; // we want to `<` and `>` at the ends
let width_bar = format!("<{label:-^bar_width$}>");
Paragraph::new(width_bar).fg(Self::AXIS_COLOR).centered()
fn render_layout_blocks(&self, area: Rect, buf: &mut Buffer) {
let [user_constraints, area] = Layout::vertical([Length(3), Fill(1)])
self.render_user_constraints_legend(user_constraints, buf);
let [start, center, end, space_around, space_between] =
Layout::vertical([Length(7); 5]).areas(area);
self.render_layout_block(Flex::Start, start, buf);
self.render_layout_block(Flex::Center, center, buf);
self.render_layout_block(Flex::End, end, buf);
self.render_layout_block(Flex::SpaceAround, space_around, buf);
self.render_layout_block(Flex::SpaceBetween, space_between, buf);
fn render_user_constraints_legend(&self, area: Rect, buf: &mut Buffer) {
let constraints = self.constraints.iter().map(|_| Constraint::Fill(1));
let blocks = Layout::horizontal(constraints).split(area);
for (i, (area, constraint)) in blocks.iter().zip(self.constraints.iter()).enumerate() {
let selected = self.selected_index == i;
ConstraintBlock::new(*constraint, selected, true).render(*area, buf);
fn render_layout_block(&self, flex: Flex, area: Rect, buf: &mut Buffer) {
let [label_area, axis_area, blocks_area] =
Layout::vertical([Length(1), Max(1), Length(4)]).areas(area);
if label_area.height > 0 {
format!("Flex::{flex:?}").bold().render(label_area, buf);
self.axis(area.width).render(axis_area, buf);
let (blocks, spacers) = Layout::horizontal(&self.constraints)
.split_with_spacers(blocks_area);
for (i, (area, constraint)) in blocks.iter().zip(self.constraints.iter()).enumerate() {
let selected = self.selected_index == i;
ConstraintBlock::new(*constraint, selected, false).render(*area, buf);
for area in spacers.iter() {
SpacerBlock.render(*area, buf);
impl Widget for ConstraintBlock {
fn render(self, area: Rect, buf: &mut Buffer) {
1 => self.render_1px(area, buf),
2 => self.render_2px(area, buf),
_ => self.render_4px(area, buf),
const TEXT_COLOR: Color = SLATE.c200;
const fn new(constraint: Constraint, selected: bool, legend: bool) -> Self {
fn label(&self, width: u16) -> String {
let long_width = format!("{width} px");
let short_width = format!("{width}");
// border takes up 2 columns
let available_space = width.saturating_sub(2) as usize;
let width_label = if long_width.len() < available_space {
} else if short_width.len() < available_space {
format!("{}\n{}", self.constraint, width_label)
fn render_1px(&self, area: Rect, buf: &mut Buffer) {
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
let main_color = ConstraintName::from(self.constraint).color();
let selected_color = if self.selected {
fn render_2px(&self, area: Rect, buf: &mut Buffer) {
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
let main_color = ConstraintName::from(self.constraint).color();
let selected_color = if self.selected {
.border_set(symbols::border::QUADRANT_OUTSIDE)
.border_style(Style::reset().fg(selected_color).reversed())
fn render_4px(&self, area: Rect, buf: &mut Buffer) {
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
let main_color = ConstraintName::from(self.constraint).color();
let selected_color = if self.selected {
let color = if self.legend {
let label = self.label(area.width);
let block = Block::bordered()
.border_set(symbols::border::QUADRANT_OUTSIDE)
.border_style(Style::reset().fg(color).reversed())
let border_color = if self.selected {
if let Some(last_row) = area.rows().last() {
buf.set_style(last_row, border_color);
impl Widget for SpacerBlock {
fn render(self, area: Rect, buf: &mut Buffer) {
2 => Self::render_2px(area, buf),
3 => Self::render_3px(area, buf),
_ => Self::render_4px(area, buf),
const TEXT_COLOR: Color = SLATE.c500;
const BORDER_COLOR: Color = SLATE.c600;
/// A block with a corner borders
fn block() -> impl Widget {
let corners_only = symbols::border::Set {
top_left: line::NORMAL.top_left,
top_right: line::NORMAL.top_right,
bottom_left: line::NORMAL.bottom_left,
bottom_right: line::NORMAL.bottom_right,
.border_set(corners_only)
.border_style(Self::BORDER_COLOR)
/// A vertical line used if there is not enough space to render the block
fn line() -> impl Widget {
Paragraph::new(Text::from(vec![
.style(Self::BORDER_COLOR)
/// A label that says "Spacer" if there is enough space
fn spacer_label(width: u16) -> impl Widget {
let label = if width >= 6 { "Spacer" } else { "" };
label.fg(Self::TEXT_COLOR).into_centered_line()
/// A label that says "8 px" if there is enough space
fn label(width: u16) -> impl Widget {
let long_label = format!("{width} px");
let short_label = format!("{width}");
let label = if long_label.len() < width as usize {
} else if short_label.len() < width as usize {
Line::styled(label, Self::TEXT_COLOR).centered()
fn render_2px(area: Rect, buf: &mut Buffer) {
Self::block().render(area, buf);
Self::line().render(area, buf);
fn render_3px(area: Rect, buf: &mut Buffer) {
Self::block().render(area, buf);
Self::line().render(area, buf);
let row = area.rows().nth(1).unwrap_or_default();
Self::spacer_label(area.width).render(row, buf);
fn render_4px(area: Rect, buf: &mut Buffer) {
Self::block().render(area, buf);
Self::line().render(area, buf);
let row = area.rows().nth(1).unwrap_or_default();
Self::spacer_label(area.width).render(row, buf);
let row = area.rows().nth(2).unwrap_or_default();
Self::label(area.width).render(row, buf);
const fn color(self) -> Color {
Self::Length => SLATE.c700,
Self::Percentage => SLATE.c800,
Self::Ratio => SLATE.c900,
Self::Fill => SLATE.c950,
const fn lighter_color(self) -> Color {
Self::Length => STONE.c500,
Self::Percentage => STONE.c600,
Self::Ratio => STONE.c700,
Self::Fill => STONE.c800,