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::{
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<dyn Error>> {
// 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<B: Backend>(terminal: &mut Terminal<B>) -> 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<impl Backend>) -> 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<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;
}
}
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<C>(constraints: C) -> Self
where
C: Into<Vec<Constraint>>,
{
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::<Vec<Span>>());
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<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 FontSize 18
Set Width 1200
Set Height 1200
Set Height 700
Hide
Type "cargo run --example=constraints --features=crossterm"
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::{
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<dyn Error>> {
// 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<B: Backend>(terminal: &mut Terminal<B>) -> 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<Constraint>,
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<impl Backend>) -> 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<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;
}
}
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<Constraint>,
flex: Flex,
}
impl Example {
fn new<C>(constraints: C) -> Self
where
C: Into<Vec<Constraint>>,
{
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::<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
}
}