2024-01-24 19:50:18 +00:00
//! # [Ratatui] Flex example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
2024-08-02 06:06:49 +00:00
use std ::{
io ::{ self , stdout } ,
num ::NonZeroUsize ,
} ;
2024-01-13 09:51:41 +00:00
2024-01-16 04:56:40 +00:00
use color_eyre ::{ config ::HookBuilder , Result } ;
2024-01-13 09:51:41 +00:00
use ratatui ::{
2024-05-29 11:42:29 +00:00
backend ::{ Backend , CrosstermBackend } ,
buffer ::Buffer ,
2024-05-28 20:23:39 +00:00
crossterm ::{
event ::{ self , Event , KeyCode , KeyEventKind } ,
terminal ::{ disable_raw_mode , enable_raw_mode , EnterAlternateScreen , LeaveAlternateScreen } ,
ExecutableCommand ,
} ,
2024-05-29 11:42:29 +00:00
layout ::{
Alignment ,
Constraint ::{ self , Fill , Length , Max , Min , Percentage , Ratio } ,
Flex , Layout , Rect ,
} ,
style ::{ palette ::tailwind , Color , Modifier , Style , Stylize } ,
symbols ::{ self , line } ,
text ::{ Line , Text } ,
widgets ::{
block ::Title , Block , Paragraph , Scrollbar , ScrollbarOrientation , ScrollbarState ,
StatefulWidget , Tabs , Widget ,
} ,
2024-08-02 11:18:00 +00:00
Terminal ,
2024-01-13 09:51:41 +00:00
} ;
2024-01-16 04:56:40 +00:00
use strum ::{ Display , EnumIter , FromRepr , IntoEnumIterator } ;
const EXAMPLE_DATA : & [ ( & str , & [ Constraint ] ) ] = & [
(
2024-01-29 14:37:50 +00:00
" Min(u16) takes any excess space always " ,
& [ Length ( 10 ) , Min ( 10 ) , Max ( 10 ) , Percentage ( 10 ) , Ratio ( 1 , 10 ) ] ,
2024-01-16 04:56:40 +00:00
) ,
(
2024-01-28 10:41:01 +00:00
" Fill(u16) takes any excess space always " ,
& [ Length ( 20 ) , Percentage ( 20 ) , Ratio ( 1 , 5 ) , Fill ( 1 ) ] ,
2024-01-16 04:56:40 +00:00
) ,
(
2024-01-27 20:35:42 +00:00
" Here's all constraints in one line " ,
2024-01-29 14:37:50 +00:00
& [ Length ( 10 ) , Min ( 10 ) , Max ( 10 ) , Percentage ( 10 ) , Ratio ( 1 , 10 ) , Fill ( 1 ) ] ,
2024-01-27 20:35:42 +00:00
) ,
(
" " ,
2024-01-29 14:37:50 +00:00
& [ Max ( 50 ) , Min ( 50 ) ] ,
2024-01-27 20:35:42 +00:00
) ,
(
2024-01-29 14:37:50 +00:00
" " ,
& [ Max ( 20 ) , Length ( 10 ) ] ,
) ,
(
" " ,
& [ Max ( 20 ) , Length ( 10 ) ] ,
) ,
(
" Min grows always but also allows Fill to grow " ,
& [ Percentage ( 50 ) , Fill ( 1 ) , Fill ( 2 ) , Min ( 50 ) ] ,
) ,
(
" In `Legacy`, the last constraint of lowest priority takes excess space " ,
& [ Length ( 20 ) , Length ( 20 ) , Percentage ( 20 ) ] ,
2024-01-16 04:56:40 +00:00
) ,
2024-01-29 14:37:50 +00:00
( " " , & [ Length ( 20 ) , Percentage ( 20 ) , Length ( 20 ) ] ) ,
2024-01-27 20:35:42 +00:00
( " A lowest priority constraint will be broken before a high priority constraint " , & [ Ratio ( 1 , 4 ) , Percentage ( 20 ) ] ) ,
( " `Length` is higher priority than `Percentage` " , & [ Percentage ( 20 ) , Length ( 10 ) ] ) ,
( " `Min/Max` is higher priority than `Length` " , & [ Length ( 10 ) , Max ( 20 ) ] ) ,
( " " , & [ Length ( 100 ) , Min ( 20 ) ] ) ,
2024-01-29 14:37:50 +00:00
( " `Length` is higher priority than `Min/Max` " , & [ Max ( 20 ) , Length ( 10 ) ] ) ,
( " " , & [ Min ( 20 ) , Length ( 90 ) ] ) ,
2024-01-28 10:41:01 +00:00
( " Fill is the lowest priority and will fill any excess space " , & [ Fill ( 1 ) , Ratio ( 1 , 4 ) ] ) ,
( " Fill can be used to scale proportionally with other Fill blocks " , & [ Fill ( 1 ) , Percentage ( 20 ) , Fill ( 2 ) ] ) ,
2024-01-27 20:35:42 +00:00
( " " , & [ Ratio ( 1 , 3 ) , Percentage ( 20 ) , Ratio ( 2 , 3 ) ] ) ,
2024-01-29 14:37:50 +00:00
( " Legacy will stretch the last lowest priority constraint \n Stretch will only stretch equal weighted constraints " , & [ Length ( 20 ) , Length ( 15 ) ] ) ,
2024-01-27 20:35:42 +00:00
( " " , & [ Percentage ( 20 ) , Length ( 15 ) ] ) ,
2024-01-29 14:37:50 +00:00
( " `Fill(u16)` fills up excess space, but is lower priority to spacers. \n i.e. Fill will only have widths in Flex::Stretch and Flex::Legacy " , & [ Fill ( 1 ) , Fill ( 1 ) ] ) ,
( " " , & [ Length ( 20 ) , Length ( 20 ) ] ) ,
2024-01-16 04:56:40 +00:00
(
2024-01-29 14:37:50 +00:00
" When not using `Flex::Stretch` or `Flex::Legacy`, \n `Min(u16)` and `Max(u16)` collapse to their lowest values " ,
2024-01-16 04:56:40 +00:00
& [ Min ( 20 ) , Max ( 20 ) ] ,
) ,
(
2024-01-27 20:35:42 +00:00
" " ,
2024-01-16 04:56:40 +00:00
& [ Max ( 20 ) ] ,
) ,
2024-01-29 14:37:50 +00:00
( " " , & [ Min ( 20 ) , Max ( 20 ) , Length ( 20 ) , Length ( 20 ) ] ) ,
2024-01-28 10:41:01 +00:00
( " " , & [ Fill ( 0 ) , Fill ( 0 ) ] ) ,
2024-01-16 04:56:40 +00:00
(
2024-01-28 10:41:01 +00:00
" `Fill(1)` can be to scale with respect to other `Fill(2)` " ,
& [ Fill ( 1 ) , Fill ( 2 ) ] ,
2024-01-16 04:56:40 +00:00
) ,
2024-01-27 20:35:42 +00:00
(
" " ,
2024-01-28 10:41:01 +00:00
& [ Fill ( 1 ) , Min ( 10 ) , Max ( 10 ) , Fill ( 2 ) ] ,
2024-01-27 20:35:42 +00:00
) ,
2024-01-16 04:56:40 +00:00
(
2024-01-28 10:41:01 +00:00
" `Fill(0)` collapses if there are other non-zero `Fill(_)` \n constraints. e.g. `[Fill(0), Fill(0), Fill(1)]`: " ,
2024-01-16 04:56:40 +00:00
& [
2024-01-28 10:41:01 +00:00
Fill ( 0 ) ,
Fill ( 0 ) ,
Fill ( 1 ) ,
2024-01-16 04:56:40 +00:00
] ,
) ,
] ;
2024-01-13 09:51:41 +00:00
2024-01-16 04:56:40 +00:00
#[ derive(Default, Clone, Copy) ]
struct App {
selected_tab : SelectedTab ,
scroll_offset : u16 ,
2024-01-16 16:19:23 +00:00
spacing : u16 ,
2024-01-16 04:56:40 +00:00
state : AppState ,
}
2024-01-14 15:49:45 +00:00
2024-01-16 04:56:40 +00:00
#[ derive(Debug, Default, Clone, Copy, PartialEq, Eq) ]
enum AppState {
#[ default ]
Running ,
Quit ,
}
2024-01-13 09:51:41 +00:00
2024-01-16 04:56:40 +00:00
#[ derive(Debug, Clone, PartialEq, Eq) ]
struct Example {
constraints : Vec < Constraint > ,
description : String ,
flex : Flex ,
2024-01-16 16:19:23 +00:00
spacing : u16 ,
2024-01-16 04:56:40 +00:00
}
/// 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 ]
2024-01-29 14:37:50 +00:00
Legacy ,
2024-01-16 04:56:40 +00:00
Start ,
Center ,
End ,
SpaceAround ,
SpaceBetween ,
}
2024-01-14 17:10:12 +00:00
2024-01-16 04:56:40 +00:00
fn main ( ) -> Result < ( ) > {
2024-01-16 16:19:23 +00:00
// assuming the user changes spacing about a 100 times or so
2024-08-02 06:06:49 +00:00
Layout ::init_cache (
NonZeroUsize ::new ( EXAMPLE_DATA . len ( ) * SelectedTab ::iter ( ) . len ( ) * 100 ) . unwrap ( ) ,
) ;
2024-01-16 04:56:40 +00:00
init_error_hooks ( ) ? ;
let terminal = init_terminal ( ) ? ;
App ::default ( ) . run ( terminal ) ? ;
2024-01-13 09:51:41 +00:00
2024-01-16 04:56:40 +00:00
restore_terminal ( ) ? ;
2024-01-13 09:51:41 +00:00
Ok ( ( ) )
}
2024-01-16 04:56:40 +00:00
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 ) ? ;
2024-01-13 09:51:41 +00:00
}
2024-01-16 04:56:40 +00:00
Ok ( ( ) )
2024-01-13 09:51:41 +00:00
}
2024-03-02 09:06:53 +00:00
fn is_running ( self ) -> bool {
2024-01-16 04:56:40 +00:00
self . state = = AppState ::Running
}
2024-01-14 15:49:45 +00:00
2024-01-16 04:56:40 +00:00
fn draw ( self , terminal : & mut Terminal < impl Backend > ) -> io ::Result < ( ) > {
2024-08-06 03:15:14 +00:00
terminal . draw ( | frame | frame . render_widget ( self , frame . area ( ) ) ) ? ;
2024-01-16 04:56:40 +00:00
Ok ( ( ) )
2024-01-14 15:49:45 +00:00
}
2024-01-16 04:56:40 +00:00
fn handle_events ( & mut self ) -> Result < ( ) > {
match event ::read ( ) ? {
Event ::Key ( key ) if key . kind = = KeyEventKind ::Press = > match key . code {
2024-05-29 11:42:29 +00:00
KeyCode ::Char ( 'q' ) | KeyCode ::Esc = > self . quit ( ) ,
KeyCode ::Char ( 'l' ) | KeyCode ::Right = > self . next ( ) ,
KeyCode ::Char ( 'h' ) | KeyCode ::Left = > self . previous ( ) ,
KeyCode ::Char ( 'j' ) | KeyCode ::Down = > self . down ( ) ,
KeyCode ::Char ( 'k' ) | KeyCode ::Up = > self . up ( ) ,
KeyCode ::Char ( 'g' ) | KeyCode ::Home = > self . top ( ) ,
KeyCode ::Char ( 'G' ) | KeyCode ::End = > self . bottom ( ) ,
KeyCode ::Char ( '+' ) = > self . increment_spacing ( ) ,
KeyCode ::Char ( '-' ) = > self . decrement_spacing ( ) ,
2024-01-16 04:56:40 +00:00
_ = > ( ) ,
} ,
_ = > { }
}
Ok ( ( ) )
}
2024-01-14 15:49:45 +00:00
fn next ( & mut self ) {
2024-01-16 04:56:40 +00:00
self . selected_tab = self . selected_tab . next ( ) ;
2024-01-14 15:49:45 +00:00
}
2024-01-16 04:56:40 +00:00
2024-01-14 15:49:45 +00:00
fn previous ( & mut self ) {
2024-01-16 04:56:40 +00:00
self . selected_tab = self . selected_tab . previous ( ) ;
2024-01-14 15:49:45 +00:00
}
2024-01-16 04:56:40 +00:00
2024-01-14 15:49:45 +00:00
fn up ( & mut self ) {
2024-03-02 09:06:53 +00:00
self . scroll_offset = self . scroll_offset . saturating_sub ( 1 ) ;
2024-01-14 15:49:45 +00:00
}
2024-01-16 04:56:40 +00:00
2024-01-14 15:49:45 +00:00
fn down ( & mut self ) {
self . scroll_offset = self
. scroll_offset
. saturating_add ( 1 )
2024-03-02 09:06:53 +00:00
. min ( max_scroll_offset ( ) ) ;
2024-01-14 15:49:45 +00:00
}
2024-01-16 04:56:40 +00:00
fn top ( & mut self ) {
self . scroll_offset = 0 ;
}
fn bottom ( & mut self ) {
self . scroll_offset = max_scroll_offset ( ) ;
2024-01-14 15:49:45 +00:00
}
2024-01-16 04:56:40 +00:00
2024-01-16 16:19:23 +00:00
fn increment_spacing ( & mut self ) {
self . spacing = self . spacing . saturating_add ( 1 ) ;
}
fn decrement_spacing ( & mut self ) {
self . spacing = self . spacing . saturating_sub ( 1 ) ;
}
2024-01-16 04:56:40 +00:00
fn quit ( & mut self ) {
self . state = AppState ::Quit ;
}
}
// when scrolling, make sure we don't scroll past the last example
fn max_scroll_offset ( ) -> u16 {
example_height ( )
- EXAMPLE_DATA
. last ( )
2024-03-02 09:06:53 +00:00
. map_or ( 0 , | ( desc , _ ) | get_description_height ( desc ) + 4 )
2024-01-16 04:56:40 +00:00
}
/// 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 ( )
2024-01-14 15:49:45 +00:00
}
impl Widget for App {
fn render ( self , area : Rect , buf : & mut Buffer ) {
2024-01-29 14:37:50 +00:00
let layout = Layout ::vertical ( [ Length ( 3 ) , Length ( 1 ) , Fill ( 0 ) ] ) ;
2024-02-02 04:26:35 +00:00
let [ tabs , axis , demo ] = layout . areas ( area ) ;
2024-01-16 04:56:40 +00:00
self . tabs ( ) . render ( tabs , buf ) ;
2024-01-16 16:19:23 +00:00
let scroll_needed = self . render_demo ( demo , buf ) ;
let axis_width = if scroll_needed {
2024-01-27 20:35:42 +00:00
axis . width . saturating_sub ( 1 )
2024-01-16 16:19:23 +00:00
} else {
axis . width
} ;
2024-03-02 09:06:53 +00:00
Self ::axis ( axis_width , self . spacing ) . render ( axis , buf ) ;
2024-01-14 15:49:45 +00:00
}
}
2024-01-16 04:56:40 +00:00
impl App {
2024-03-02 09:06:53 +00:00
fn tabs ( self ) -> impl Widget {
2024-01-16 04:56:40 +00:00
let tab_titles = SelectedTab ::iter ( ) . map ( SelectedTab ::to_tab_title ) ;
let block = Block ::new ( )
. title ( Title ::from ( " Flex Layouts " . bold ( ) ) )
2024-01-16 16:19:23 +00:00
. title ( " Use ◄ ► to change tab, ▲ ▼ to scroll, - + to change spacing " ) ;
2024-01-16 04:56:40 +00:00
Tabs ::new ( tab_titles )
. block ( block )
. highlight_style ( Modifier ::REVERSED )
. select ( self . selected_tab as usize )
. divider ( " " )
. padding ( " " , " " )
}
2024-01-16 16:19:23 +00:00
/// a bar like `<----- 80 px (gap: 2 px)? ----->`
2024-03-02 09:06:53 +00:00
fn axis ( width : u16 , spacing : u16 ) -> impl Widget {
2024-01-16 04:56:40 +00:00
let width = width as usize ;
2024-01-16 16:19:23 +00:00
// only show gap when spacing is not zero
let label = if spacing ! = 0 {
2024-03-02 09:06:53 +00:00
format! ( " {width} px (gap: {spacing} px) " )
2024-01-16 16:19:23 +00:00
} else {
2024-03-02 09:06:53 +00:00
format! ( " {width} px " )
2024-01-16 16:19:23 +00:00
} ;
2024-01-27 20:35:42 +00:00
let bar_width = width . saturating_sub ( 2 ) ; // we want to `<` and `>` at the ends
2024-01-16 16:19:23 +00:00
let width_bar = format! ( " < {label:-^bar_width$} > " ) ;
2024-01-24 11:31:52 +00:00
Paragraph ::new ( width_bar . dark_gray ( ) ) . centered ( )
2024-01-16 04:56:40 +00:00
}
/// 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.
2024-01-16 16:19:23 +00:00
///
/// Returns bool indicating whether scroll was needed
2024-03-02 09:06:53 +00:00
#[ allow(clippy::cast_possible_truncation) ]
2024-01-16 16:19:23 +00:00
fn render_demo ( self , area : Rect , buf : & mut Buffer ) -> bool {
2024-01-16 04:56:40 +00:00
// 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
} ;
2024-01-16 16:19:23 +00:00
let mut spacing = self . spacing ;
self . selected_tab
. render ( content_area , & mut demo_buf , & mut spacing ) ;
2024-01-16 04:56:40 +00:00
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 ) ;
}
2024-01-16 16:19:23 +00:00
scrollbar_needed
2024-01-16 04:56:40 +00:00
}
2024-01-13 09:51:41 +00:00
}
2024-01-16 04:56:40 +00:00
impl SelectedTab {
/// Get the previous tab, if there is no previous tab return the current tab.
2024-03-02 09:06:53 +00:00
fn previous ( self ) -> Self {
let current_index : usize = self as usize ;
2024-01-16 04:56:40 +00:00
let previous_index = current_index . saturating_sub ( 1 ) ;
2024-03-02 09:06:53 +00:00
Self ::from_repr ( previous_index ) . unwrap_or ( self )
2024-01-13 09:51:41 +00:00
}
2024-01-16 04:56:40 +00:00
/// Get the next tab, if there is no next tab return the current tab.
2024-03-02 09:06:53 +00:00
fn next ( self ) -> Self {
let current_index = self as usize ;
2024-01-16 04:56:40 +00:00
let next_index = current_index . saturating_add ( 1 ) ;
2024-03-02 09:06:53 +00:00
Self ::from_repr ( next_index ) . unwrap_or ( self )
2024-01-13 09:51:41 +00:00
}
2024-01-16 04:56:40 +00:00
/// Convert a `SelectedTab` into a `Line` to display it by the `Tabs` widget.
2024-03-02 09:06:53 +00:00
fn to_tab_title ( value : Self ) -> Line < 'static > {
2024-05-29 11:42:29 +00:00
use tailwind ::{ INDIGO , ORANGE , SKY } ;
2024-01-16 04:56:40 +00:00
let text = value . to_string ( ) ;
let color = match value {
2024-03-02 09:06:53 +00:00
Self ::Legacy = > ORANGE . c400 ,
Self ::Start = > SKY . c400 ,
Self ::Center = > SKY . c300 ,
Self ::End = > SKY . c200 ,
Self ::SpaceAround = > INDIGO . c400 ,
Self ::SpaceBetween = > INDIGO . c300 ,
2024-01-16 04:56:40 +00:00
} ;
format! ( " {text} " ) . fg ( color ) . bg ( Color ::Black ) . into ( )
2024-01-13 09:51:41 +00:00
}
}
2024-01-16 16:19:23 +00:00
impl StatefulWidget for SelectedTab {
type State = u16 ;
fn render ( self , area : Rect , buf : & mut Buffer , spacing : & mut Self ::State ) {
let spacing = * spacing ;
2024-01-13 09:51:41 +00:00
match self {
2024-03-02 09:06:53 +00:00
Self ::Legacy = > Self ::render_examples ( area , buf , Flex ::Legacy , spacing ) ,
Self ::Start = > Self ::render_examples ( area , buf , Flex ::Start , spacing ) ,
Self ::Center = > Self ::render_examples ( area , buf , Flex ::Center , spacing ) ,
Self ::End = > Self ::render_examples ( area , buf , Flex ::End , spacing ) ,
Self ::SpaceAround = > Self ::render_examples ( area , buf , Flex ::SpaceAround , spacing ) ,
Self ::SpaceBetween = > Self ::render_examples ( area , buf , Flex ::SpaceBetween , spacing ) ,
2024-01-13 09:51:41 +00:00
}
}
}
2024-01-16 04:56:40 +00:00
impl SelectedTab {
2024-03-02 09:06:53 +00:00
fn render_examples ( area : Rect , buf : & mut Buffer , flex : Flex , spacing : u16 ) {
2024-01-16 04:56:40 +00:00
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 ( ) ) {
2024-01-16 16:19:23 +00:00
Example ::new ( constraints , description , flex , spacing ) . render ( * area , buf ) ;
2024-01-14 15:49:45 +00:00
}
2024-01-13 09:51:41 +00:00
}
}
impl Example {
2024-01-16 16:19:23 +00:00
fn new ( constraints : & [ Constraint ] , description : & str , flex : Flex , spacing : u16 ) -> Self {
2024-01-13 09:51:41 +00:00
Self {
constraints : constraints . into ( ) ,
2024-01-16 04:56:40 +00:00
description : description . into ( ) ,
flex ,
2024-01-16 16:19:23 +00:00
spacing ,
2024-01-13 09:51:41 +00:00
}
}
}
impl Widget for Example {
fn render ( self , area : Rect , buf : & mut Buffer ) {
2024-01-16 04:56:40 +00:00
let title_height = get_description_height ( & self . description ) ;
2024-01-29 14:37:50 +00:00
let layout = Layout ::vertical ( [ Length ( title_height ) , Fill ( 0 ) ] ) ;
2024-02-02 04:26:35 +00:00
let [ title , illustrations ] = layout . areas ( area ) ;
2024-01-27 20:35:42 +00:00
let ( blocks , spacers ) = Layout ::horizontal ( & self . constraints )
2024-01-13 09:51:41 +00:00
. flex ( self . flex )
2024-01-16 16:19:23 +00:00
. spacing ( self . spacing )
2024-01-27 20:35:42 +00:00
. split_with_spacers ( illustrations ) ;
2024-01-16 04:56:40 +00:00
if ! self . description . is_empty ( ) {
Paragraph ::new (
self . description
. split ( '\n' )
2024-03-02 09:06:53 +00:00
. map ( | s | format! ( " // {s} " ) . italic ( ) . fg ( tailwind ::SLATE . c400 ) )
2024-01-16 04:56:40 +00:00
. map ( Line ::from )
. collect ::< Vec < Line > > ( ) ,
)
. render ( title , buf ) ;
}
2024-01-13 09:51:41 +00:00
2024-01-14 15:49:45 +00:00
for ( block , constraint ) in blocks . iter ( ) . zip ( & self . constraints ) {
2024-03-02 09:06:53 +00:00
Self ::illustration ( * constraint , block . width ) . render ( * block , buf ) ;
2024-01-13 09:51:41 +00:00
}
2024-01-27 20:35:42 +00:00
for spacer in spacers . iter ( ) {
2024-03-02 09:06:53 +00:00
Self ::render_spacer ( * spacer , buf ) ;
2024-01-27 20:35:42 +00:00
}
2024-01-13 09:51:41 +00:00
}
}
impl Example {
2024-03-02 09:06:53 +00:00
fn render_spacer ( spacer : Rect , buf : & mut Buffer ) {
2024-01-27 20:35:42 +00:00
if spacer . width > 1 {
let corners_only = symbols ::border ::Set {
top_left : line ::NORMAL . top_left ,
top_right : line ::NORMAL . top_right ,
bottom_left : line ::NORMAL . bottom_left ,
bottom_right : line ::NORMAL . bottom_right ,
vertical_left : " " ,
vertical_right : " " ,
horizontal_top : " " ,
horizontal_bottom : " " ,
} ;
Block ::bordered ( )
. border_set ( corners_only )
. border_style ( Style ::reset ( ) . dark_gray ( ) )
. render ( spacer , buf ) ;
} else {
Paragraph ::new ( Text ::from ( vec! [
Line ::from ( " " ) ,
Line ::from ( " │ " ) ,
Line ::from ( " │ " ) ,
Line ::from ( " " ) ,
] ) )
. style ( Style ::reset ( ) . dark_gray ( ) )
. render ( spacer , buf ) ;
}
let width = spacer . width ;
let label = if width > 4 {
format! ( " {width} px " )
} else if width > 2 {
format! ( " {width} " )
} else {
2024-03-02 09:06:53 +00:00
String ::new ( )
2024-01-27 20:35:42 +00:00
} ;
let text = Text ::from ( vec! [
Line ::raw ( " " ) ,
Line ::raw ( " " ) ,
Line ::styled ( label , Style ::reset ( ) . dark_gray ( ) ) ,
] ) ;
Paragraph ::new ( text )
. style ( Style ::reset ( ) . dark_gray ( ) )
. alignment ( Alignment ::Center )
. render ( spacer , buf ) ;
}
2024-03-02 09:06:53 +00:00
fn illustration ( constraint : Constraint , width : u16 ) -> impl Widget {
2024-01-16 04:56:40 +00:00
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 ) ) ;
2024-01-24 11:31:52 +00:00
Paragraph ::new ( text ) . centered ( ) . block ( block )
2024-01-13 09:51:41 +00:00
}
2024-01-16 04:56:40 +00:00
}
2024-01-13 09:51:41 +00:00
2024-03-02 09:06:53 +00:00
const fn color_for_constraint ( constraint : Constraint ) -> Color {
2024-05-29 11:42:29 +00:00
use tailwind ::{ BLUE , SLATE } ;
2024-01-16 04:56:40 +00:00
match constraint {
Constraint ::Min ( _ ) = > BLUE . c900 ,
Constraint ::Max ( _ ) = > BLUE . c800 ,
Constraint ::Length ( _ ) = > SLATE . c700 ,
Constraint ::Percentage ( _ ) = > SLATE . c800 ,
Constraint ::Ratio ( _ , _ ) = > SLATE . c900 ,
2024-01-28 10:41:01 +00:00
Constraint ::Fill ( _ ) = > SLATE . c950 ,
2024-01-16 04:56:40 +00:00
}
}
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 ( ) ;
2024-03-02 09:06:53 +00:00
panic ( info ) ;
2024-01-16 04:56:40 +00:00
} ) ) ;
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 ( ( ) )
}
2024-03-02 09:06:53 +00:00
#[ allow(clippy::cast_possible_truncation) ]
2024-01-16 04:56:40 +00:00
fn get_description_height ( s : & str ) -> u16 {
if s . is_empty ( ) {
0
} else {
s . split ( '\n' ) . count ( ) as u16
2024-01-13 09:51:41 +00:00
}
}