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.
//!
2024-08-21 18:35:08 +00:00
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
2024-01-24 19:50:18 +00:00
feat(terminal): Add ratatui::init() and restore() methods (#1289)
These are simple opinionated methods for creating a terminal that is
useful to use in most apps. The new init method creates a crossterm
backend writing to stdout, enables raw mode, enters the alternate
screen, and sets a panic handler that restores the terminal on panic.
A minimal hello world now looks a bit like:
```rust
use ratatui::{
crossterm::event::{self, Event},
text::Text,
Frame,
};
fn main() {
let mut terminal = ratatui::init();
loop {
terminal
.draw(|frame: &mut Frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))
.expect("Failed to draw");
if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
break;
}
}
ratatui::restore();
}
```
A type alias `DefaultTerminal` is added to represent this terminal
type and to simplify any cases where applications need to pass this
terminal around. It is equivalent to:
`Terminal<CrosstermBackend<Stdout>>`
We also added `ratatui::try_init()` and `try_restore()`, for situations
where you might want to handle initialization errors yourself instead
of letting the panic handler fire and cleanup. Simple Apps should
prefer the `init` and `restore` functions over these functions.
Corresponding functions to allow passing a `TerminalOptions` with
a `Viewport` (e.g. inline, fixed) are also available
(`init_with_options`,
and `try_init_with_options`).
The existing code to create a backend and terminal will remain and
is not deprecated by this approach. This just provides a simple one
line initialization using the common options.
---------
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-08-22 12:16:35 +00:00
use std ::num ::NonZeroUsize ;
2024-01-13 09:51:41 +00:00
feat(terminal): Add ratatui::init() and restore() methods (#1289)
These are simple opinionated methods for creating a terminal that is
useful to use in most apps. The new init method creates a crossterm
backend writing to stdout, enables raw mode, enters the alternate
screen, and sets a panic handler that restores the terminal on panic.
A minimal hello world now looks a bit like:
```rust
use ratatui::{
crossterm::event::{self, Event},
text::Text,
Frame,
};
fn main() {
let mut terminal = ratatui::init();
loop {
terminal
.draw(|frame: &mut Frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))
.expect("Failed to draw");
if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
break;
}
}
ratatui::restore();
}
```
A type alias `DefaultTerminal` is added to represent this terminal
type and to simplify any cases where applications need to pass this
terminal around. It is equivalent to:
`Terminal<CrosstermBackend<Stdout>>`
We also added `ratatui::try_init()` and `try_restore()`, for situations
where you might want to handle initialization errors yourself instead
of letting the panic handler fire and cleanup. Simple Apps should
prefer the `init` and `restore` functions over these functions.
Corresponding functions to allow passing a `TerminalOptions` with
a `Viewport` (e.g. inline, fixed) are also available
(`init_with_options`,
and `try_init_with_options`).
The existing code to create a backend and terminal will remain and
is not deprecated by this approach. This just provides a simple one
line initialization using the common options.
---------
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-08-22 12:16:35 +00:00
use color_eyre ::Result ;
2024-01-13 09:51:41 +00:00
use ratatui ::{
2024-05-29 11:42:29 +00:00
buffer ::Buffer ,
feat(terminal): Add ratatui::init() and restore() methods (#1289)
These are simple opinionated methods for creating a terminal that is
useful to use in most apps. The new init method creates a crossterm
backend writing to stdout, enables raw mode, enters the alternate
screen, and sets a panic handler that restores the terminal on panic.
A minimal hello world now looks a bit like:
```rust
use ratatui::{
crossterm::event::{self, Event},
text::Text,
Frame,
};
fn main() {
let mut terminal = ratatui::init();
loop {
terminal
.draw(|frame: &mut Frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))
.expect("Failed to draw");
if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
break;
}
}
ratatui::restore();
}
```
A type alias `DefaultTerminal` is added to represent this terminal
type and to simplify any cases where applications need to pass this
terminal around. It is equivalent to:
`Terminal<CrosstermBackend<Stdout>>`
We also added `ratatui::try_init()` and `try_restore()`, for situations
where you might want to handle initialization errors yourself instead
of letting the panic handler fire and cleanup. Simple Apps should
prefer the `init` and `restore` functions over these functions.
Corresponding functions to allow passing a `TerminalOptions` with
a `Viewport` (e.g. inline, fixed) are also available
(`init_with_options`,
and `try_init_with_options`).
The existing code to create a backend and terminal will remain and
is not deprecated by this approach. This just provides a simple one
line initialization using the common options.
---------
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-08-22 12:16:35 +00:00
crossterm ::event ::{ self , Event , KeyCode , KeyEventKind } ,
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 ::{
2024-09-20 07:21:26 +00:00
Block , Paragraph , Scrollbar , ScrollbarOrientation , ScrollbarState , StatefulWidget , Tabs ,
Widget ,
2024-05-29 11:42:29 +00:00
} ,
feat(terminal): Add ratatui::init() and restore() methods (#1289)
These are simple opinionated methods for creating a terminal that is
useful to use in most apps. The new init method creates a crossterm
backend writing to stdout, enables raw mode, enters the alternate
screen, and sets a panic handler that restores the terminal on panic.
A minimal hello world now looks a bit like:
```rust
use ratatui::{
crossterm::event::{self, Event},
text::Text,
Frame,
};
fn main() {
let mut terminal = ratatui::init();
loop {
terminal
.draw(|frame: &mut Frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))
.expect("Failed to draw");
if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
break;
}
}
ratatui::restore();
}
```
A type alias `DefaultTerminal` is added to represent this terminal
type and to simplify any cases where applications need to pass this
terminal around. It is equivalent to:
`Terminal<CrosstermBackend<Stdout>>`
We also added `ratatui::try_init()` and `try_restore()`, for situations
where you might want to handle initialization errors yourself instead
of letting the panic handler fire and cleanup. Simple Apps should
prefer the `init` and `restore` functions over these functions.
Corresponding functions to allow passing a `TerminalOptions` with
a `Viewport` (e.g. inline, fixed) are also available
(`init_with_options`,
and `try_init_with_options`).
The existing code to create a backend and terminal will remain and
is not deprecated by this approach. This just provides a simple one
line initialization using the common options.
---------
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-08-22 12:16:35 +00:00
DefaultTerminal ,
2024-01-13 09:51:41 +00:00
} ;
2024-01-16 04:56:40 +00:00
use strum ::{ Display , EnumIter , FromRepr , IntoEnumIterator } ;
feat(terminal): Add ratatui::init() and restore() methods (#1289)
These are simple opinionated methods for creating a terminal that is
useful to use in most apps. The new init method creates a crossterm
backend writing to stdout, enables raw mode, enters the alternate
screen, and sets a panic handler that restores the terminal on panic.
A minimal hello world now looks a bit like:
```rust
use ratatui::{
crossterm::event::{self, Event},
text::Text,
Frame,
};
fn main() {
let mut terminal = ratatui::init();
loop {
terminal
.draw(|frame: &mut Frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))
.expect("Failed to draw");
if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
break;
}
}
ratatui::restore();
}
```
A type alias `DefaultTerminal` is added to represent this terminal
type and to simplify any cases where applications need to pass this
terminal around. It is equivalent to:
`Terminal<CrosstermBackend<Stdout>>`
We also added `ratatui::try_init()` and `try_restore()`, for situations
where you might want to handle initialization errors yourself instead
of letting the panic handler fire and cleanup. Simple Apps should
prefer the `init` and `restore` functions over these functions.
Corresponding functions to allow passing a `TerminalOptions` with
a `Viewport` (e.g. inline, fixed) are also available
(`init_with_options`,
and `try_init_with_options`).
The existing code to create a backend and terminal will remain and
is not deprecated by this approach. This just provides a simple one
line initialization using the common options.
---------
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-08-22 12:16:35 +00:00
fn main ( ) -> Result < ( ) > {
color_eyre ::install ( ) ? ;
let terminal = ratatui ::init ( ) ;
let app_result = App ::default ( ) . run ( terminal ) ;
ratatui ::restore ( ) ;
app_result
}
2024-01-16 04:56:40 +00:00
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
impl App {
feat(terminal): Add ratatui::init() and restore() methods (#1289)
These are simple opinionated methods for creating a terminal that is
useful to use in most apps. The new init method creates a crossterm
backend writing to stdout, enables raw mode, enters the alternate
screen, and sets a panic handler that restores the terminal on panic.
A minimal hello world now looks a bit like:
```rust
use ratatui::{
crossterm::event::{self, Event},
text::Text,
Frame,
};
fn main() {
let mut terminal = ratatui::init();
loop {
terminal
.draw(|frame: &mut Frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))
.expect("Failed to draw");
if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
break;
}
}
ratatui::restore();
}
```
A type alias `DefaultTerminal` is added to represent this terminal
type and to simplify any cases where applications need to pass this
terminal around. It is equivalent to:
`Terminal<CrosstermBackend<Stdout>>`
We also added `ratatui::try_init()` and `try_restore()`, for situations
where you might want to handle initialization errors yourself instead
of letting the panic handler fire and cleanup. Simple Apps should
prefer the `init` and `restore` functions over these functions.
Corresponding functions to allow passing a `TerminalOptions` with
a `Viewport` (e.g. inline, fixed) are also available
(`init_with_options`,
and `try_init_with_options`).
The existing code to create a backend and terminal will remain and
is not deprecated by this approach. This just provides a simple one
line initialization using the common options.
---------
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-08-22 12:16:35 +00:00
fn run ( mut self , mut terminal : DefaultTerminal ) -> Result < ( ) > {
// increase the layout cache to account for the number of layout events. This ensures that
// layout is not generally reprocessed on every frame (which would lead to possible janky
// results when there are more than one possible solution to the requested layout). This
// assumes the user changes spacing about a 100 times or so.
let cache_size = EXAMPLE_DATA . len ( ) * SelectedTab ::iter ( ) . len ( ) * 100 ;
Layout ::init_cache ( NonZeroUsize ::new ( cache_size ) . unwrap ( ) ) ;
2024-01-16 04:56:40 +00:00
while self . is_running ( ) {
feat(terminal): Add ratatui::init() and restore() methods (#1289)
These are simple opinionated methods for creating a terminal that is
useful to use in most apps. The new init method creates a crossterm
backend writing to stdout, enables raw mode, enters the alternate
screen, and sets a panic handler that restores the terminal on panic.
A minimal hello world now looks a bit like:
```rust
use ratatui::{
crossterm::event::{self, Event},
text::Text,
Frame,
};
fn main() {
let mut terminal = ratatui::init();
loop {
terminal
.draw(|frame: &mut Frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))
.expect("Failed to draw");
if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
break;
}
}
ratatui::restore();
}
```
A type alias `DefaultTerminal` is added to represent this terminal
type and to simplify any cases where applications need to pass this
terminal around. It is equivalent to:
`Terminal<CrosstermBackend<Stdout>>`
We also added `ratatui::try_init()` and `try_restore()`, for situations
where you might want to handle initialization errors yourself instead
of letting the panic handler fire and cleanup. Simple Apps should
prefer the `init` and `restore` functions over these functions.
Corresponding functions to allow passing a `TerminalOptions` with
a `Viewport` (e.g. inline, fixed) are also available
(`init_with_options`,
and `try_init_with_options`).
The existing code to create a backend and terminal will remain and
is not deprecated by this approach. This just provides a simple one
line initialization using the common options.
---------
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-08-22 12:16:35 +00:00
terminal . draw ( | frame | frame . render_widget ( self , frame . area ( ) ) ) ? ;
2024-01-16 04:56:40 +00:00
self . handle_events ( ) ? ;
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 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 ( )
2024-09-20 07:21:26 +00:00
. title ( " 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 ;
feat(buffer): add Buffer::cell, cell_mut and index implementations (#1084)
Code which previously called `buf.get(x, y)` or `buf.get_mut(x, y)`
should now use index operators, or be transitioned to `buff.cell()` or
`buf.cell_mut()` for safe access that avoids panics by returning
`Option<&Cell>` and `Option<&mut Cell>`.
The new methods accept `Into<Position>` instead of `x` and `y`
coordinates, which makes them more ergonomic to use.
```rust
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
let cell = buf[(0, 0)];
let cell = buf[Position::new(0, 0)];
let symbol = buf.cell((0, 0)).map(|cell| cell.symbol());
let symbol = buf.cell(Position::new(0, 0)).map(|cell| cell.symbol());
buf[(0, 0)].set_symbol("🐀");
buf[Position::new(0, 0)].set_symbol("🐀");
buf.cell_mut((0, 0)).map(|cell| cell.set_symbol("🐀"));
buf.cell_mut(Position::new(0, 0)).map(|cell| cell.set_symbol("🐀"));
```
The existing `get()` and `get_mut()` methods are marked as deprecated.
These are fairly widely used and we will leave these methods around on
the buffer for a longer time than our normal deprecation approach (2
major release)
Addresses part of: https://github.com/ratatui-org/ratatui/issues/1011
---------
Co-authored-by: EdJoPaTo <rfc-conform-git-commit-email@funny-long-domain-label-everyone-hates-as-it-is-too-long.edjopato.de>
2024-08-06 07:40:47 +00:00
buf [ ( area . x + x , area . y + y ) ] = cell ;
2024-01-16 04:56:40 +00:00
}
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
}
}
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
}
}