mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-26 06:30:29 +00:00
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:
parent
48b0380cb3
commit
813f707892
3 changed files with 752 additions and 683 deletions
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
667
examples/flex.rs
667
examples/flex.rs
|
@ -1,160 +1,91 @@
|
||||||
use std::{error::Error, io};
|
use std::io::{self, stdout};
|
||||||
|
|
||||||
|
use color_eyre::{config::HookBuilder, Result};
|
||||||
use crossterm::{
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue