From 813f707892d77177b5f7bfe910ff0d312f17eb83 Mon Sep 17 00:00:00 2001 From: Valentin271 <36198422+Valentin271@users.noreply.github.com> Date: Tue, 16 Jan 2024 05:56:40 +0100 Subject: [PATCH] refactor(example): improve constraints and flex examples (#817) This PR is a follow up to https://github.com/ratatui-org/ratatui/pull/811. It improves the UI of the layouts by - thoughtful accessible color that represent priority in constraints resolving - using QUADRANT_OUTSIDE symbol set for block rendering - adding a scrollbar - panic handling - refactoring for readability to name a few. Here are some example gifs of the outcome: ![constraints](https://github.com/ratatui-org/ratatui/assets/381361/8eed34cf-e959-472f-961b-d439bfe3324e) ![flex](https://github.com/ratatui-org/ratatui/assets/381361/3195a56c-9cb6-4525-bc1c-b969c0d6a812) --------- Co-authored-by: Dheepak Krishnamurthy Co-authored-by: Josh McKinney --- examples/constraints.rs | 768 ++++++++++++++++++-------------------- examples/constraints.tape | 2 +- examples/flex.rs | 665 +++++++++++++++++++-------------- 3 files changed, 752 insertions(+), 683 deletions(-) diff --git a/examples/constraints.rs b/examples/constraints.rs index c9be848c..c012104c 100644 --- a/examples/constraints.rs +++ b/examples/constraints.rs @@ -1,435 +1,374 @@ -use std::{error::Error, io}; +use std::io::{self, stdout}; +use color_eyre::{config::HookBuilder, Result}; use crossterm::{ event::{self, Event, KeyCode}, - execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + ExecutableCommand, }; -use itertools::Itertools; -use ratatui::{layout::Constraint::*, prelude::*, widgets::*}; +use ratatui::{layout::Constraint::*, prelude::*, style::palette::tailwind, widgets::*}; +use strum::{Display, EnumIter, FromRepr, IntoEnumIterator}; -fn main() -> Result<(), Box> { - // setup terminal - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; +const SPACER_HEIGHT: u16 = 0; +const ILLUSTRATION_HEIGHT: u16 = 4; +const EXAMPLE_HEIGHT: u16 = ILLUSTRATION_HEIGHT + SPACER_HEIGHT; - // Each line in the example is a layout - // There is on average 4 row per example - // 4 row * 7 example = 28 - // Plus additional layout for tabs ... - // Examples might also grow in a very near future - Layout::init_cache(50); +// priority 1 +const FIXED_COLOR: Color = tailwind::RED.c900; +// priority 2 +const MIN_COLOR: Color = tailwind::BLUE.c900; +const MAX_COLOR: Color = tailwind::BLUE.c800; +// priority 3 +const LENGTH_COLOR: Color = tailwind::SLATE.c700; +const PERCENTAGE_COLOR: Color = tailwind::SLATE.c800; +const RATIO_COLOR: Color = tailwind::SLATE.c900; +// priority 4 +const PROPORTIONAL_COLOR: Color = tailwind::SLATE.c950; - // create app and run it - let res = run_app(&mut terminal); - - // restore terminal - disable_raw_mode()?; - execute!(terminal.backend_mut(), LeaveAlternateScreen)?; - terminal.show_cursor()?; - - if let Err(err) = res { - println!("{err:?}"); - } - - Ok(()) +#[derive(Default, Clone, Copy)] +struct App { + selected_tab: SelectedTab, + scroll_offset: u16, + max_scroll_offset: u16, + state: AppState, } -fn run_app(terminal: &mut Terminal) -> io::Result<()> { - let mut selection = ExampleSelection::Fixed; - loop { - terminal.draw(|f| f.render_widget(selection, 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(); - } - _ => (), - } - } - } -} - -#[derive(Debug, Copy, Clone)] -enum ExampleSelection { +/// Tabs for the different examples +/// +/// The order of the variants is the order in which they are displayed. +#[derive(Default, Debug, Copy, Clone, Display, FromRepr, EnumIter, PartialEq, Eq)] +enum SelectedTab { + #[default] Fixed, + Min, + Max, Length, Percentage, Ratio, Proportional, - Min, - Max, } -impl ExampleSelection { - fn previous(&self) -> Self { - use ExampleSelection::*; - match *self { - Fixed => Fixed, - Length => Fixed, - Percentage => Length, - Ratio => Percentage, - Proportional => Ratio, - Min => Proportional, - Max => Min, +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +enum AppState { + #[default] + Running, + Quit, +} + +fn main() -> Result<()> { + init_error_hooks()?; + let terminal = init_terminal()?; + + // increase the cache size to avoid flickering for indeterminate layouts + Layout::init_cache(100); + + App::default().run(terminal)?; + + restore_terminal()?; + + Ok(()) +} + +impl App { + fn run(&mut self, mut terminal: Terminal) -> Result<()> { + self.update_max_scroll_offset(); + while self.is_running() { + self.draw(&mut terminal)?; + self.handle_events()?; } + Ok(()) } - fn next(&self) -> Self { - use ExampleSelection::*; - match *self { - Fixed => Length, - Length => Percentage, - Percentage => Ratio, - Ratio => Proportional, - Proportional => Min, - Min => Max, - Max => Max, - } + fn update_max_scroll_offset(&mut self) { + self.max_scroll_offset = (self.selected_tab.get_example_count() - 1) * EXAMPLE_HEIGHT; } - fn selected(&self) -> usize { - use ExampleSelection::*; - match self { - Fixed => 0, - Length => 1, - Percentage => 2, - Ratio => 3, - Proportional => 4, - Min => 5, - Max => 6, + fn is_running(&self) -> bool { + self.state == AppState::Running + } + + fn draw(self, terminal: &mut Terminal) -> io::Result<()> { + terminal.draw(|frame| frame.render_widget(self, frame.size()))?; + Ok(()) + } + + fn handle_events(&mut self) -> Result<()> { + if let Event::Key(key) = event::read()? { + use KeyCode::*; + match key.code { + Char('q') | Esc => self.quit(), + Char('l') | Right => self.next(), + Char('h') | Left => self.previous(), + Char('j') | Down => self.down(), + Char('k') | Up => self.up(), + Char('g') | Home => self.top(), + Char('G') | End => self.bottom(), + _ => (), + } } + Ok(()) + } + + fn quit(&mut self) { + self.state = AppState::Quit; + } + + fn next(&mut self) { + self.selected_tab = self.selected_tab.next(); + self.update_max_scroll_offset(); + self.scroll_offset = 0; + } + + fn previous(&mut self) { + self.selected_tab = self.selected_tab.previous(); + self.update_max_scroll_offset(); + self.scroll_offset = 0; + } + + 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 top(&mut self) { + self.scroll_offset = 0; + } + + fn bottom(&mut self) { + self.scroll_offset = self.max_scroll_offset; } } -impl Widget for ExampleSelection { +impl Widget for App { fn render(self, area: Rect, buf: &mut Buffer) { - let [tabs, area] = area.split(&Layout::vertical([Fixed(3), Proportional(0)])); - let [area, _] = area.split(&Layout::horizontal([Fixed(80), Proportional(0)])); + let [tabs, axis, demo] = area.split(&Layout::vertical([ + Constraint::Fixed(3), + Constraint::Fixed(3), + Proportional(0), + ])); self.render_tabs(tabs, buf); + self.render_axis(axis, buf); + self.render_demo(demo, buf); + } +} - match self { - ExampleSelection::Fixed => self.render_fixed_example(area, buf), - ExampleSelection::Length => self.render_length_example(area, buf), - ExampleSelection::Percentage => self.render_percentage_example(area, buf), - ExampleSelection::Ratio => self.render_ratio_example(area, buf), - ExampleSelection::Proportional => self.render_proportional_example(area, buf), - ExampleSelection::Min => self.render_min_example(area, buf), - ExampleSelection::Max => self.render_max_example(area, buf), +impl App { + fn render_tabs(&self, area: Rect, buf: &mut Buffer) { + let titles = SelectedTab::iter().map(SelectedTab::to_tab_title); + let block = Block::new() + .title("Constraints ".bold()) + .title(" Use h l or ◄ ► to change tab and j k or ▲ ▼ to scroll"); + Tabs::new(titles) + .block(block) + .highlight_style(Modifier::REVERSED) + .select(self.selected_tab as usize) + .padding("", "") + .divider(" ") + .render(area, buf); + } + + fn render_axis(&self, area: Rect, buf: &mut Buffer) { + let width = area.width as usize; + // a bar like `<----- 80 px ----->` + let width_label = format!("{} px", width); + let width_bar = format!( + "<{width_label:-^width$}>", + width = width - width_label.len() / 2 + ); + Paragraph::new(width_bar.dark_gray()) + .alignment(Alignment::Center) + .block(Block::default().padding(Padding { + left: 0, + right: 0, + top: 1, + bottom: 0, + })) + .render(area, buf); + } + + /// Render the demo content + /// + /// This function renders the demo content into a separate buffer and then splices the buffer + /// into the main buffer. This is done to make it possible to handle scrolling easily. + fn render_demo(&self, area: Rect, buf: &mut Buffer) { + // render demo content into a separate buffer so all examples fit we add an extra + // area.height to make sure the last example is fully visible even when the scroll offset is + // at the max + let height = self.selected_tab.get_example_count() * EXAMPLE_HEIGHT; + let demo_area = Rect::new(0, 0, area.width, height + area.height); + let mut demo_buf = Buffer::empty(demo_area); + + let scrollbar_needed = self.scroll_offset != 0 || height > area.height; + let content_area = if scrollbar_needed { + Rect { + width: demo_area.width - 1, + ..demo_area + } + } else { + demo_area + }; + self.selected_tab.render(content_area, &mut demo_buf); + + let visible_content = demo_buf + .content + .into_iter() + .skip((demo_area.width * self.scroll_offset) as usize) + .take(area.area() as usize); + for (i, cell) in visible_content.enumerate() { + let x = i as u16 % area.width; + let y = i as u16 / area.width; + *buf.get_mut(area.x + x, area.y + y) = cell; + } + + if scrollbar_needed { + let mut state = ScrollbarState::new(self.max_scroll_offset as usize) + .position(self.scroll_offset as usize); + Scrollbar::new(ScrollbarOrientation::VerticalRight).render(area, buf, &mut state); } } } -impl ExampleSelection { - fn render_tabs(&self, area: Rect, buf: &mut Buffer) { - // ┌Constraints───────────────────────────────────────────────────────────────────┐ - // │ Fixed │ Length │ Percentage │ Ratio │ Proportional │ Min │ Max │ - // └──────────────────────────────────────────────────────────────────────────────┘ - Tabs::new( - [ - ExampleSelection::Fixed, - ExampleSelection::Length, - ExampleSelection::Percentage, - ExampleSelection::Ratio, - ExampleSelection::Proportional, - ExampleSelection::Min, - ExampleSelection::Max, - ] - .iter() - .map(|e| format!("{:?}", e)), - ) - .block(Block::bordered().title("Constraints")) - .highlight_style(Style::default().yellow()) - .select(self.selected()) - .padding(" ", " ") - .render(area, buf); +impl SelectedTab { + /// Get the previous tab, if there is no previous tab return the current tab. + fn previous(&self) -> Self { + let current_index: usize = *self as usize; + let previous_index = current_index.saturating_sub(1); + Self::from_repr(previous_index).unwrap_or(*self) } + /// Get the next tab, if there is no next tab return the current tab. + fn next(&self) -> Self { + let current_index = *self as usize; + let next_index = current_index.saturating_add(1); + Self::from_repr(next_index).unwrap_or(*self) + } + + fn get_example_count(&self) -> u16 { + use SelectedTab::*; + match self { + Fixed => 4, + Length => 4, + Percentage => 5, + Ratio => 4, + Proportional => 2, + Min => 5, + Max => 5, + } + } + + fn to_tab_title(value: SelectedTab) -> Line<'static> { + use SelectedTab::*; + let text = format!(" {value} "); + let color = match value { + Fixed => FIXED_COLOR, + Length => LENGTH_COLOR, + Percentage => PERCENTAGE_COLOR, + Ratio => RATIO_COLOR, + Proportional => PROPORTIONAL_COLOR, + Min => MIN_COLOR, + Max => MAX_COLOR, + }; + text.fg(tailwind::SLATE.c200).bg(color).into() + } +} + +impl Widget for SelectedTab { + fn render(self, area: Rect, buf: &mut Buffer) { + match self { + SelectedTab::Fixed => self.render_fixed_example(area, buf), + SelectedTab::Length => self.render_length_example(area, buf), + SelectedTab::Percentage => self.render_percentage_example(area, buf), + SelectedTab::Ratio => self.render_ratio_example(area, buf), + SelectedTab::Proportional => self.render_proportional_example(area, buf), + SelectedTab::Min => self.render_min_example(area, buf), + SelectedTab::Max => self.render_max_example(area, buf), + } + } +} + +impl SelectedTab { fn render_fixed_example(&self, area: Rect, buf: &mut Buffer) { - let [example1, example2, _] = area.split(&Layout::vertical([Fixed(8); 3])); + let [example1, example2, example3, example4, _] = + area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 5])); - // Fixed(40), Proportional(0) - // - // <---------------------50 px----------------------> - // - // ┌──────────────────────────────────────┐┌────────┐ - // │ 40 px ││ 10 px │ - // └──────────────────────────────────────┘└────────┘ - Example::new([Fixed(40), Proportional(0)]).render(example1, buf); - - // Fixed(20), Fixed(20), Proportional(0) - // - // <---------------------50 px----------------------> - // - // ┌──────────────────┐┌──────────────────┐┌────────┐ - // │ 20 px ││ 20 px ││ 10 px │ - // └──────────────────┘└──────────────────┘└────────┘ - Example::new([Fixed(20), Fixed(20), Proportional(0)]).render(example2, buf); + Example::new(&[Fixed(40), Proportional(0)]).render(example1, buf); + Example::new(&[Fixed(20), Fixed(20), Proportional(0)]).render(example2, buf); + Example::new(&[Fixed(20), Min(20), Max(20)]).render(example3, buf); + Example::new(&[ + Length(20), + Percentage(20), + Ratio(1, 5), + Proportional(1), + Fixed(15), + ]) + .render(example4, buf); } fn render_length_example(&self, area: Rect, buf: &mut Buffer) { let [example1, example2, example3, example4, _] = - area.split(&Layout::vertical([Fixed(8); 5])); + area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 5])); - // Length(20), Fixed(20) - // - // <---------------------50 px----------------------> - // - // ┌────────────────────────────┐┌──────────────────┐ - // │ 30 px ││ 20 px │ - // └────────────────────────────┘└──────────────────┘ - Example::new([Length(20), Fixed(20)]).render(example1, buf); - - // Length(20), Length(20) - // - // <---------------------50 px----------------------> - // - // ┌──────────────────┐┌────────────────────────────┐ - // │ 20 px ││ 30 px │ - // └──────────────────┘└────────────────────────────┘ - Example::new([Length(20), Length(20)]).render(example2, buf); - - // Length(20), Min(20) - // - // <---------------------50 px----------------------> - // - // ┌──────────────────┐┌────────────────────────────┐ - // │ 20 px ││ 30 px │ - // └──────────────────┘└────────────────────────────┘ - Example::new([Length(20), Min(20)]).render(example3, buf); - - // Length(20), Max(20) - // - // <---------------------50 px----------------------> - // - // ┌────────────────────────────┐┌──────────────────┐ - // │ 30 px ││ 20 px │ - // └────────────────────────────┘└──────────────────┘ - Example::new([Length(20), Max(20)]).render(example4, buf); + Example::new(&[Length(20), Fixed(20)]).render(example1, buf); + Example::new(&[Length(20), Length(20)]).render(example2, buf); + Example::new(&[Length(20), Min(20)]).render(example3, buf); + Example::new(&[Length(20), Max(20)]).render(example4, buf); } fn render_percentage_example(&self, area: Rect, buf: &mut Buffer) { let [example1, example2, example3, example4, example5, _] = - area.split(&Layout::vertical([Fixed(8); 6])); + area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 6])); - // Percentage(75), Proportional(0) - // - // <---------------------50 px----------------------> - // - // ┌────────────────────────────────────────────────┐ - // │ 50 px │ - // └────────────────────────────────────────────────┘ - // - Example::new([Percentage(75), Proportional(0)]).render(example1, buf); - - // Percentage(25), Proportional(0) - // - // <---------------------50 px----------------------> - // - // ┌────────────────────────────────────────────────┐ - // │ 50 px │ - // └────────────────────────────────────────────────┘ - Example::new([Percentage(25), Proportional(0)]).render(example2, buf); - - // Percentage(50), Min(20) - // - // <---------------------50 px----------------------> - // - // ┌───────────────────────┐┌───────────────────────┐ - // │ 25 px ││ 25 px │ - // └───────────────────────┘└───────────────────────┘ - Example::new([Percentage(50), Min(20)]).render(example3, buf); - - // Percentage(0), Max(0) - // - // <---------------------50 px----------------------> - // - // ┌────────────────────────────────────────────────┐ - // │ 50 px │ - // └────────────────────────────────────────────────┘ - Example::new([Percentage(0), Max(0)]).render(example4, buf); - - // Percentage(0), Proportional(0) - // - // <---------------------50 px----------------------> - // - // ┌────────────────────────────────────────────────┐ - // │ 50 px │ - // └────────────────────────────────────────────────┘ - Example::new([Percentage(0), Proportional(0)]).render(example5, buf); + Example::new(&[Percentage(75), Proportional(0)]).render(example1, buf); + Example::new(&[Percentage(25), Proportional(0)]).render(example2, buf); + Example::new(&[Percentage(50), Min(20)]).render(example3, buf); + Example::new(&[Percentage(0), Max(0)]).render(example4, buf); + Example::new(&[Percentage(0), Proportional(0)]).render(example5, buf); } fn render_ratio_example(&self, area: Rect, buf: &mut Buffer) { let [example1, example2, example3, example4, _] = - area.split(&Layout::vertical([Fixed(8); 5])); + area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 5])); - // Ratio(1, 2), Ratio(1, 2) - // - // <---------------------50 px----------------------> - // - // ┌───────────────────────┐┌───────────────────────┐ - // │ 25 px ││ 25 px │ - // └───────────────────────┘└───────────────────────┘ - Example::new([Ratio(1, 2); 2]).render(example1, buf); - - // Ratio(1, 4), Ratio(1, 4), Ratio(1, 4), Ratio(1, 4) - // - // <---------------------50 px----------------------> - // - // ┌───────────┐┌──────────┐┌───────────┐┌──────────┐ - // │ 13 px ││ 12 px ││ 13 px ││ 12 px │ - // └───────────┘└──────────┘└───────────┘└──────────┘ - Example::new([Ratio(1, 4); 4]).render(example2, buf); - - // Ratio(1, 2), Ratio(1, 3), Ratio(1, 4) - // - // <---------------------50 px----------------------> - // - // ┌───────────────────────┐┌───────────────┐┌──────┐ - // │ 25 px ││ 17 px ││ 8 px │ - // └───────────────────────┘└───────────────┘└──────┘ - Example::new([Ratio(1, 2), Ratio(1, 3), Ratio(1, 4)]).render(example3, buf); - - // Ratio(1, 2), Percentage(25), Length(10) - // - // <---------------------50 px----------------------> - // - // ┌───────────────────────┐┌───────────┐┌──────────┐ - // │ 25 px ││ 13 px ││ 12 px │ - // └───────────────────────┘└───────────┘└──────────┘ - Example::new([Ratio(1, 2), Percentage(25), Length(10)]).render(example4, buf); + Example::new(&[Ratio(1, 2); 2]).render(example1, buf); + Example::new(&[Ratio(1, 4); 4]).render(example2, buf); + Example::new(&[Ratio(1, 2), Ratio(1, 3), Ratio(1, 4)]).render(example3, buf); + Example::new(&[Ratio(1, 2), Percentage(25), Length(10)]).render(example4, buf); } fn render_proportional_example(&self, area: Rect, buf: &mut Buffer) { - let [example1, example2, _] = area.split(&Layout::vertical([Fixed(8); 3])); + let [example1, example2, _] = area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 3])); - // Proportional(1), Proportional(2), Proportional(3) - // - // <---------------------50 px----------------------> - // - // ┌──────┐┌───────────────┐┌───────────────────────┐ - // │ 8 px ││ 17 px ││ 25 px │ - // └──────┘└───────────────┘└───────────────────────┘ - Example::new([Proportional(1), Proportional(2), Proportional(3)]).render(example1, buf); - - // Proportional(1), Percentage(50), Proportional(1) - // - // <---------------------50 px----------------------> - // - // ┌───────────┐┌───────────────────────┐┌──────────┐ - // │ 13 px ││ 25 px ││ 12 px │ - // └───────────┘└───────────────────────┘└──────────┘ - Example::new([Proportional(1), Percentage(50), Proportional(1)]).render(example2, buf); + Example::new(&[Proportional(1), Proportional(2), Proportional(3)]).render(example1, buf); + Example::new(&[Proportional(1), Percentage(50), Proportional(1)]).render(example2, buf); } fn render_min_example(&self, area: Rect, buf: &mut Buffer) { let [example1, example2, example3, example4, example5, _] = - area.split(&Layout::vertical([Fixed(8); 6])); - // Percentage(100), Min(0) - // - // <---------------------50 px----------------------> - // - // ┌────────────────────────────────────────────────┐ - // │ 50 px │ - // └────────────────────────────────────────────────┘ - Example::new([Percentage(100), Min(0)]).render(example1, buf); + area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 6])); - // Percentage(100), Min(20) - // - // <---------------------50 px----------------------> - // - // ┌────────────────────────────┐┌──────────────────┐ - // │ 30 px ││ 20 px │ - // └────────────────────────────┘└──────────────────┘ - Example::new([Percentage(100), Min(20)]).render(example2, buf); - - // Percentage(100), Min(40) - // - // <---------------------50 px----------------------> - // - // ┌────────┐┌──────────────────────────────────────┐ - // │ 10 px ││ 40 px │ - // └────────┘└──────────────────────────────────────┘ - Example::new([Percentage(100), Min(40)]).render(example3, buf); - - // Percentage(100), Min(60) - // - // <---------------------50 px----------------------> - // - // ┌────────────────────────────────────────────────┐ - // │ 50 px │ - // └────────────────────────────────────────────────┘ - Example::new([Percentage(100), Min(60)]).render(example4, buf); - - // Percentage(100), Min(80) - // - // <---------------------50 px----------------------> - // - // ┌────────────────────────────────────────────────┐ - // │ 50 px │ - // └────────────────────────────────────────────────┘ - Example::new([Percentage(100), Min(80)]).render(example5, buf); + Example::new(&[Percentage(100), Min(0)]).render(example1, buf); + Example::new(&[Percentage(100), Min(20)]).render(example2, buf); + Example::new(&[Percentage(100), Min(40)]).render(example3, buf); + Example::new(&[Percentage(100), Min(60)]).render(example4, buf); + Example::new(&[Percentage(100), Min(80)]).render(example5, buf); } fn render_max_example(&self, area: Rect, buf: &mut Buffer) { let [example1, example2, example3, example4, example5, _] = - area.split(&Layout::vertical([Fixed(8); 6])); + area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 6])); - // Percentage(0), Max(0) - // - // <---------------------50 px----------------------> - // - // ┌────────────────────────────────────────────────┐ - // │ 50 px │ - // └────────────────────────────────────────────────┘ - Example::new([Percentage(0), Max(0)]).render(example1, buf); - - // - // Percentage(0), Max(20) - // - // <---------------------50 px----------------------> - // - // ┌────────────────────────────┐┌──────────────────┐ - // │ 30 px ││ 20 px │ - // └────────────────────────────┘└──────────────────┘ - Example::new([Percentage(0), Max(20)]).render(example2, buf); - - // Percentage(0), Max(40) - // - // <---------------------50 px----------------------> - // - // ┌────────┐┌──────────────────────────────────────┐ - // │ 10 px ││ 40 px │ - // └────────┘└──────────────────────────────────────┘ - Example::new([Percentage(0), Max(40)]).render(example3, buf); - - // Percentage(0), Max(60) - // - // <---------------------50 px----------------------> - // - // ┌────────────────────────────────────────────────┐ - // │ 50 px │ - // └────────────────────────────────────────────────┘ - Example::new([Percentage(0), Max(60)]).render(example4, buf); - - // Percentage(0), Max(80) - // - // <---------------------50 px----------------------> - // - // ┌────────────────────────────────────────────────┐ - // │ 50 px │ - // └────────────────────────────────────────────────┘ - Example::new([Percentage(0), Max(80)]).render(example5, buf); + Example::new(&[Percentage(0), Max(0)]).render(example1, buf); + Example::new(&[Percentage(0), Max(20)]).render(example2, buf); + Example::new(&[Percentage(0), Max(40)]).render(example3, buf); + Example::new(&[Percentage(0), Max(60)]).render(example4, buf); + Example::new(&[Percentage(0), Max(80)]).render(example5, buf); } } @@ -438,10 +377,7 @@ struct Example { } impl Example { - fn new(constraints: C) -> Self - where - C: Into>, - { + fn new(constraints: &[Constraint]) -> Self { Self { constraints: constraints.into(), } @@ -450,53 +386,69 @@ 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 [area, _] = area.split(&Layout::vertical([ + Fixed(ILLUSTRATION_HEIGHT), + Fixed(SPACER_HEIGHT), + ])); let blocks = Layout::horizontal(&self.constraints).split(area); - self.heading().render(title, buf); - - self.legend(legend.width as usize).render(legend, buf); - - for (i, block) in blocks.iter().enumerate() { - let text = format!("{} px", block.width); - let fg = Color::Indexed(i as u8 + 1); - self.illustration(text, fg).render(*block, buf); + for (block, constraint) in blocks.iter().zip(&self.constraints) { + self.illustration(*constraint, block.width) + .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); - let width_bar = format!( - "<{width_label:-^width$}>", - width = width - width_label.len() / 2 - ); - Paragraph::new(width_bar.dark_gray()).alignment(Alignment::Center) - } - - fn illustration(&self, text: String, fg: Color) -> Paragraph { - // Renders the following - // - // ┌─────────┐┌─────────┐ - // │ 40 px ││ 40 px │ - // └─────────┘└─────────┘ + fn illustration(&self, constraint: Constraint, width: u16) -> Paragraph { + let color = match constraint { + Constraint::Fixed(_) => FIXED_COLOR, + Constraint::Length(_) => LENGTH_COLOR, + Constraint::Percentage(_) => PERCENTAGE_COLOR, + Constraint::Ratio(_, _) => RATIO_COLOR, + Constraint::Proportional(_) => PROPORTIONAL_COLOR, + Constraint::Min(_) => MIN_COLOR, + Constraint::Max(_) => MAX_COLOR, + }; + let fg = Color::White; + let title = format!("{constraint}"); + let content = format!("{width} px"); + let text = format!("{title}\n{content}"); + let block = Block::bordered() + .border_set(symbols::border::QUADRANT_OUTSIDE) + .border_style(Style::reset().fg(color).reversed()) + .style(Style::default().fg(fg).bg(color)); Paragraph::new(text) .alignment(Alignment::Center) - .block(Block::bordered().style(Style::default().fg(fg))) + .block(block) } } + +fn init_error_hooks() -> Result<()> { + let (panic, error) = HookBuilder::default().into_hooks(); + let panic = panic.into_panic_hook(); + let error = error.into_eyre_hook(); + color_eyre::eyre::set_hook(Box::new(move |e| { + let _ = restore_terminal(); + error(e) + }))?; + std::panic::set_hook(Box::new(move |info| { + let _ = restore_terminal(); + panic(info) + })); + Ok(()) +} + +fn init_terminal() -> Result> { + enable_raw_mode()?; + stdout().execute(EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout()); + let terminal = Terminal::new(backend)?; + Ok(terminal) +} + +fn restore_terminal() -> Result<()> { + disable_raw_mode()?; + stdout().execute(LeaveAlternateScreen)?; + Ok(()) +} diff --git a/examples/constraints.tape b/examples/constraints.tape index 2a6fe6a7..203af020 100644 --- a/examples/constraints.tape +++ b/examples/constraints.tape @@ -4,7 +4,7 @@ Output "target/constraints.gif" Set Theme "Aardvark Blue" Set FontSize 18 Set Width 1200 -Set Height 1200 +Set Height 700 Hide Type "cargo run --example=constraints --features=crossterm" Enter diff --git a/examples/flex.rs b/examples/flex.rs index 339f5bc4..bb855b9a 100644 --- a/examples/flex.rs +++ b/examples/flex.rs @@ -1,160 +1,91 @@ -use std::{error::Error, io}; +use std::io::{self, stdout}; +use color_eyre::{config::HookBuilder, Result}; use crossterm::{ - event::{self, Event, KeyCode}, - execute, + event::{self, Event, KeyCode, KeyEventKind}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + ExecutableCommand, }; use ratatui::{ layout::{Constraint::*, Flex}, prelude::*, + style::palette::tailwind, widgets::{block::Title, *}, }; +use strum::{Display, EnumIter, FromRepr, IntoEnumIterator}; -const EXAMPLE_HEIGHT: u16 = 5; -const N_EXAMPLES_PER_TAB: u16 = 11; - -fn main() -> Result<(), Box> { - // setup terminal - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - // Each line in the example is a layout - // so EXAMPLE_HEIGHT * N_EXAMPLES_PER_TAB = 55 currently - // Plus additional layout for tabs ... - Layout::init_cache(50); - - // create app and run it - let res = run_app(&mut terminal); - - // restore terminal - disable_raw_mode()?; - execute!(terminal.backend_mut(), LeaveAlternateScreen)?; - terminal.show_cursor()?; - - if let Err(err) = res { - println!("{err:?}"); - } - - Ok(()) -} - -fn run_app(terminal: &mut Terminal) -> io::Result<()> { - // 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(app, f.size()))?; - - if let Event::Key(key) = event::read()? { - use KeyCode::*; - match key.code { - Char('q') => break Ok(()), - Char('l') | Right => app.next(), - Char('h') | Left => app.previous(), - Char('j') | Down => app.down(), - Char('k') | Up => app.up(), - _ => (), - } - } - } -} +const EXAMPLE_DATA: &[(&str, &[Constraint])] = &[ + ( + "Min(u16) takes any excess space when using `Stretch` or `StretchLast`", + &[Fixed(20), Min(20), Max(20)], + ), + ( + "Proportional(u16) takes any excess space in all `Flex` layouts", + &[Length(20), Percentage(20), Ratio(1, 5), Proportional(1)], + ), + ( + "In `StretchLast`, last constraint of lowest priority takes excess space", + &[Length(20), Fixed(20), Percentage(20)], + ), + ("", &[Fixed(20), Percentage(20), Length(20)]), + ("", &[Percentage(20), Length(20), Fixed(20)]), + ("", &[Length(20), Length(15)]), + ("", &[Length(20), Fixed(20)]), + ( + "When not using `Flex::Stretch` or `Flex::StretchLast`,\n`Min(u16)` and `Max(u16)` collapse to their lowest values", + &[Min(20), Max(20)], + ), + ( + "`SpaceBetween` stretches when there's only one constraint", + &[Max(20)], + ), + ("", &[Min(20), Max(20), Length(20), Fixed(20)]), + ("`Proportional(u16)` always fills up space in every `Flex` layout", &[Proportional(0), Proportional(0)]), + ( + "`Proportional(1)` can be to scale with respect to other `Proportional(2)`", + &[Proportional(1), Proportional(2)], + ), + ( + "`Proportional(0)` collapses if there are other non-zero `Proportional(_)`\nconstraints. e.g. `[Proportional(0), Proportional(0), Proportional(1)]`:", + &[ + Proportional(0), + Proportional(0), + Proportional(1), + ], + ), +]; #[derive(Default, Clone, Copy)] struct App { - selected_example: ExampleSelection, + selected_tab: SelectedTab, scroll_offset: u16, - max_scroll_offset: u16, + state: AppState, } -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 { +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +enum AppState { + #[default] + Running, + Quit, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct Example { + constraints: Vec, + description: String, + flex: Flex, +} + +/// Tabs for the different layouts +/// +/// Note: the order of the variants will determine the order of the tabs this uses several derive +/// macros from the `strum` crate to make it easier to iterate over the variants. +/// (`FromRepr`,`Display`,`EnumIter`). +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, FromRepr, Display, EnumIter)] +enum SelectedTab { #[default] - Stretch, StretchLast, + Stretch, Start, Center, End, @@ -162,148 +93,139 @@ enum ExampleSelection { SpaceBetween, } -impl ExampleSelection { - fn previous(&self) -> Self { - use ExampleSelection::*; - match *self { - Stretch => Stretch, - StretchLast => Stretch, - Start => StretchLast, - Center => Start, - End => Center, - SpaceAround => End, - SpaceBetween => SpaceAround, +fn main() -> Result<()> { + init_error_hooks()?; + let terminal = init_terminal()?; + + // Each line in the example is a layout + // so 13 examples * 7 = 91 currently + // Plus additional layout for tabs ... + Layout::init_cache(120); + + App::default().run(terminal)?; + + restore_terminal()?; + Ok(()) +} + +impl App { + fn run(&mut self, mut terminal: Terminal) -> Result<()> { + self.draw(&mut terminal)?; + while self.is_running() { + self.handle_events()?; + self.draw(&mut terminal)?; } + Ok(()) } - fn next(&self) -> Self { - use ExampleSelection::*; - match *self { - Stretch => StretchLast, - StretchLast => Start, - Start => Center, - Center => End, - End => SpaceAround, - SpaceAround => SpaceBetween, - SpaceBetween => SpaceBetween, - } + fn is_running(&self) -> bool { + self.state == AppState::Running } - fn selected(&self) -> usize { - use ExampleSelection::*; - match self { - Stretch => 0, - StretchLast => 1, - Start => 2, - Center => 3, - End => 4, - SpaceAround => 5, - SpaceBetween => 6, + fn draw(self, terminal: &mut Terminal) -> io::Result<()> { + terminal.draw(|frame| frame.render_widget(self, frame.size()))?; + Ok(()) + } + + fn handle_events(&mut self) -> Result<()> { + use KeyCode::*; + match event::read()? { + Event::Key(key) if key.kind == KeyEventKind::Press => match key.code { + Char('q') | Esc => self.quit(), + Char('l') | Right => self.next(), + Char('h') | Left => self.previous(), + Char('j') | Down => self.down(), + Char('k') | Up => self.up(), + Char('g') | Home => self.top(), + Char('G') | End => self.bottom(), + _ => (), + }, + _ => {} } + Ok(()) + } + + fn next(&mut self) { + self.selected_tab = self.selected_tab.next(); + } + + fn previous(&mut self) { + self.selected_tab = self.selected_tab.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(max_scroll_offset()) + } + + fn top(&mut self) { + self.scroll_offset = 0; + } + + fn bottom(&mut self) { + self.scroll_offset = max_scroll_offset(); + } + + fn quit(&mut self) { + self.state = AppState::Quit; } } -impl Widget for ExampleSelection { +// when scrolling, make sure we don't scroll past the last example +fn max_scroll_offset() -> u16 { + example_height() + - EXAMPLE_DATA + .last() + .map(|(desc, _)| get_description_height(desc) + 4) + .unwrap_or(0) +} + +/// The height of all examples combined +/// +/// Each may or may not have a title so we need to account for that. +fn example_height() -> u16 { + EXAMPLE_DATA + .iter() + .map(|(desc, _)| get_description_height(desc) + 4) + .sum() +} + +impl Widget for App { fn render(self, area: Rect, buf: &mut Buffer) { - match self { - ExampleSelection::Stretch => self.render_example(area, buf, Flex::Stretch), - ExampleSelection::StretchLast => self.render_example(area, buf, Flex::StretchLast), - ExampleSelection::Start => self.render_example(area, buf, Flex::Start), - ExampleSelection::Center => self.render_example(area, buf, Flex::Center), - ExampleSelection::End => self.render_example(area, buf, Flex::End), - ExampleSelection::SpaceAround => self.render_example(area, buf, Flex::SpaceAround), - ExampleSelection::SpaceBetween => self.render_example(area, buf, Flex::SpaceBetween), - } + let layout = Layout::vertical([Fixed(3), Fixed(3), Proportional(0)]); + let [tabs, axis, demo] = area.split(&layout); + self.tabs().render(tabs, buf); + self.axis(axis.width).render(axis, buf); + self.render_demo(demo, buf); } } -impl ExampleSelection { - fn render_example(&self, area: Rect, buf: &mut Buffer, flex: Flex) { - let areas = &Layout::vertical([Fixed(EXAMPLE_HEIGHT); N_EXAMPLES_PER_TAB as usize]) - .flex(Flex::Start) - .split(area); - - 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); - } - } -} - -struct Example { - constraints: Vec, - flex: Flex, -} - -impl Example { - fn new(constraints: C) -> Self - where - C: Into>, - { - Self { - constraints: constraints.into(), - flex: Flex::default(), - } +impl App { + fn tabs(&self) -> impl Widget { + let tab_titles = SelectedTab::iter().map(SelectedTab::to_tab_title); + let block = Block::new() + .title(Title::from("Flex Layouts ".bold())) + .title(" Use h l or ◄ ► to change tab and j k or ▲ ▼ to scroll"); + Tabs::new(tab_titles) + .block(block) + .highlight_style(Modifier::REVERSED) + .select(self.selected_tab as usize) + .divider(" ") + .padding("", "") } - fn flex(mut self, flex: Flex) -> Self { - self.flex = flex; - self - } -} - -impl Widget for Example { - fn render(self, area: Rect, buf: &mut Buffer) { - let [legend, area] = area.split(&Layout::vertical([Ratio(1, 3); 2])); - let blocks = Layout::horizontal(&self.constraints) - .flex(self.flex) - .split(area); - - self.legend(legend.width as usize).render(legend, buf); - - for (block, constraint) in blocks.iter().zip(&self.constraints) { - let text = format!("{} px", block.width); - 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 legend(&self, width: usize) -> Paragraph { - // a bar like `<----- 80 px ----->` - let width_label = format!("{} px", width); - let width_bar = format!( - "<{width_label:-^width$}>", - width = width - width_label.len() / 2 - ); + /// a bar like `<----- 80 px ----->` + fn axis(&self, width: u16) -> impl Widget { + let width = width as usize; + let label = format!("{} px", width); + let bar_width = width - label.len() / 2; + let width_bar = format!("<{label:-^bar_width$}>",); Paragraph::new(width_bar.dark_gray()) .alignment(Alignment::Center) .block(Block::default().padding(Padding { @@ -314,13 +236,208 @@ impl Example { })) } - 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)) - .title(block::Title::from(text).alignment(Alignment::Center)), - ) + /// Render the demo content + /// + /// This function renders the demo content into a separate buffer and then splices the buffer + /// into the main buffer. This is done to make it possible to handle scrolling easily. + fn render_demo(self, area: Rect, buf: &mut Buffer) { + // render demo content into a separate buffer so all examples fit we add an extra + // area.height to make sure the last example is fully visible even when the scroll offset is + // at the max + let height = example_height(); + let demo_area = Rect::new(0, 0, area.width, height); + let mut demo_buf = Buffer::empty(demo_area); + + let scrollbar_needed = self.scroll_offset != 0 || height > area.height; + let content_area = if scrollbar_needed { + Rect { + width: demo_area.width - 1, + ..demo_area + } + } else { + demo_area + }; + self.selected_tab.render(content_area, &mut demo_buf); + + let visible_content = demo_buf + .content + .into_iter() + .skip((area.width * self.scroll_offset) as usize) + .take(area.area() as usize); + for (i, cell) in visible_content.enumerate() { + let x = i as u16 % area.width; + let y = i as u16 / area.width; + *buf.get_mut(area.x + x, area.y + y) = cell; + } + + if scrollbar_needed { + let area = area.intersection(buf.area); + let mut state = ScrollbarState::new(max_scroll_offset() as usize) + .position(self.scroll_offset as usize); + Scrollbar::new(ScrollbarOrientation::VerticalRight).render(area, buf, &mut state); + } + } +} + +impl SelectedTab { + /// Get the previous tab, if there is no previous tab return the current tab. + fn previous(&self) -> Self { + let current_index: usize = *self as usize; + let previous_index = current_index.saturating_sub(1); + Self::from_repr(previous_index).unwrap_or(*self) + } + + /// Get the next tab, if there is no next tab return the current tab. + fn next(&self) -> Self { + let current_index = *self as usize; + let next_index = current_index.saturating_add(1); + Self::from_repr(next_index).unwrap_or(*self) + } + + /// Convert a `SelectedTab` into a `Line` to display it by the `Tabs` widget. + fn to_tab_title(value: SelectedTab) -> Line<'static> { + use tailwind::*; + use SelectedTab::*; + let text = value.to_string(); + let color = match value { + StretchLast => ORANGE.c400, + Stretch => ORANGE.c300, + Start => SKY.c400, + Center => SKY.c300, + End => SKY.c200, + SpaceAround => INDIGO.c400, + SpaceBetween => INDIGO.c300, + }; + format!(" {text} ").fg(color).bg(Color::Black).into() + } +} + +impl Widget for SelectedTab { + fn render(self, area: Rect, buf: &mut Buffer) { + match self { + SelectedTab::StretchLast => self.render_examples(area, buf, Flex::StretchLast), + SelectedTab::Stretch => self.render_examples(area, buf, Flex::Stretch), + SelectedTab::Start => self.render_examples(area, buf, Flex::Start), + SelectedTab::Center => self.render_examples(area, buf, Flex::Center), + SelectedTab::End => self.render_examples(area, buf, Flex::End), + SelectedTab::SpaceAround => self.render_examples(area, buf, Flex::SpaceAround), + SelectedTab::SpaceBetween => self.render_examples(area, buf, Flex::SpaceBetween), + } + } +} + +impl SelectedTab { + fn render_examples(&self, area: Rect, buf: &mut Buffer, flex: Flex) { + let heights = EXAMPLE_DATA + .iter() + .map(|(desc, _)| get_description_height(desc) + 4); + let areas = Layout::vertical(heights).flex(Flex::Start).split(area); + for (area, (description, constraints)) in areas.iter().zip(EXAMPLE_DATA.iter()) { + Example::new(constraints, description, flex).render(*area, buf); + } + } +} + +impl Example { + fn new(constraints: &[Constraint], description: &str, flex: Flex) -> Self { + Self { + constraints: constraints.into(), + description: description.into(), + flex, + } + } +} + +impl Widget for Example { + fn render(self, area: Rect, buf: &mut Buffer) { + let title_height = get_description_height(&self.description); + let layout = Layout::vertical([Fixed(title_height), Proportional(0)]); + let [title, illustrations] = area.split(&layout); + let blocks = Layout::horizontal(&self.constraints) + .flex(self.flex) + .split(illustrations); + + if !self.description.is_empty() { + Paragraph::new( + self.description + .split('\n') + .map(|s| format!("// {}", s).italic().fg(tailwind::SLATE.c400)) + .map(Line::from) + .collect::>(), + ) + .render(title, buf); + } + + for (block, constraint) in blocks.iter().zip(&self.constraints) { + self.illustration(*constraint, block.width) + .render(*block, buf); + } + } +} + +impl Example { + fn illustration(&self, constraint: Constraint, width: u16) -> Paragraph { + let main_color = color_for_constraint(constraint); + let fg_color = Color::White; + let title = format!("{constraint}"); + let content = format!("{width} px"); + let text = format!("{title}\n{content}"); + let block = Block::bordered() + .border_set(symbols::border::QUADRANT_OUTSIDE) + .border_style(Style::reset().fg(main_color).reversed()) + .style(Style::default().fg(fg_color).bg(main_color)); + Paragraph::new(text) + .alignment(Alignment::Center) + .block(block) + } +} + +fn color_for_constraint(constraint: Constraint) -> Color { + use tailwind::*; + match constraint { + Constraint::Fixed(_) => RED.c900, + Constraint::Min(_) => BLUE.c900, + Constraint::Max(_) => BLUE.c800, + Constraint::Length(_) => SLATE.c700, + Constraint::Percentage(_) => SLATE.c800, + Constraint::Ratio(_, _) => SLATE.c900, + Constraint::Proportional(_) => SLATE.c950, + } +} + +fn init_error_hooks() -> Result<()> { + let (panic, error) = HookBuilder::default().into_hooks(); + let panic = panic.into_panic_hook(); + let error = error.into_eyre_hook(); + color_eyre::eyre::set_hook(Box::new(move |e| { + let _ = restore_terminal(); + error(e) + }))?; + std::panic::set_hook(Box::new(move |info| { + let _ = restore_terminal(); + panic(info) + })); + Ok(()) +} + +fn init_terminal() -> Result> { + enable_raw_mode()?; + stdout().execute(EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout()); + let terminal = Terminal::new(backend)?; + Ok(terminal) +} + +fn restore_terminal() -> Result<()> { + disable_raw_mode()?; + stdout().execute(LeaveAlternateScreen)?; + Ok(()) +} + +fn get_description_height(s: &str) -> u16 { + if s.is_empty() { + 0 + } else { + s.split('\n').count() as u16 } }