Files
ratatui/examples/flex.rs
Dheepak Krishnamurthy de97a1f1da feat: Add flex to layout
This PR adds a new way to space elements in a `Layout`.

Loosely based on
[flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/), this
PR adds a `Flex` enum with the following variants:

- Start
- Center
- End
- SpaceAround
- SpaceBetween

<img width="380" alt="image" src="https://github.com/ratatui-org/ratatui/assets/1813121/b744518c-eae7-4e35-bbc4-fe3c95193cde">

It also adds two more variants, to make this backward compatible and to
make it replace `SegmentSize`:

- StretchLast (default in the `Flex` enum, also behavior matches old
  default `SegmentSize::LastTakesRemainder`)
- Stretch (behavior matches `SegmentSize::EvenDistribution`)

The `Start` variant from above matches `SegmentSize::None`.

This allows `Flex` to be a complete replacement for `SegmentSize`, hence
this PR also deprecates the `segment_size` constructor on `Layout`.
`SegmentSize` is still used in `Table` but under the hood `segment_size`
maps to `Flex` with all tests passing unchanged.

I also put together a simple example for `Flex` layouts so that I could
test it visually, shared below:

https://github.com/ratatui-org/ratatui/assets/1813121/c8716c59-493f-4631-add5-feecf4bd4e06
2024-01-13 01:56:27 -08:00

253 lines
7.3 KiB
Rust

use std::{error::Error, io};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{
layout::{Constraint::*, Flex},
prelude::*,
widgets::*,
};
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let res = run_app(&mut terminal);
// restore terminal
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
let mut selection = ExampleSelection::Stretch;
loop {
terminal.draw(|f| f.render_widget(selection, f.size()))?;
if let Event::Key(key) = event::read()? {
use KeyCode::*;
match key.code {
Char('q') => break Ok(()),
Char('j') | Char('l') | Down | Right => {
selection = selection.next();
}
Char('k') | Char('h') | Up | Left => {
selection = selection.previous();
}
_ => (),
}
}
}
}
#[derive(Debug, Copy, Clone)]
enum ExampleSelection {
Stretch,
StretchLast,
Start,
Center,
End,
SpaceAround,
SpaceBetween,
}
impl ExampleSelection {
fn previous(&self) -> Self {
use ExampleSelection::*;
match *self {
Stretch => Stretch,
StretchLast => Stretch,
Start => StretchLast,
Center => Start,
End => Center,
SpaceAround => End,
SpaceBetween => SpaceAround,
}
}
fn next(&self) -> Self {
use ExampleSelection::*;
match *self {
Stretch => StretchLast,
StretchLast => Start,
Start => Center,
Center => End,
End => SpaceAround,
SpaceAround => SpaceBetween,
SpaceBetween => SpaceBetween,
}
}
fn selected(&self) -> usize {
use ExampleSelection::*;
match self {
Stretch => 0,
StretchLast => 1,
Start => 2,
Center => 3,
End => 4,
SpaceAround => 5,
SpaceBetween => 6,
}
}
}
impl Widget for ExampleSelection {
fn render(self, area: Rect, buf: &mut Buffer) {
let [tabs, area] = area.split(&Layout::vertical([Fixed(3), Proportional(0)]));
self.render_tabs(tabs, buf);
match self {
ExampleSelection::Stretch => self.render_example(area, buf, Flex::Stretch),
ExampleSelection::StretchLast => self.render_example(area, buf, Flex::StretchLast),
ExampleSelection::Start => self.render_example(area, buf, Flex::Start),
ExampleSelection::Center => self.render_example(area, buf, Flex::Center),
ExampleSelection::End => self.render_example(area, buf, Flex::End),
ExampleSelection::SpaceAround => self.render_example(area, buf, Flex::SpaceAround),
ExampleSelection::SpaceBetween => self.render_example(area, buf, Flex::SpaceBetween),
}
}
}
impl ExampleSelection {
fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
Tabs::new(
[
ExampleSelection::Stretch,
ExampleSelection::StretchLast,
ExampleSelection::Start,
ExampleSelection::Center,
ExampleSelection::End,
ExampleSelection::SpaceAround,
ExampleSelection::SpaceBetween,
]
.iter()
.map(|e| format!("{:?}", e)),
)
.block(Block::bordered().title("Flex Layouts"))
.highlight_style(Style::default().yellow())
.select(self.selected())
.padding(" ", " ")
.render(area, buf);
}
fn render_example(&self, area: Rect, buf: &mut Buffer, flex: Flex) {
let [example1, example2, example3, example4, example5, example6, _] =
area.split(&Layout::vertical([Fixed(8); 7]));
Example::new([Length(20), Length(10)])
.flex(flex)
.render(example1, buf);
Example::new([Length(20), Fixed(10)])
.flex(flex)
.render(example2, buf);
Example::new([Proportional(1), Proportional(1), Length(40), Fixed(20)])
.flex(flex)
.render(example3, buf);
Example::new([Min(20), Length(40), Fixed(20)])
.flex(flex)
.render(example4, buf);
Example::new([Min(20), Proportional(0), Length(40), Fixed(20)])
.flex(flex)
.render(example5, buf);
Example::new([
Min(20),
Proportional(0),
Percentage(10),
Length(40),
Fixed(20),
])
.flex(flex)
.render(example6, buf);
}
}
struct Example {
constraints: Vec<Constraint>,
flex: Flex,
}
impl Example {
fn new<C>(constraints: C) -> Self
where
C: Into<Vec<Constraint>>,
{
Self {
constraints: constraints.into(),
flex: Flex::default(),
}
}
fn flex(mut self, flex: Flex) -> Self {
self.flex = flex;
self
}
}
impl Widget for Example {
fn render(self, area: Rect, buf: &mut Buffer) {
let [title, legend, area] = area.split(&Layout::vertical([Ratio(1, 3); 3]));
let blocks = Layout::horizontal(&self.constraints)
.flex(self.flex)
.split(area);
self.heading().render(title, buf);
self.legend(legend.width as usize).render(legend, buf);
for (i, (block, _constraint)) in blocks.iter().zip(&self.constraints).enumerate() {
let text = format!("{} px", block.width);
let fg = Color::Indexed(i as u8 + 1);
self.illustration(text, fg).render(*block, buf);
}
}
}
impl Example {
fn heading(&self) -> Paragraph {
// Renders the following
//
// Fixed(40), Proportional(0)
let spans = self.constraints.iter().enumerate().map(|(i, c)| {
let color = Color::Indexed(i as u8 + 1);
Span::styled(format!("{:?}", c), color)
});
let heading =
Line::from(Itertools::intersperse(spans, Span::raw(", ")).collect::<Vec<Span>>());
Paragraph::new(heading).block(Block::default().padding(Padding::vertical(1)))
}
fn legend(&self, width: usize) -> Paragraph {
// a bar like `<----- 80 px ----->`
let width_label = format!("{} px", width);
let width_bar = format!(
"<{width_label:-^width$}>",
width = width - width_label.len() / 2
);
Paragraph::new(width_bar.dark_gray()).alignment(Alignment::Center)
}
fn illustration(&self, text: String, fg: Color) -> Paragraph {
Paragraph::new(text)
.alignment(Alignment::Center)
.block(Block::bordered().style(Style::default().fg(fg)))
}
}