refactor(example): add scroll to flex example (#811)

This commit adds `scroll` to the flex example. It also adds more examples to showcase how constraints interact. It improves the UI to make it easier to understand and short terminal friendly.

<img width="380" alt="image" src="https://github.com/ratatui-org/ratatui/assets/1813121/30541efc-ecbe-4e28-b4ef-4d5f1dc63fec"/>

---------

Co-authored-by: Dheepak Krishnamurthy <me@kdheepak.com>
This commit is contained in:
Valentin271 2024-01-14 16:49:45 +01:00 committed by GitHub
parent e0aa6c5e1f
commit bb5444f618
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 172 additions and 86 deletions

View file

@ -220,7 +220,7 @@ doc-scrape-examples = false
[[example]] [[example]]
name = "flex" name = "flex"
required-features = ["crossterm"] required-features = ["crossterm"]
doc-scrape-examples = false doc-scrape-examples = true
[[example]] [[example]]
name = "list" name = "list"

View file

@ -5,13 +5,15 @@ use crossterm::{
execute, execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
}; };
use itertools::Itertools;
use ratatui::{ use ratatui::{
layout::{Constraint::*, Flex}, layout::{Constraint::*, Flex},
prelude::*, prelude::*,
widgets::*, widgets::{block::Title, *},
}; };
const EXAMPLE_HEIGHT: u16 = 5;
const N_EXAMPLES_PER_TAB: u16 = 11;
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
// setup terminal // setup terminal
enable_raw_mode()?; enable_raw_mode()?;
@ -36,28 +38,116 @@ fn main() -> Result<(), Box<dyn Error>> {
} }
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> { fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
let mut selection = ExampleSelection::Stretch; // we always want to show the last example when scrolling
let mut app = App::default().max_scroll_offset((N_EXAMPLES_PER_TAB - 1) * EXAMPLE_HEIGHT);
loop { loop {
terminal.draw(|f| f.render_widget(selection, f.size()))?; terminal.draw(|f| f.render_widget(app, f.size()))?;
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
use KeyCode::*; use KeyCode::*;
match key.code { match key.code {
Char('q') => break Ok(()), Char('q') => break Ok(()),
Char('j') | Char('l') | Down | Right => { Char('l') | Right => app.next(),
selection = selection.next(); Char('h') | Left => app.previous(),
} Char('j') | Down => app.down(),
Char('k') | Char('h') | Up | Left => { Char('k') | Up => app.up(),
selection = selection.previous();
}
_ => (), _ => (),
} }
} }
} }
} }
#[derive(Debug, Copy, Clone)] #[derive(Default, Clone, Copy)]
struct App {
selected_example: ExampleSelection,
scroll_offset: u16,
max_scroll_offset: u16,
}
impl App {
fn max_scroll_offset(mut self, max_scroll_offset: u16) -> Self {
self.max_scroll_offset = max_scroll_offset;
self
}
fn next(&mut self) {
self.selected_example = self.selected_example.next();
}
fn previous(&mut self) {
self.selected_example = self.selected_example.previous();
}
fn up(&mut self) {
self.scroll_offset = self.scroll_offset.saturating_sub(1)
}
fn down(&mut self) {
self.scroll_offset = self
.scroll_offset
.saturating_add(1)
.min(self.max_scroll_offset)
}
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(Title::from("Flex Layouts ".bold()))
.title(" Use h l or ◄ ► to change tab and j k or ▲ ▼ to scroll".bold()),
)
.highlight_style(Style::default().yellow().bold())
.select(self.selected_example.selected())
.padding(" ", " ")
.render(area, buf);
}
}
impl Widget for App {
fn render(self, area: Rect, buf: &mut Buffer) {
let [tabs_area, demo_area] = area.split(&Layout::vertical([Fixed(3), Proportional(0)]));
// render demo content into a separate buffer so all examples fit
let mut demo_buf = Buffer::empty(Rect::new(
0,
0,
buf.area.width,
N_EXAMPLES_PER_TAB * EXAMPLE_HEIGHT,
));
self.selected_example.render(demo_buf.area, &mut demo_buf);
// render tabs into a separate buffer
let mut tabs_buf = Buffer::empty(tabs_area);
self.render_tabs(tabs_area, &mut tabs_buf);
// Assemble both buffers
// NOTE: You shouldn't do this in a production app
buf.content = tabs_buf.content;
buf.content.append(
&mut demo_buf
.content
.into_iter()
.skip((buf.area.width * self.scroll_offset) as usize)
.take(demo_area.area() as usize)
.collect(),
);
buf.resize(buf.area);
}
}
#[derive(Default, Debug, Copy, Clone)]
enum ExampleSelection { enum ExampleSelection {
#[default]
Stretch, Stretch,
StretchLast, StretchLast,
Start, Start,
@ -110,10 +200,6 @@ impl ExampleSelection {
impl Widget for ExampleSelection { impl Widget for ExampleSelection {
fn render(self, area: Rect, buf: &mut Buffer) { 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 { match self {
ExampleSelection::Stretch => self.render_example(area, buf, Flex::Stretch), ExampleSelection::Stretch => self.render_example(area, buf, Flex::Stretch),
ExampleSelection::StretchLast => self.render_example(area, buf, Flex::StretchLast), ExampleSelection::StretchLast => self.render_example(area, buf, Flex::StretchLast),
@ -127,55 +213,34 @@ impl Widget for ExampleSelection {
} }
impl ExampleSelection { 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) { fn render_example(&self, area: Rect, buf: &mut Buffer, flex: Flex) {
let [example1, example2, example3, example4, example5, example6, _] = let areas = &Layout::vertical([Fixed(EXAMPLE_HEIGHT); N_EXAMPLES_PER_TAB as usize])
area.split(&Layout::vertical([Fixed(8); 7])); .flex(Flex::Start)
.split(area);
Example::new([Length(20), Length(10)]) let examples = [
.flex(flex) vec![Length(20), Fixed(20), Percentage(20)],
.render(example1, buf); vec![Fixed(20), Percentage(20), Length(20)],
Example::new([Length(20), Fixed(10)]) vec![Percentage(20), Length(20), Fixed(20)],
.flex(flex) vec![Length(20), Length(15)],
.render(example2, buf); vec![Length(20), Fixed(20)],
Example::new([Proportional(1), Proportional(1), Length(40), Fixed(20)]) vec![Min(20), Max(20)],
.flex(flex) vec![Max(20)],
.render(example3, buf); vec![Min(20), Max(20), Length(20), Fixed(20)],
Example::new([Min(20), Length(40), Fixed(20)]) vec![Proportional(0), Proportional(0)],
.flex(flex) vec![Proportional(1), Proportional(1), Length(20), Fixed(20)],
.render(example4, buf); vec![
Example::new([Min(20), Proportional(0), Length(40), Fixed(20)]) Min(10),
.flex(flex) Proportional(3),
.render(example5, buf); Proportional(2),
Example::new([ Length(15),
Min(20), Fixed(15),
Proportional(0), ],
Percentage(10), ];
Length(40),
Fixed(20), for (area, constraints) in areas.iter().zip(examples) {
]) Example::new(constraints).flex(flex).render(*area, buf);
.flex(flex) }
.render(example6, buf);
} }
} }
@ -203,37 +268,30 @@ impl Example {
impl Widget for Example { impl Widget for Example {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let [title, legend, area] = area.split(&Layout::vertical([Ratio(1, 3); 3])); let [legend, area] = area.split(&Layout::vertical([Ratio(1, 3); 2]));
let blocks = Layout::horizontal(&self.constraints) let blocks = Layout::horizontal(&self.constraints)
.flex(self.flex) .flex(self.flex)
.split(area); .split(area);
self.heading().render(title, buf);
self.legend(legend.width as usize).render(legend, buf); self.legend(legend.width as usize).render(legend, buf);
for (i, (block, _constraint)) in blocks.iter().zip(&self.constraints).enumerate() { for (block, constraint) in blocks.iter().zip(&self.constraints) {
let text = format!("{} px", block.width); let text = format!("{} px", block.width);
let fg = Color::Indexed(i as u8 + 1); let fg = match constraint {
self.illustration(text, fg).render(*block, buf); Constraint::Ratio(_, _) => Color::Indexed(1),
Constraint::Percentage(_) => Color::Indexed(2),
Constraint::Max(_) => Color::Indexed(3),
Constraint::Min(_) => Color::Indexed(4),
Constraint::Length(_) => Color::Indexed(5),
Constraint::Fixed(_) => Color::Indexed(6),
Constraint::Proportional(_) => Color::Indexed(7),
};
self.illustration(*constraint, text, fg).render(*block, buf);
} }
} }
} }
impl Example { 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 { fn legend(&self, width: usize) -> Paragraph {
// a bar like `<----- 80 px ----->` // a bar like `<----- 80 px ----->`
let width_label = format!("{} px", width); let width_label = format!("{} px", width);
@ -241,12 +299,23 @@ impl Example {
"<{width_label:-^width$}>", "<{width_label:-^width$}>",
width = width - width_label.len() / 2 width = width - width_label.len() / 2
); );
Paragraph::new(width_bar.dark_gray()).alignment(Alignment::Center) Paragraph::new(width_bar.dark_gray())
.alignment(Alignment::Center)
.block(Block::default().padding(Padding {
left: 0,
right: 0,
top: 1,
bottom: 0,
}))
} }
fn illustration(&self, text: String, fg: Color) -> Paragraph { fn illustration(&self, constraint: Constraint, text: String, fg: Color) -> Paragraph {
Paragraph::new(text) Paragraph::new(format!("{:?}", constraint))
.alignment(Alignment::Center) .alignment(Alignment::Center)
.block(Block::bordered().style(Style::default().fg(fg))) .block(
Block::bordered()
.style(Style::default().fg(fg))
.title(block::Title::from(text).alignment(Alignment::Center)),
)
} }
} }

17
examples/flex.tape Normal file
View file

@ -0,0 +1,17 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/layout.tape`
Output "target/flex.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 1410
Hide
Type "cargo run --example=flex --features=crossterm"
Enter
Sleep 2s
Show
Sleep 2s
Right @5s 7
Sleep 2s
Left 7
Sleep 2s
Down @200ms 50