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 <me@kdheepak.com>
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
This commit is contained in:
Valentin271 2024-01-16 05:56:40 +01:00 committed by GitHub
parent 48b0380cb3
commit 813f707892
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 752 additions and 683 deletions

View file

@ -1,435 +1,374 @@
use std::{error::Error, io}; use std::io::{self, stdout};
use color_eyre::{config::HookBuilder, Result};
use crossterm::{ use crossterm::{
event::{self, Event, KeyCode}, event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
}; };
use itertools::Itertools; use ratatui::{layout::Constraint::*, prelude::*, style::palette::tailwind, widgets::*};
use ratatui::{layout::Constraint::*, prelude::*, widgets::*}; use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
fn main() -> Result<(), Box<dyn Error>> { const SPACER_HEIGHT: u16 = 0;
// setup terminal const ILLUSTRATION_HEIGHT: u16 = 4;
enable_raw_mode()?; const EXAMPLE_HEIGHT: u16 = ILLUSTRATION_HEIGHT + SPACER_HEIGHT;
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 // priority 1
// There is on average 4 row per example const FIXED_COLOR: Color = tailwind::RED.c900;
// 4 row * 7 example = 28 // priority 2
// Plus additional layout for tabs ... const MIN_COLOR: Color = tailwind::BLUE.c900;
// Examples might also grow in a very near future const MAX_COLOR: Color = tailwind::BLUE.c800;
Layout::init_cache(50); // 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 #[derive(Default, Clone, Copy)]
let res = run_app(&mut terminal); struct App {
selected_tab: SelectedTab,
// restore terminal scroll_offset: u16,
disable_raw_mode()?; max_scroll_offset: u16,
execute!(terminal.backend_mut(), LeaveAlternateScreen)?; state: AppState,
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
} }
Ok(()) /// Tabs for the different examples
} ///
/// The order of the variants is the order in which they are displayed.
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> { #[derive(Default, Debug, Copy, Clone, Display, FromRepr, EnumIter, PartialEq, Eq)]
let mut selection = ExampleSelection::Fixed; enum SelectedTab {
loop { #[default]
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 {
Fixed, Fixed,
Min,
Max,
Length, Length,
Percentage, Percentage,
Ratio, Ratio,
Proportional, Proportional,
Min,
Max,
} }
impl ExampleSelection { #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
fn previous(&self) -> Self { enum AppState {
use ExampleSelection::*; #[default]
match *self { Running,
Fixed => Fixed, Quit,
Length => Fixed, }
Percentage => Length,
Ratio => Percentage, fn main() -> Result<()> {
Proportional => Ratio, init_error_hooks()?;
Min => Proportional, let terminal = init_terminal()?;
Max => Min,
// 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<impl Backend>) -> Result<()> {
self.update_max_scroll_offset();
while self.is_running() {
self.draw(&mut terminal)?;
self.handle_events()?;
}
Ok(())
}
fn update_max_scroll_offset(&mut self) {
self.max_scroll_offset = (self.selected_tab.get_example_count() - 1) * EXAMPLE_HEIGHT;
}
fn is_running(&self) -> bool {
self.state == AppState::Running
}
fn draw(self, terminal: &mut Terminal<impl Backend>) -> 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;
} }
} }
fn next(&self) -> Self { impl Widget for App {
use ExampleSelection::*;
match *self {
Fixed => Length,
Length => Percentage,
Percentage => Ratio,
Ratio => Proportional,
Proportional => Min,
Min => Max,
Max => Max,
}
}
fn selected(&self) -> usize {
use ExampleSelection::*;
match self {
Fixed => 0,
Length => 1,
Percentage => 2,
Ratio => 3,
Proportional => 4,
Min => 5,
Max => 6,
}
}
}
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)])); let [tabs, axis, demo] = area.split(&Layout::vertical([
let [area, _] = area.split(&Layout::horizontal([Fixed(80), Proportional(0)])); Constraint::Fixed(3),
Constraint::Fixed(3),
Proportional(0),
]));
self.render_tabs(tabs, buf); self.render_tabs(tabs, buf);
self.render_axis(axis, buf);
match self { self.render_demo(demo, buf);
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 ExampleSelection { impl App {
fn render_tabs(&self, area: Rect, buf: &mut Buffer) { fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
// ┌Constraints───────────────────────────────────────────────────────────────────┐ let titles = SelectedTab::iter().map(SelectedTab::to_tab_title);
// │ Fixed │ Length │ Percentage │ Ratio │ Proportional │ Min │ Max │ let block = Block::new()
// └──────────────────────────────────────────────────────────────────────────────┘ .title("Constraints ".bold())
Tabs::new( .title(" Use h l or ◄ ► to change tab and j k or ▲ ▼ to scroll");
[ Tabs::new(titles)
ExampleSelection::Fixed, .block(block)
ExampleSelection::Length, .highlight_style(Modifier::REVERSED)
ExampleSelection::Percentage, .select(self.selected_tab as usize)
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("", "") .padding("", "")
.divider(" ")
.render(area, buf); .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 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) { 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) Example::new(&[Fixed(40), Proportional(0)]).render(example1, buf);
// Example::new(&[Fixed(20), Fixed(20), Proportional(0)]).render(example2, buf);
// <---------------------50 px----------------------> Example::new(&[Fixed(20), Min(20), Max(20)]).render(example3, buf);
// Example::new(&[
// ┌──────────────────────────────────────┐┌────────┐ Length(20),
// │ 40 px ││ 10 px │ Percentage(20),
// └──────────────────────────────────────┘└────────┘ Ratio(1, 5),
Example::new([Fixed(40), Proportional(0)]).render(example1, buf); Proportional(1),
Fixed(15),
// Fixed(20), Fixed(20), Proportional(0) ])
// .render(example4, buf);
// <---------------------50 px---------------------->
//
// ┌──────────────────┐┌──────────────────┐┌────────┐
// │ 20 px ││ 20 px ││ 10 px │
// └──────────────────┘└──────────────────┘└────────┘
Example::new([Fixed(20), Fixed(20), Proportional(0)]).render(example2, buf);
} }
fn render_length_example(&self, area: Rect, buf: &mut Buffer) { fn render_length_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, _] = let [example1, example2, example3, example4, _] =
area.split(&Layout::vertical([Fixed(8); 5])); area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 5]));
// Length(20), Fixed(20) Example::new(&[Length(20), Fixed(20)]).render(example1, buf);
// Example::new(&[Length(20), Length(20)]).render(example2, buf);
// <---------------------50 px----------------------> Example::new(&[Length(20), Min(20)]).render(example3, buf);
// Example::new(&[Length(20), Max(20)]).render(example4, buf);
// ┌────────────────────────────┐┌──────────────────┐
// │ 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);
} }
fn render_percentage_example(&self, area: Rect, buf: &mut Buffer) { fn render_percentage_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, example5, _] = 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) Example::new(&[Percentage(75), Proportional(0)]).render(example1, buf);
// Example::new(&[Percentage(25), Proportional(0)]).render(example2, buf);
// <---------------------50 px----------------------> 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);
// │ 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);
} }
fn render_ratio_example(&self, area: Rect, buf: &mut Buffer) { fn render_ratio_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, _] = 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) Example::new(&[Ratio(1, 2); 2]).render(example1, buf);
// Example::new(&[Ratio(1, 4); 4]).render(example2, buf);
// <---------------------50 px----------------------> 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);
// ┌───────────────────────┐┌───────────────────────┐
// │ 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);
} }
fn render_proportional_example(&self, area: Rect, buf: &mut Buffer) { 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) Example::new(&[Proportional(1), Proportional(2), Proportional(3)]).render(example1, buf);
// Example::new(&[Proportional(1), Percentage(50), Proportional(1)]).render(example2, buf);
// <---------------------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);
} }
fn render_min_example(&self, area: Rect, buf: &mut Buffer) { fn render_min_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, example5, _] = let [example1, example2, example3, example4, example5, _] =
area.split(&Layout::vertical([Fixed(8); 6])); area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 6]));
// Percentage(100), Min(0)
//
// <---------------------50 px---------------------->
//
// ┌────────────────────────────────────────────────┐
// │ 50 px │
// └────────────────────────────────────────────────┘
Example::new([Percentage(100), Min(0)]).render(example1, buf);
// Percentage(100), Min(20) Example::new(&[Percentage(100), Min(0)]).render(example1, buf);
// Example::new(&[Percentage(100), Min(20)]).render(example2, buf);
// <---------------------50 px----------------------> 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);
// │ 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);
} }
fn render_max_example(&self, area: Rect, buf: &mut Buffer) { fn render_max_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, example5, _] = 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) Example::new(&[Percentage(0), Max(0)]).render(example1, buf);
// Example::new(&[Percentage(0), Max(20)]).render(example2, buf);
// <---------------------50 px----------------------> 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);
// │ 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);
} }
} }
@ -438,10 +377,7 @@ struct Example {
} }
impl Example { impl Example {
fn new<C>(constraints: C) -> Self fn new(constraints: &[Constraint]) -> Self {
where
C: Into<Vec<Constraint>>,
{
Self { Self {
constraints: constraints.into(), constraints: constraints.into(),
} }
@ -450,53 +386,69 @@ 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 [area, _] = area.split(&Layout::vertical([
Fixed(ILLUSTRATION_HEIGHT),
Fixed(SPACER_HEIGHT),
]));
let blocks = Layout::horizontal(&self.constraints).split(area); let blocks = Layout::horizontal(&self.constraints).split(area);
self.heading().render(title, buf); for (block, constraint) in blocks.iter().zip(&self.constraints) {
self.illustration(*constraint, block.width)
self.legend(legend.width as usize).render(legend, buf); .render(*block, 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);
} }
} }
} }
impl Example { impl Example {
fn heading(&self) -> Paragraph { fn illustration(&self, constraint: Constraint, width: u16) -> Paragraph {
// Renders the following let color = match constraint {
// Constraint::Fixed(_) => FIXED_COLOR,
// Fixed(40), Proportional(0) Constraint::Length(_) => LENGTH_COLOR,
let spans = self.constraints.iter().enumerate().map(|(i, c)| { Constraint::Percentage(_) => PERCENTAGE_COLOR,
let color = Color::Indexed(i as u8 + 1); Constraint::Ratio(_, _) => RATIO_COLOR,
Span::styled(format!("{:?}", c), color) Constraint::Proportional(_) => PROPORTIONAL_COLOR,
}); Constraint::Min(_) => MIN_COLOR,
let heading = Constraint::Max(_) => MAX_COLOR,
Line::from(Itertools::intersperse(spans, Span::raw(", ")).collect::<Vec<Span>>()); };
Paragraph::new(heading).block(Block::default().padding(Padding::vertical(1))) let fg = Color::White;
} let title = format!("{constraint}");
let content = format!("{width} px");
fn legend(&self, width: usize) -> Paragraph { let text = format!("{title}\n{content}");
// a bar like `<----- 80 px ----->` let block = Block::bordered()
let width_label = format!("{} px", width); .border_set(symbols::border::QUADRANT_OUTSIDE)
let width_bar = format!( .border_style(Style::reset().fg(color).reversed())
"<{width_label:-^width$}>", .style(Style::default().fg(fg).bg(color));
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 │
// └─────────┘└─────────┘
Paragraph::new(text) Paragraph::new(text)
.alignment(Alignment::Center) .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<Terminal<impl Backend>> {
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(())
}

View file

@ -4,7 +4,7 @@ Output "target/constraints.gif"
Set Theme "Aardvark Blue" Set Theme "Aardvark Blue"
Set FontSize 18 Set FontSize 18
Set Width 1200 Set Width 1200
Set Height 1200 Set Height 700
Hide Hide
Type "cargo run --example=constraints --features=crossterm" Type "cargo run --example=constraints --features=crossterm"
Enter Enter

View file

@ -1,160 +1,91 @@
use std::{error::Error, io}; use std::io::{self, stdout};
use color_eyre::{config::HookBuilder, Result};
use crossterm::{ use crossterm::{
event::{self, Event, KeyCode}, event::{self, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
}; };
use ratatui::{ use ratatui::{
layout::{Constraint::*, Flex}, layout::{Constraint::*, Flex},
prelude::*, prelude::*,
style::palette::tailwind,
widgets::{block::Title, *}, widgets::{block::Title, *},
}; };
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
const EXAMPLE_HEIGHT: u16 = 5; const EXAMPLE_DATA: &[(&str, &[Constraint])] = &[
const N_EXAMPLES_PER_TAB: u16 = 11; (
"Min(u16) takes any excess space when using `Stretch` or `StretchLast`",
fn main() -> Result<(), Box<dyn Error>> { &[Fixed(20), Min(20), Max(20)],
// setup terminal ),
enable_raw_mode()?; (
let mut stdout = io::stdout(); "Proportional(u16) takes any excess space in all `Flex` layouts",
execute!(stdout, EnterAlternateScreen)?; &[Length(20), Percentage(20), Ratio(1, 5), Proportional(1)],
let backend = CrosstermBackend::new(stdout); ),
let mut terminal = Terminal::new(backend)?; (
"In `StretchLast`, last constraint of lowest priority takes excess space",
// Each line in the example is a layout &[Length(20), Fixed(20), Percentage(20)],
// so EXAMPLE_HEIGHT * N_EXAMPLES_PER_TAB = 55 currently ),
// Plus additional layout for tabs ... ("", &[Fixed(20), Percentage(20), Length(20)]),
Layout::init_cache(50); ("", &[Percentage(20), Length(20), Fixed(20)]),
("", &[Length(20), Length(15)]),
// create app and run it ("", &[Length(20), Fixed(20)]),
let res = run_app(&mut terminal); (
"When not using `Flex::Stretch` or `Flex::StretchLast`,\n`Min(u16)` and `Max(u16)` collapse to their lowest values",
// restore terminal &[Min(20), Max(20)],
disable_raw_mode()?; ),
execute!(terminal.backend_mut(), LeaveAlternateScreen)?; (
terminal.show_cursor()?; "`SpaceBetween` stretches when there's only one constraint",
&[Max(20)],
if let Err(err) = res { ),
println!("{err:?}"); ("", &[Min(20), Max(20), Length(20), Fixed(20)]),
} ("`Proportional(u16)` always fills up space in every `Flex` layout", &[Proportional(0), Proportional(0)]),
(
Ok(()) "`Proportional(1)` can be to scale with respect to other `Proportional(2)`",
} &[Proportional(1), Proportional(2)],
),
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> { (
// we always want to show the last example when scrolling "`Proportional(0)` collapses if there are other non-zero `Proportional(_)`\nconstraints. e.g. `[Proportional(0), Proportional(0), Proportional(1)]`:",
let mut app = App::default().max_scroll_offset((N_EXAMPLES_PER_TAB - 1) * EXAMPLE_HEIGHT); &[
Proportional(0),
loop { Proportional(0),
terminal.draw(|f| f.render_widget(app, f.size()))?; Proportional(1),
],
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(),
_ => (),
}
}
}
}
#[derive(Default, Clone, Copy)] #[derive(Default, Clone, Copy)]
struct App { struct App {
selected_example: ExampleSelection, selected_tab: SelectedTab,
scroll_offset: u16, scroll_offset: u16,
max_scroll_offset: u16, state: AppState,
} }
impl App { #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
fn max_scroll_offset(mut self, max_scroll_offset: u16) -> Self { enum AppState {
self.max_scroll_offset = max_scroll_offset; #[default]
self Running,
} Quit,
fn next(&mut self) { }
self.selected_example = self.selected_example.next();
} #[derive(Debug, Clone, PartialEq, Eq)]
fn previous(&mut self) { struct Example {
self.selected_example = self.selected_example.previous(); constraints: Vec<Constraint>,
} description: String,
fn up(&mut self) { flex: Flex,
self.scroll_offset = self.scroll_offset.saturating_sub(1) }
}
fn down(&mut self) { /// Tabs for the different layouts
self.scroll_offset = self ///
.scroll_offset /// Note: the order of the variants will determine the order of the tabs this uses several derive
.saturating_add(1) /// macros from the `strum` crate to make it easier to iterate over the variants.
.min(self.max_scroll_offset) /// (`FromRepr`,`Display`,`EnumIter`).
} #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, FromRepr, Display, EnumIter)]
enum SelectedTab {
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] #[default]
Stretch,
StretchLast, StretchLast,
Stretch,
Start, Start,
Center, Center,
End, End,
@ -162,148 +93,139 @@ enum ExampleSelection {
SpaceBetween, SpaceBetween,
} }
impl ExampleSelection { fn main() -> Result<()> {
fn previous(&self) -> Self { init_error_hooks()?;
use ExampleSelection::*; let terminal = init_terminal()?;
match *self {
Stretch => Stretch, // Each line in the example is a layout
StretchLast => Stretch, // so 13 examples * 7 = 91 currently
Start => StretchLast, // Plus additional layout for tabs ...
Center => Start, Layout::init_cache(120);
End => Center,
SpaceAround => End, App::default().run(terminal)?;
SpaceBetween => SpaceAround,
restore_terminal()?;
Ok(())
}
impl App {
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
self.draw(&mut terminal)?;
while self.is_running() {
self.handle_events()?;
self.draw(&mut terminal)?;
}
Ok(())
}
fn is_running(&self) -> bool {
self.state == AppState::Running
}
fn draw(self, terminal: &mut Terminal<impl Backend>) -> 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;
} }
} }
fn next(&self) -> Self { // when scrolling, make sure we don't scroll past the last example
use ExampleSelection::*; fn max_scroll_offset() -> u16 {
match *self { example_height()
Stretch => StretchLast, - EXAMPLE_DATA
StretchLast => Start, .last()
Start => Center, .map(|(desc, _)| get_description_height(desc) + 4)
Center => End, .unwrap_or(0)
End => SpaceAround,
SpaceAround => SpaceBetween,
SpaceBetween => SpaceBetween,
}
} }
fn selected(&self) -> usize { /// The height of all examples combined
use ExampleSelection::*; ///
match self { /// Each may or may not have a title so we need to account for that.
Stretch => 0, fn example_height() -> u16 {
StretchLast => 1, EXAMPLE_DATA
Start => 2, .iter()
Center => 3, .map(|(desc, _)| get_description_height(desc) + 4)
End => 4, .sum()
SpaceAround => 5,
SpaceBetween => 6,
}
}
} }
impl Widget for ExampleSelection { impl Widget for App {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
match self { let layout = Layout::vertical([Fixed(3), Fixed(3), Proportional(0)]);
ExampleSelection::Stretch => self.render_example(area, buf, Flex::Stretch), let [tabs, axis, demo] = area.split(&layout);
ExampleSelection::StretchLast => self.render_example(area, buf, Flex::StretchLast), self.tabs().render(tabs, buf);
ExampleSelection::Start => self.render_example(area, buf, Flex::Start), self.axis(axis.width).render(axis, buf);
ExampleSelection::Center => self.render_example(area, buf, Flex::Center), self.render_demo(demo, buf);
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),
}
}
}
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<Constraint>,
flex: Flex,
}
impl Example {
fn new<C>(constraints: C) -> Self
where
C: Into<Vec<Constraint>>,
{
Self {
constraints: constraints.into(),
flex: Flex::default(),
} }
} }
fn flex(mut self, flex: Flex) -> Self { impl App {
self.flex = flex; fn tabs(&self) -> impl Widget {
self 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("", "")
} }
impl Widget for Example { /// a bar like `<----- 80 px ----->`
fn render(self, area: Rect, buf: &mut Buffer) { fn axis(&self, width: u16) -> impl Widget {
let [legend, area] = area.split(&Layout::vertical([Ratio(1, 3); 2])); let width = width as usize;
let blocks = Layout::horizontal(&self.constraints) let label = format!("{} px", width);
.flex(self.flex) let bar_width = width - label.len() / 2;
.split(area); let width_bar = format!("<{label:-^bar_width$}>",);
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
);
Paragraph::new(width_bar.dark_gray()) Paragraph::new(width_bar.dark_gray())
.alignment(Alignment::Center) .alignment(Alignment::Center)
.block(Block::default().padding(Padding { .block(Block::default().padding(Padding {
@ -314,13 +236,208 @@ impl Example {
})) }))
} }
fn illustration(&self, constraint: Constraint, text: String, fg: Color) -> Paragraph { /// Render the demo content
Paragraph::new(format!("{:?}", constraint)) ///
.alignment(Alignment::Center) /// This function renders the demo content into a separate buffer and then splices the buffer
.block( /// into the main buffer. This is done to make it possible to handle scrolling easily.
Block::bordered() fn render_demo(self, area: Rect, buf: &mut Buffer) {
.style(Style::default().fg(fg)) // render demo content into a separate buffer so all examples fit we add an extra
.title(block::Title::from(text).alignment(Alignment::Center)), // 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::<Vec<Line>>(),
) )
.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<Terminal<impl Backend>> {
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
} }
} }