mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-22 12:43:16 +00:00
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:
parent
e0aa6c5e1f
commit
bb5444f618
3 changed files with 172 additions and 86 deletions
|
@ -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"
|
||||
|
|
239
examples/flex.rs
239
examples/flex.rs
|
@ -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
17
examples/flex.tape
Normal 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
|
Loading…
Reference in a new issue