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]]
name = "flex"
required-features = ["crossterm"]
doc-scrape-examples = false
doc-scrape-examples = true
[[example]]
name = "list"

View file

@ -5,13 +5,15 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{
layout::{Constraint::*, Flex},
prelude::*,
widgets::*,
widgets::{block::Title, *},
};
const EXAMPLE_HEIGHT: u16 = 5;
const N_EXAMPLES_PER_TAB: u16 = 11;
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
@ -36,28 +38,116 @@ fn main() -> Result<(), Box<dyn Error>> {
}
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 {
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()? {
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();
}
Char('l') | Right => app.next(),
Char('h') | Left => app.previous(),
Char('j') | Down => app.down(),
Char('k') | Up => app.up(),
_ => (),
}
}
}
}
#[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 {
#[default]
Stretch,
StretchLast,
Start,
@ -110,10 +200,6 @@ impl ExampleSelection {
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),
@ -127,55 +213,34 @@ impl Widget for 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) {
let [example1, example2, example3, example4, example5, example6, _] =
area.split(&Layout::vertical([Fixed(8); 7]));
let areas = &Layout::vertical([Fixed(EXAMPLE_HEIGHT); N_EXAMPLES_PER_TAB as usize])
.flex(Flex::Start)
.split(area);
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);
let examples = [
vec![Length(20), Fixed(20), Percentage(20)],
vec![Fixed(20), Percentage(20), Length(20)],
vec![Percentage(20), Length(20), Fixed(20)],
vec![Length(20), Length(15)],
vec![Length(20), Fixed(20)],
vec![Min(20), Max(20)],
vec![Max(20)],
vec![Min(20), Max(20), Length(20), Fixed(20)],
vec![Proportional(0), Proportional(0)],
vec![Proportional(1), Proportional(1), Length(20), Fixed(20)],
vec![
Min(10),
Proportional(3),
Proportional(2),
Length(15),
Fixed(15),
],
];
for (area, constraints) in areas.iter().zip(examples) {
Example::new(constraints).flex(flex).render(*area, buf);
}
}
}
@ -203,37 +268,30 @@ impl Example {
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 [legend, area] = area.split(&Layout::vertical([Ratio(1, 3); 2]));
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() {
for (block, constraint) in blocks.iter().zip(&self.constraints) {
let text = format!("{} px", block.width);
let fg = Color::Indexed(i as u8 + 1);
self.illustration(text, fg).render(*block, buf);
let fg = match constraint {
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 {
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);
@ -241,12 +299,23 @@ impl Example {
"<{width_label:-^width$}>",
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 {
Paragraph::new(text)
fn illustration(&self, constraint: Constraint, text: String, fg: Color) -> Paragraph {
Paragraph::new(format!("{:?}", constraint))
.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