diff --git a/Cargo.toml b/Cargo.toml index 33ee8a99..7c5ae2a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/examples/flex.rs b/examples/flex.rs index 233acf56..77ece6fe 100644 --- a/examples/flex.rs +++ b/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> { // setup terminal enable_raw_mode()?; @@ -36,28 +38,116 @@ fn main() -> Result<(), Box> { } fn run_app(terminal: &mut Terminal) -> 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::>()); - 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)), + ) } } diff --git a/examples/flex.tape b/examples/flex.tape new file mode 100644 index 00000000..5c77bf74 --- /dev/null +++ b/examples/flex.tape @@ -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