mirror of
https://github.com/ratatui-org/ratatui
synced 2025-02-16 22:18:51 +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]]
|
[[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"
|
||||||
|
|
239
examples/flex.rs
239
examples/flex.rs
|
@ -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
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…
Add table
Reference in a new issue