From 554805d6cbbf140c6da474daa891e9e754a5d281 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Fri, 4 Aug 2023 00:46:37 -0700 Subject: [PATCH] docs(examples): Update block example (#351) ![Block example](https://vhs.charm.sh/vhs-5X6hpReuDBKjD6hLxmDQ6F.gif) --- examples/README.md | 2 +- examples/block.rs | 330 ++++++++++++++++++++++++++++++-------------- examples/block.tape | 7 +- 3 files changed, 235 insertions(+), 104 deletions(-) diff --git a/examples/README.md b/examples/README.md index ff6f7749..58eed78d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -189,7 +189,7 @@ done ``` --> [barchart.gif]: https://vhs.charm.sh/vhs-6ioxdeRBVkVpyXcjIEVaJU.gif -[block.gif]: https://vhs.charm.sh/vhs-1sEo9vVkHRwFtu95MOXrTj.gif +[block.gif]: https://vhs.charm.sh/vhs-5X6hpReuDBKjD6hLxmDQ6F.gif [calendar.gif]: https://vhs.charm.sh/vhs-1dBcpMSSP80WkBgm4lBhNo.gif [canvas.gif]: https://vhs.charm.sh/vhs-4zeWEPF6bLEFSHuJrvaHlN.gif [chart.gif]: https://vhs.charm.sh/vhs-zRzsE2AwRixQhcWMTAeF1.gif diff --git a/examples/block.rs b/examples/block.rs index ea95eef1..152e652f 100644 --- a/examples/block.rs +++ b/examples/block.rs @@ -1,123 +1,253 @@ -use std::{error::Error, io, time::Duration}; +use std::{ + error::Error, + io::{stdout, Stdout}, + ops::ControlFlow, + time::Duration, +}; use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + event::{self, Event, KeyCode}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; -use ratatui::{prelude::*, widgets::*}; +use itertools::Itertools; +use ratatui::{ + prelude::*, + widgets::{ + block::{Position, Title}, + Block, BorderType, Borders, Padding, Paragraph, Wrap, + }, +}; -fn main() -> Result<(), Box> { - // setup terminal - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; +// These type aliases are used to make the code more readable by reducing repetition of the generic +// types. They are not necessary for the functionality of the code. +type Frame<'a> = ratatui::Frame<'a, CrosstermBackend>; +type Terminal = ratatui::Terminal>; +type Result = std::result::Result>; - // create app and run it - let res = run_app(&mut terminal); +fn main() -> Result<()> { + let mut terminal = setup_terminal()?; + let result = run(&mut terminal); + restore_terminal(terminal)?; - // restore terminal - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.clear()?; - terminal.show_cursor()?; - - if let Err(err) = res { - println!("{err:?}"); + if let Err(err) = result { + eprintln!("{err:?}"); } - Ok(()) } -fn run_app(terminal: &mut Terminal) -> io::Result<()> { +fn setup_terminal() -> Result { + enable_raw_mode()?; + let mut stdout = stdout(); + execute!(stdout, EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout); + let terminal = Terminal::new(backend)?; + Ok(terminal) +} + +fn restore_terminal(mut terminal: Terminal) -> Result<()> { + disable_raw_mode()?; + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; + Ok(()) +} + +fn run(terminal: &mut Terminal) -> Result<()> { loop { terminal.draw(ui)?; - - if event::poll(Duration::from_millis(250))? { - if let Event::Key(key) = event::read()? { - if let KeyCode::Char('q') = key.code { - return Ok(()); - } - } + if handle_events()?.is_break() { + return Ok(()); } } } -fn ui(f: &mut Frame) { - // Wrapping block for a group - // Just draw the block and the group on the same area and build the group - let outer = f.size(); - let outer_block = Block::default() - .borders(Borders::ALL) - .title(block::Title::from("Main block with round corners").alignment(Alignment::Center)) - .border_type(BorderType::Rounded); - let inner = outer_block.inner(outer); - let [top, bottom] = *Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(inner) - else { - return; - }; - let [top_left, top_right] = *Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(top) - else { - return; - }; - let [bottom_left, bottom_right] = *Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(bottom) - else { - return; - }; +fn handle_events() -> Result> { + if event::poll(Duration::from_millis(100))? { + if let Event::Key(key) = event::read()? { + if let KeyCode::Char('q') = key.code { + return Ok(ControlFlow::Break(())); + } + } + } + Ok(ControlFlow::Continue(())) +} - let top_left_block = Block::default() - .title("With Green Background") - .borders(Borders::all()) - .on_green(); - let top_right_block = Block::default() +fn ui(frame: &mut Frame) { + let (title_area, layout) = calculate_layout(frame.size()); + + render_title(frame, title_area); + + let paragraph = placeholder_paragraph(); + + render_borders(¶graph, Borders::ALL, frame, layout[0][0]); + render_borders(¶graph, Borders::NONE, frame, layout[0][1]); + render_borders(¶graph, Borders::LEFT, frame, layout[1][0]); + render_borders(¶graph, Borders::RIGHT, frame, layout[1][1]); + render_borders(¶graph, Borders::TOP, frame, layout[2][0]); + render_borders(¶graph, Borders::BOTTOM, frame, layout[2][1]); + + render_border_type(¶graph, BorderType::Plain, frame, layout[3][0]); + render_border_type(¶graph, BorderType::Rounded, frame, layout[3][1]); + render_border_type(¶graph, BorderType::Double, frame, layout[4][0]); + render_border_type(¶graph, BorderType::Thick, frame, layout[4][1]); + + render_styled_block(¶graph, frame, layout[5][0]); + render_styled_borders(¶graph, frame, layout[5][1]); + render_styled_title(¶graph, frame, layout[6][0]); + render_styled_title_content(¶graph, frame, layout[6][1]); + render_multiple_titles(¶graph, frame, layout[7][0]); + render_multiple_title_positions(¶graph, frame, layout[7][1]); + render_padding(¶graph, frame, layout[8][0]); + render_nested_blocks(¶graph, frame, layout[8][1]); +} + +/// Calculate the layout of the UI elements. +/// +/// Returns a tuple of the title area and the main areas. +fn calculate_layout(area: Rect) -> (Rect, Vec>) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![Constraint::Length(1), Constraint::Min(0)]) + .split(area); + let title_area = layout[0]; + let main_areas = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![Constraint::Max(4); 9]) + .split(layout[1]) + .iter() + .map(|&area| { + Layout::default() + .direction(Direction::Horizontal) + .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(area) + .to_vec() + }) + .collect_vec(); + (title_area, main_areas) +} + +fn render_title(frame: &mut Frame, area: Rect) { + frame.render_widget( + Paragraph::new("Block example. Press q to quit") + .dark_gray() + .alignment(Alignment::Center), + area, + ); +} + +fn placeholder_paragraph() -> Paragraph<'static> { + let text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."; + Paragraph::new(text.dark_gray()).wrap(Wrap { trim: true }) +} + +fn render_borders(paragraph: &Paragraph, border: Borders, frame: &mut Frame, area: Rect) { + let block = Block::new() + .borders(border) + .title(format!("Borders::{border:#?}", border = border)); + frame.render_widget(paragraph.clone().block(block), area); +} + +fn render_border_type( + paragraph: &Paragraph, + border_type: BorderType, + frame: &mut Frame, + area: Rect, +) { + let block = Block::new() + .borders(Borders::ALL) + .border_type(border_type) + .title(format!("BorderType::{border_type:#?}")); + frame.render_widget(paragraph.clone().block(block), area); +} +fn render_styled_borders(paragraph: &Paragraph, frame: &mut Frame, area: Rect) { + let block = Block::new() + .borders(Borders::ALL) + .border_style(Style::new().blue().on_white().bold().italic()) + .title("Styled borders"); + frame.render_widget(paragraph.clone().block(block), area); +} + +fn render_styled_block(paragraph: &Paragraph, frame: &mut Frame, area: Rect) { + let block = Block::new() + .borders(Borders::ALL) + .style(Style::new().blue().on_white().bold().italic()) + .title("Styled block"); + frame.render_widget(paragraph.clone().block(block), area); +} + +// Note: this currently renders incorrectly, see https://github.com/ratatui-org/ratatui/issues/349 +fn render_styled_title(paragraph: &Paragraph, frame: &mut Frame, area: Rect) { + let block = Block::new() + .borders(Borders::ALL) + .title("Styled title") + .title_style(Style::new().blue().on_white().bold().italic()); + frame.render_widget(paragraph.clone().block(block), area); +} + +fn render_styled_title_content(paragraph: &Paragraph, frame: &mut Frame, area: Rect) { + let title = Line::from(vec![ + "Styled ".blue().on_white().bold().italic(), + "title content".red().on_white().bold().italic(), + ]); + let block = Block::new().borders(Borders::ALL).title(title); + frame.render_widget(paragraph.clone().block(block), area); +} + +fn render_multiple_titles(paragraph: &Paragraph, frame: &mut Frame, area: Rect) { + let block = Block::new() + .borders(Borders::ALL) + .title("Multiple".blue().on_white().bold().italic()) + .title("Titles".red().on_white().bold().italic()); + frame.render_widget(paragraph.clone().block(block), area); +} + +fn render_multiple_title_positions(paragraph: &Paragraph, frame: &mut Frame, area: Rect) { + let block = Block::new() + .borders(Borders::ALL) .title( - block::Title::from("With styled title".white().on_red().bold()) + Title::from("top left") + .position(Position::Top) + .alignment(Alignment::Left), + ) + .title( + Title::from("top center") + .position(Position::Top) + .alignment(Alignment::Center), + ) + .title( + Title::from("top right") + .position(Position::Top) .alignment(Alignment::Right), ) - .borders(Borders::ALL); - let bottom_left_block = Paragraph::new("Text inside padded block").block( - Block::default() - .title("With borders") - .borders(Borders::ALL) - .padding(Padding { - left: 4, - right: 4, - top: 2, - bottom: 2, - }), - ); - let bottom_right_block = Block::default() - .title("With styled borders and doubled borders") - .border_style(Style::default().fg(Color::Cyan)) - .borders(Borders::LEFT | Borders::RIGHT) - .border_type(BorderType::Double) - .padding(Padding::uniform(1)); - let bottom_inner_block = Block::default() - .title("Block inside padded block") - .borders(Borders::ALL); - - f.render_widget(outer_block, outer); - f.render_widget(Clear, top_left); - f.render_widget(top_left_block, top_left); - f.render_widget(top_right_block, top_right); - f.render_widget(bottom_left_block, bottom_left); - let bottom_right_inner = bottom_right_block.inner(bottom_right); - f.render_widget(bottom_right_block, bottom_right); - f.render_widget(bottom_inner_block, bottom_right_inner); + .title( + Title::from("bottom left") + .position(Position::Bottom) + .alignment(Alignment::Left), + ) + .title( + Title::from("bottom center") + .position(Position::Bottom) + .alignment(Alignment::Center), + ) + .title( + Title::from("bottom right") + .position(Position::Bottom) + .alignment(Alignment::Right), + ); + frame.render_widget(paragraph.clone().block(block), area); +} + +fn render_padding(paragraph: &Paragraph, frame: &mut Frame, area: Rect) { + let block = Block::new() + .borders(Borders::ALL) + .title("Padding") + .padding(Padding::new(5, 10, 1, 2)); + frame.render_widget(paragraph.clone().block(block), area); +} + +fn render_nested_blocks(paragraph: &Paragraph, frame: &mut Frame, area: Rect) { + let outer_block = Block::new().borders(Borders::ALL).title("Outer block"); + let inner_block = Block::new().borders(Borders::ALL).title("Inner block"); + let inner = outer_block.inner(area); + frame.render_widget(outer_block, area); + frame.render_widget(paragraph.clone().block(inner_block), inner); } diff --git a/examples/block.tape b/examples/block.tape index 1f6a2956..90045082 100644 --- a/examples/block.tape +++ b/examples/block.tape @@ -1,11 +1,12 @@ # This is a vhs script. See https://github.com/charmbracelet/vhs for more info. # To run this script, install vhs and run `vhs ./examples/block.tape` Output "target/block.gif" +Set Theme "Builtin Dark" Set Width 1200 -Set Height 800 +Set Height 1200 Hide Type "cargo run --example=block" Enter -Sleep 1s +Sleep 2s Show -Sleep 5s +Sleep 2s