diff --git a/README.md b/README.md index f587b5193..988ad7268 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -![Demo of Ratatui](https://repository-images.githubusercontent.com/600886023/96016d23-3bdd-4611-8c03-2e8b5836f900) +![Demo of +Ratatui](https://github.com/ratatui-org/ratatui/blob/ed851fb5bb3673b3213d1511e77be82fa6719ea0/examples/demo2-noborders.gif?raw=true)
diff --git a/examples/README.md b/examples/README.md index 71bf26137..2f95cf0fe 100644 --- a/examples/README.md +++ b/examples/README.md @@ -8,7 +8,7 @@ occur in a terminal. ## Demo -This is the demo example from the main README. It is available for each of the backends. Source: +This is the previous demo example from the main README. It is available for each of the backends. Source: [demo.rs](./demo/). ```shell diff --git a/examples/demo2.tape b/examples/demo2.tape index 8535d5b87..b7ef094d3 100644 --- a/examples/demo2.tape +++ b/examples/demo2.tape @@ -2,27 +2,71 @@ # To run this script, install vhs and run `vhs ./examples/demo.tape` Output "target/demo2.gif" Set Theme "OceanicMaterial" -# Github social preview size (1280x640 with 80px padding) +# Github social preview size (1280x640 with 80px padding) and must be < 1MB +# This puts some constraints on the amount of interactivity we can do. Set Width 1280 Set Height 640 Set Padding 80 +# Without the padding for README.md, etc. +# Set Width 1120 +# Set Height 480 +# Set Padding 0 Hide Type "cargo run --example demo2" Enter Sleep 2s Show # About screen -Sleep 5s +Sleep 3.5s +Down # Red eye +Sleep 0.5s +Down # black eye +Sleep 1s Tab # Email -Sleep 5s +Down +Sleep 1s +Down +Sleep 1s +Down +Sleep 1s +Down +Sleep 2s Tab # Trace route -Sleep 5s -Tab -# Misc -Sleep 5s +Sleep 1s +Down +Sleep 0.5s +Down +Sleep 0.5s +Down +Sleep 0.5s +Down +Sleep 0.5s +Down +Sleep 0.5s +Down +Sleep 0.5s +Down +Sleep 1s Tab # Recipe +Sleep 1s +Down +Sleep 0.5s +Down +Sleep 0.5s +Down +Sleep 0.5s +Down +Sleep 0.5s +Down +Sleep 0.5s +Down +Sleep 0.5s +Down +Sleep 1s +Tab +# Misc Sleep 5s Tab \ No newline at end of file diff --git a/examples/demo2/app.rs b/examples/demo2/app.rs index 0a26f332a..8da855264 100644 --- a/examples/demo2/app.rs +++ b/examples/demo2/app.rs @@ -1,79 +1,52 @@ -use std::{io::Stdout, time::Duration}; +use std::time::Duration; use anyhow::{Context, Result}; use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; -use itertools::Itertools; -use ratatui::prelude::*; -use crate::{ - app_widget::AppWidget, - tabs, - tabs::{AboutTab, EmailTab, MiscWidgetsTab, RecipeTab, Tab, TracerouteTab}, - tui, -}; +use crate::{Root, Term}; +#[derive(Debug)] pub struct App { - terminal: Terminal>, + term: Term, should_quit: bool, - tab_index: usize, - selected_index: usize, - tabs: Vec>, + context: AppContext, +} + +#[derive(Debug, Default, Clone, Copy)] +pub struct AppContext { + pub tab_index: usize, + pub row_index: usize, } impl App { - pub fn new() -> Result { - let terminal = tui::create_terminal()?; + fn new() -> Result { Ok(Self { - terminal, + term: Term::start()?, should_quit: false, - tab_index: 0, - selected_index: 0, - tabs: vec![ - Box::new(tabs::AboutTab::new()), - Box::new(tabs::EmailTab::new(0)), - Box::new(tabs::TracerouteTab::new(0)), - Box::new(tabs::MiscWidgetsTab::new()), - Box::new(tabs::RecipeTab::new(0)), - ], + context: AppContext::default(), }) } - pub fn run(&mut self) -> Result<()> { - tui::setup()?; - while !self.should_quit { - self.draw()?; - self.handle_events()?; + pub fn run() -> Result<()> { + install_panic_hook(); + let mut app = Self::new()?; + while !app.should_quit { + app.draw()?; + app.handle_events()?; } - tui::restore()?; + Term::stop()?; Ok(()) } fn draw(&mut self) -> Result<()> { - self.terminal - .draw(|frame| { - let titles = self.tabs.iter().map(|tab| tab.title()).collect_vec(); - let tab: Box = { - // This is a bit of a hack to get around the borrow checker. - // which works because we know that the tabs are all static. - match self.tab_index { - 0 => Box::new(AboutTab::new()), - 1 => Box::new(EmailTab::new(self.selected_index)), - 2 => Box::new(TracerouteTab::new(self.selected_index)), - 3 => Box::new(MiscWidgetsTab::new()), - 4 => Box::new(RecipeTab::new(self.selected_index)), - _ => unreachable!(), - } - }; - let view = AppWidget::new(tab, self.tab_index, titles); - let area = frame.size(); - frame.render_widget(view, area) - }) + self.term + .draw(|frame| frame.render_widget(Root::new(&self.context), frame.size())) .context("terminal.draw")?; Ok(()) } fn handle_events(&mut self) -> Result<()> { - match tui::next_event(Duration::from_millis(16))? { + match Term::next_event(Duration::from_millis(16))? { Some(Event::Key(key)) => self.handle_key_event(key), _ => Ok(()), } @@ -83,26 +56,29 @@ impl App { if key.kind != KeyEventKind::Press { return Ok(()); } + + let context = &mut self.context; + const TAB_COUNT: usize = 5; match key.code { KeyCode::Char('q') => { self.should_quit = true; } KeyCode::Tab | KeyCode::BackTab if key.modifiers.contains(KeyModifiers::SHIFT) => { - let tab_index = self.tab_index + self.tabs.len(); // to wrap around properly - self.tab_index = tab_index.saturating_sub(1) % self.tabs.len(); - self.selected_index = 0; + let tab_index = context.tab_index + TAB_COUNT; // to wrap around properly + context.tab_index = tab_index.saturating_sub(1) % TAB_COUNT; + context.row_index = 0; } KeyCode::Tab | KeyCode::BackTab => { - self.tab_index = self.tab_index.saturating_add(1) % self.tabs.len(); - self.selected_index = 0; + context.tab_index = context.tab_index.saturating_add(1) % TAB_COUNT; + context.row_index = 0; } KeyCode::Left | KeyCode::Char('h') => {} KeyCode::Right | KeyCode::Char('l') => {} KeyCode::Up | KeyCode::Char('k') => { - self.selected_index = self.selected_index.saturating_sub(1); + context.row_index = context.row_index.saturating_sub(1); } KeyCode::Down | KeyCode::Char('j') => { - self.selected_index = self.selected_index.saturating_add(1); + context.row_index = context.row_index.saturating_add(1); } _ => {} }; @@ -110,17 +86,11 @@ impl App { } } -impl Drop for App { - fn drop(&mut self) { - let _ = tui::restore(); - } -} - pub fn install_panic_hook() { better_panic::install(); let hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { - let _ = tui::restore(); + let _ = Term::stop(); hook(info); std::process::exit(1); })); diff --git a/examples/demo2/app_widget.rs b/examples/demo2/app_widget.rs deleted file mode 100644 index 5c5a142c1..000000000 --- a/examples/demo2/app_widget.rs +++ /dev/null @@ -1,69 +0,0 @@ -use ratatui::{prelude::*, widgets::*}; - -use crate::{styles, tabs::Tab, tui}; - -pub struct AppWidget { - tab: Box, - tab_index: usize, - titles: Vec, -} - -impl AppWidget { - pub fn new(tab: Box, tab_index: usize, titles: Vec) -> Self { - AppWidget { - tab, - tab_index, - titles, - } - } -} - -impl Widget for AppWidget { - fn render(self, area: Rect, buf: &mut Buffer) { - Block::new().bg(styles::APP_BACKGROUND).render(area, buf); - let area = tui::layout(area, Direction::Vertical, vec![1, 0, 1]); - self.render_title_bar(area[0], buf); - self.render_selected_tab(area[1], buf); - self.render_bottom_bar(area[2], buf); - } -} - -impl AppWidget { - fn render_title_bar(&self, area: Rect, buf: &mut Buffer) { - let area = tui::layout(area, Direction::Horizontal, vec![17, 0]); - - Paragraph::new(Span::styled("Ratatui v0.23.0 ", styles::APP_TITLE)).render(area[0], buf); - - Tabs::new(self.titles.clone()) - .style(styles::TABS) - .highlight_style(styles::TABS_SELECTED) - .select(self.tab_index) - .render(area[1], buf); - } - - fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) { - self.tab.render(area, buf); - } - - fn render_bottom_bar(&self, area: Rect, buf: &mut Buffer) { - let key_style = Style::new().fg(Color::Indexed(232)).bg(Color::Indexed(236)); - Paragraph::new(Line::from(vec![ - " Q/Esc ".set_style(key_style), - " Quit ".into(), - " Tab ".set_style(key_style), - " Next Tab ".into(), - " ←/h ".set_style(key_style), - " Left ".into(), - " →/l ".set_style(key_style), - " Right ".into(), - " ↑/k ".set_style(key_style), - " Up ".into(), - " ↓/j ".set_style(key_style), - " Down".into(), - ])) - .alignment(Alignment::Center) - .fg(Color::Indexed(236)) - .bg(Color::Indexed(232)) - .render(area, buf); - } -} diff --git a/examples/demo2/colors.rs b/examples/demo2/colors.rs index 67489a97c..d82caeedb 100644 --- a/examples/demo2/colors.rs +++ b/examples/demo2/colors.rs @@ -1,11 +1,11 @@ -#![allow(dead_code)] use palette::{ convert::{FromColorUnclamped, IntoColorUnclamped}, Okhsv, Srgb, }; use ratatui::{prelude::*, widgets::*}; -fn render_16_colors(area: Rect, buf: &mut Buffer) { +#[allow(dead_code)] +fn render_16_color_swatch(area: Rect, buf: &mut Buffer) { let sym = "██"; Paragraph::new(vec![ Line::from(vec![sym.black(), sym.red(), sym.green(), sym.yellow()]), @@ -26,7 +26,8 @@ fn render_16_colors(area: Rect, buf: &mut Buffer) { .render(area, buf); } -fn render_256_colors(area: Rect, buf: &mut Buffer) { +#[allow(dead_code)] +fn render_256_color_swatch(area: Rect, buf: &mut Buffer) { for (xi, x) in (16..52).zip(area.left()..area.right()) { for (yi, y) in (0..3).zip(area.top()..area.bottom()) { let fg = Color::Indexed(yi * 72 + xi); @@ -38,7 +39,7 @@ fn render_256_colors(area: Rect, buf: &mut Buffer) { } } -pub fn render_rgb_colors(area: Rect, buf: &mut Buffer) { +pub fn render_rgb_swatch(area: Rect, buf: &mut Buffer) { for (xi, x) in (area.left()..area.right()).enumerate() { for (yi, y) in (area.top()..area.bottom()).enumerate() { let yi = area.height as usize - yi - 1; diff --git a/examples/demo2/main.rs b/examples/demo2/main.rs index 8a2597901..bafa8e901 100644 --- a/examples/demo2/main.rs +++ b/examples/demo2/main.rs @@ -1,13 +1,16 @@ use anyhow::Result; +pub use app::*; +pub use root::*; +pub use term::*; +pub use theme::*; mod app; -mod app_widget; mod colors; -mod styles; +mod root; mod tabs; -mod tui; +mod term; +mod theme; fn main() -> Result<()> { - app::install_panic_hook(); - app::App::new()?.run() + App::run() } diff --git a/examples/demo2/root.rs b/examples/demo2/root.rs new file mode 100644 index 000000000..37890af32 --- /dev/null +++ b/examples/demo2/root.rs @@ -0,0 +1,74 @@ +use itertools::Itertools; +use ratatui::{prelude::*, widgets::*}; + +use crate::{layout, tabs::*, AppContext, THEME}; + +pub struct Root<'a> { + context: &'a AppContext, +} + +impl<'a> Root<'a> { + pub fn new(context: &'a AppContext) -> Self { + Root { context } + } +} + +impl Widget for Root<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + Block::new().style(THEME.root).render(area, buf); + let area = layout(area, Direction::Vertical, vec![1, 0, 1]); + self.render_title_bar(area[0], buf); + self.render_selected_tab(area[1], buf); + self.render_bottom_bar(area[2], buf); + } +} + +impl Root<'_> { + fn render_title_bar(&self, area: Rect, buf: &mut Buffer) { + let area = layout(area, Direction::Horizontal, vec![0, 53]); + + Paragraph::new(Span::styled("Ratatui Demos ", THEME.app_title)).render(area[0], buf); + let titles = vec![" About ", " Email ", " Traceroute ", " Recipe ", " Misc "]; + Tabs::new(titles) + .style(THEME.tabs) + .highlight_style(THEME.tabs_selected) + .select(self.context.tab_index) + .render(area[1], buf); + } + + fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) { + let row_index = self.context.row_index; + match self.context.tab_index { + 0 => AboutTab::new(row_index).render(area, buf), + 1 => EmailTab::new(row_index).render(area, buf), + 2 => TracerouteTab::new(row_index).render(area, buf), + 3 => RecipeTab::new(row_index).render(area, buf), + 4 => MiscWidgetsTab::new().render(area, buf), + _ => unreachable!(), + }; + } + + fn render_bottom_bar(&self, area: Rect, buf: &mut Buffer) { + let keys = [ + ("Q/Esc", "Quit"), + ("Tab", "Next Tab"), + ("←/h", "Left"), + ("→/l", "Right"), + ("↑/k", "Up"), + ("↓/j", "Down"), + ]; + let spans = keys + .iter() + .flat_map(|(key, desc)| { + let key = Span::styled(format!(" {} ", key), THEME.key_binding.key); + let desc = Span::styled(format!(" {} ", desc), THEME.key_binding.description); + [key, desc] + }) + .collect_vec(); + Paragraph::new(Line::from(spans)) + .alignment(Alignment::Center) + .fg(Color::Indexed(236)) + .bg(Color::Indexed(232)) + .render(area, buf); + } +} diff --git a/examples/demo2/styles.rs b/examples/demo2/styles.rs deleted file mode 100644 index ea853a386..000000000 --- a/examples/demo2/styles.rs +++ /dev/null @@ -1,16 +0,0 @@ -use ratatui::prelude::*; - -pub const DARK_BLUE: Color = Color::Rgb(16, 24, 48); -pub const LIGHT_BLUE: Color = Color::Rgb(64, 96, 192); -pub const APP_BACKGROUND: Color = DARK_BLUE; -pub const APP: Style = Style::new().bg(APP_BACKGROUND); -pub const APP_TITLE: Style = Style::new() - .add_modifier(Modifier::BOLD) - .fg(Color::Indexed(252)) - .bg(Color::Indexed(232)); -pub const TABS: Style = Style::new().fg(Color::Indexed(244)).bg(Color::Indexed(232)); -pub const TABS_SELECTED: Style = Style::new().add_modifier(Modifier::BOLD).fg(LIGHT_BLUE); -pub const BORDERS: Style = Style::new() - .fg(Color::Indexed(252)) - .add_modifier(Modifier::BOLD); -pub const DESCRIPTION: Style = Style::new().fg(Color::Gray).bg(DARK_BLUE); diff --git a/examples/demo2/tabs.rs b/examples/demo2/tabs.rs index f5f91fd34..47a77e023 100644 --- a/examples/demo2/tabs.rs +++ b/examples/demo2/tabs.rs @@ -1,5 +1,3 @@ -use ratatui::prelude::*; - mod about; mod email; mod misc; @@ -11,9 +9,3 @@ pub use email::EmailTab; pub use misc::MiscWidgetsTab; pub use recipe::RecipeTab; pub use traceroute::TracerouteTab; - -pub trait Tab { - fn title(&self) -> String; - fn render(&self, area: Rect, buf: &mut Buffer); - fn select(&mut self, _row: usize) {} -} diff --git a/examples/demo2/tabs/about.rs b/examples/demo2/tabs/about.rs index 33b4695d3..e070bac90 100644 --- a/examples/demo2/tabs/about.rs +++ b/examples/demo2/tabs/about.rs @@ -1,8 +1,7 @@ use itertools::Itertools; use ratatui::{prelude::*, widgets::*}; -use super::Tab; -use crate::{colors, styles, tui::layout}; +use crate::{colors, layout, THEME}; const RATATUI_LOGO: [&str; 32] = [ " ███ ", @@ -16,8 +15,8 @@ const RATATUI_LOGO: [&str; 32] = [ " █████████████ ██████", " ███████████ ████████", " █████ ███████████ ", - " ███ ██xx████████ ", - " █ ███xx████████ ", + " ███ ██ee████████ ", + " █ ███ee████████ ", " ████ █████████████ ", " █████████████████ ", " ████████████████ ", @@ -39,28 +38,65 @@ const RATATUI_LOGO: [&str; 32] = [ " █xxxxxxxxxxxxxxxxxxxx█ █ ", ]; -pub struct AboutTab; +pub struct AboutTab { + selected_row: usize, +} impl AboutTab { - pub fn new() -> Self { - Self {} + pub fn new(selected_row: usize) -> Self { + Self { selected_row } } } -impl Tab for AboutTab { - fn title(&self) -> String { - "About".to_string() - } - - fn render(&self, area: Rect, buf: &mut Buffer) { - colors::render_rgb_colors(area, buf); +impl Widget for AboutTab { + fn render(self, area: Rect, buf: &mut Buffer) { + colors::render_rgb_swatch(area, buf); let area = layout(area, Direction::Horizontal, vec![34, 0]); render_crate_description(area[1], buf); - render_logo(area[0], buf); + render_logo(self.selected_row, area[0], buf); } } -pub fn render_logo(area: Rect, buf: &mut Buffer) { +fn render_crate_description(area: Rect, buf: &mut Buffer) { + let area = area.inner( + &(Margin { + vertical: 4, + horizontal: 2, + }), + ); + Clear.render(area, buf); // clear out the color swatches + Block::new().style(THEME.content).render(area, buf); + let area = area.inner( + &(Margin { + vertical: 1, + horizontal: 2, + }), + ); + let text = "- cooking up terminal user interfaces - + + Ratatui is a Rust crate that provides widgets (e.g. Paragraph, Table) and draws them to the \ + screen efficiently every frame."; + Paragraph::new(text) + .style(THEME.description) + .block( + Block::new() + .title(" Ratatui ") + .title_alignment(Alignment::Center) + .borders(Borders::TOP) + .border_style(THEME.description_title) + .padding(Padding::new(0, 0, 0, 0)), + ) + .wrap(Wrap { trim: true }) + .scroll((0, 0)) + .render(area, buf); +} + +pub fn render_logo(selected_row: usize, area: Rect, buf: &mut Buffer) { + let eye_color = if selected_row % 2 == 0 { + THEME.logo.rat_eye + } else { + THEME.logo.rat_eye_alt + }; let area = area.inner(&Margin { vertical: 0, horizontal: 2, @@ -70,70 +106,48 @@ pub fn render_logo(area: Rect, buf: &mut Buffer) { let x = area.left() + x as u16; let y = area.top() + y as u16; let cell = buf.get_mut(x, y); + let rat_color = THEME.logo.rat; + let term_color = THEME.logo.term; match (ch1, ch2) { ('█', '█') => { cell.set_char('█'); - cell.fg = Color::Indexed(255); + cell.fg = rat_color; } ('█', ' ') => { cell.set_char('▀'); - cell.fg = Color::Indexed(255); + cell.fg = rat_color; } (' ', '█') => { cell.set_char('▄'); - cell.fg = Color::Indexed(255); + cell.fg = rat_color; } ('█', 'x') => { cell.set_char('▀'); - cell.fg = Color::Indexed(255); - cell.bg = Color::Black; + cell.fg = rat_color; + cell.bg = term_color; } ('x', '█') => { cell.set_char('▄'); - cell.fg = Color::Indexed(255); - cell.bg = Color::Black; + cell.fg = rat_color; + cell.bg = term_color; } ('x', 'x') => { cell.set_char(' '); - cell.fg = Color::Indexed(255); - cell.bg = Color::Black; + cell.fg = term_color; + cell.bg = term_color; + } + ('█', 'e') => { + cell.set_char('▀'); + cell.fg = rat_color; + cell.bg = eye_color; + } + ('e', '█') => { + cell.set_char('▄'); + cell.fg = rat_color; + cell.bg = eye_color; } (_, _) => {} }; } } } - -fn render_crate_description(area: Rect, buf: &mut Buffer) { - let area = area.inner( - &(Margin { - vertical: 4, - horizontal: 2, - }), - ); - Clear.render(area, buf); // clear out the color swatches - Block::new().style(styles::APP).render(area, buf); - let area = area.inner( - &(Margin { - vertical: 1, - horizontal: 2, - }), - ); - let text = "- cooking up terminal user interfaces - - - Ratatui is a Rust crate that provides widgets (e.g. Pargraph, Table) and draws them to the \ - screen efficiently every frame."; - Paragraph::new(text) - .style(styles::DESCRIPTION) - .block( - Block::new() - .title("Ratatui") - .title_alignment(Alignment::Center) - .borders(Borders::TOP) - .border_style(styles::BORDERS) - .padding(Padding::new(0, 0, 0, 0)), - ) - .wrap(Wrap { trim: true }) - .scroll((0, 0)) - .render(area, buf); -} diff --git a/examples/demo2/tabs/email.rs b/examples/demo2/tabs/email.rs index 47f6596cc..d0841fa5d 100644 --- a/examples/demo2/tabs/email.rs +++ b/examples/demo2/tabs/email.rs @@ -2,8 +2,7 @@ use itertools::Itertools; use ratatui::{prelude::*, widgets::*}; use unicode_width::UnicodeWidthStr; -use super::Tab; -use crate::{colors, styles, tui::layout}; +use crate::{colors, layout, THEME}; #[derive(Debug, Default)] pub struct Email { @@ -12,25 +11,6 @@ pub struct Email { body: &'static str, } -#[derive(Debug, Default)] -pub struct EmailTab { - selected_index: usize, -} - -impl Tab for EmailTab { - fn title(&self) -> String { - "Email".to_string() - } - - fn select(&mut self, row: usize) { - self.selected_index = row; - } - - fn render(&self, area: Rect, buf: &mut Buffer) { - self.render(area, buf); - } -} - const EMAILS: &[Email] = &[ Email { from: "Alice ", @@ -59,81 +39,95 @@ const EMAILS: &[Email] = &[ }, ]; +#[derive(Debug, Default)] +pub struct EmailTab { + selected_index: usize, +} + impl EmailTab { pub fn new(selected_index: usize) -> Self { Self { selected_index: selected_index % EMAILS.len(), } } +} - fn render(&self, area: Rect, buf: &mut Buffer) { - colors::render_rgb_colors(area, buf); +impl Widget for EmailTab { + fn render(self, area: Rect, buf: &mut Buffer) { + colors::render_rgb_swatch(area, buf); let area = area.inner(&Margin { vertical: 1, horizontal: 2, }); Clear.render(area, buf); let area = layout(area, Direction::Vertical, vec![6, 0]); - self.render_inbox(area[0], buf); - self.render_email(area[1], buf); + render_inbox(self.selected_index, area[0], buf); + render_email(self.selected_index, area[1], buf); } +} +fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) { + let area = layout(area, Direction::Vertical, vec![1, 0]); + let theme = THEME.email; + Tabs::new(vec![" Inbox ", " Sent ", " Drafts "]) + .style(theme.tabs) + .highlight_style(theme.tabs_selected) + .select(0) + .divider("") + .render(area[0], buf); - fn render_inbox(&self, area: Rect, buf: &mut Buffer) { - let area = layout(area, Direction::Vertical, vec![1, 0]); - Tabs::new(vec![" Inbox ", " Sent ", " Drafts "]) - .style(Style::new().fg(Color::Indexed(244)).bg(Color::Indexed(232))) - .highlight_style( - Style::new() - .bold() - .fg(Color::Indexed(232)) - .bg(Color::Rgb(64, 96, 192)), - ) - .select(0) - .divider("") - .render(area[0], buf); - - let highlight_symbol = ">>"; - let from_width = EMAILS - .iter() - .map(|e| e.from.width()) - .max() - .unwrap_or_default(); - let subject_width = area[1].width as usize - from_width - highlight_symbol.width() - 1; - let items = EMAILS - .iter() - .map(|e| { - let from = format!("{:width$}", e.from, width = from_width); - let subject = format!("{:width$}", e.subject, width = subject_width); - let text = [from, subject].join(" "); - ListItem::new(text) - }) - .collect_vec(); - let mut state = ListState::default().with_selected(Some(self.selected_index)); - StatefulWidget::render( - List::new(items) - .highlight_style(Style::new().bold().yellow()) - .highlight_symbol(highlight_symbol), - area[1], - buf, - &mut state, - ); - } + let highlight_symbol = ">>"; + let from_width = EMAILS + .iter() + .map(|e| e.from.width()) + .max() + .unwrap_or_default(); + let subject_width = area[1].width as usize - from_width - highlight_symbol.width() - 1; + let items = EMAILS + .iter() + .map(|e| { + let from = format!("{:width$}", e.from, width = from_width); + let subject = format!("{:width$}", e.subject, width = subject_width); + let text = [from, subject].join(" "); + ListItem::new(text) + }) + .collect_vec(); + let mut state = ListState::default().with_selected(Some(selected_index)); + StatefulWidget::render( + List::new(items) + .style(theme.inbox) + .highlight_style(theme.selected_item) + .highlight_symbol(highlight_symbol), + area[1], + buf, + &mut state, + ); +} - fn render_email(&self, area: Rect, buf: &mut Buffer) { - let email = EMAILS.get(self.selected_index); - let block = Block::new().borders(Borders::TOP).style(styles::APP); - let inner = block.inner(area); - block.render(area, buf); - if let Some(email) = email { - let mut text = vec![ - Line::from(vec!["From: ".bold(), email.from.clone().into()]), - Line::from(vec!["Subject: ".bold(), email.subject.clone().into()]), - "-".repeat(inner.width as usize).dim().into(), - ]; - text.extend(email.body.lines().map(Line::from)); - Paragraph::new(text).render(inner, buf); - } else { - Paragraph::new("No email selected").render(inner, buf); - } +fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) { + let theme = THEME.email; + let email = EMAILS.get(selected_index); + let block = Block::new().style(theme.body); + let inner = block.inner(area); + block.render(area, buf); + if let Some(email) = email { + let area = layout(inner, Direction::Vertical, vec![3, 0]); + let headers = vec![ + Line::from(vec![ + "From: ".set_style(theme.header), + email.from.set_style(theme.header_value), + ]), + Line::from(vec![ + "Subject: ".set_style(theme.header), + email.subject.set_style(theme.header_value), + ]), + "-".repeat(inner.width as usize).dim().into(), + ]; + Paragraph::new(headers) + .style(theme.body) + .render(area[0], buf); + let body = email.body.lines().map(Line::from).collect_vec(); + Paragraph::new(body).style(theme.body).render(area[1], buf); + } else { + Paragraph::new("No email selected").render(inner, buf); } } diff --git a/examples/demo2/tabs/misc.rs b/examples/demo2/tabs/misc.rs index 5ac2e2142..7c960ce86 100644 --- a/examples/demo2/tabs/misc.rs +++ b/examples/demo2/tabs/misc.rs @@ -1,9 +1,6 @@ -#![allow(dead_code)] - use ratatui::{prelude::*, widgets::*}; -use super::Tab; -use crate::{colors, styles, tui::layout}; +use crate::{colors, layout, THEME}; pub struct MiscWidgetsTab { pub selected_row: usize, @@ -15,19 +12,15 @@ impl MiscWidgetsTab { } } -impl Tab for MiscWidgetsTab { - fn title(&self) -> String { - "Misc Widgets".to_string() - } - - fn render(&self, area: Rect, buf: &mut Buffer) { - colors::render_rgb_colors(area, buf); +impl Widget for MiscWidgetsTab { + fn render(self, area: Rect, buf: &mut Buffer) { + colors::render_rgb_swatch(area, buf); let area = area.inner(&Margin { vertical: 1, horizontal: 2, }); Clear.render(area, buf); - Block::new().style(styles::APP).render(area, buf); + Block::new().style(THEME.content).render(area, buf); let area = layout(area, Direction::Vertical, vec![0, 5]); render_bars(area[0], buf); render_gauges(self.selected_row, area[1], buf); diff --git a/examples/demo2/tabs/recipe.rs b/examples/demo2/tabs/recipe.rs index acdd0869c..425015551 100644 --- a/examples/demo2/tabs/recipe.rs +++ b/examples/demo2/tabs/recipe.rs @@ -1,7 +1,88 @@ +use itertools::Itertools; use ratatui::{prelude::*, widgets::*}; -use super::Tab; -use crate::{colors, styles, tui}; +use crate::{colors, layout, THEME}; + +#[derive(Debug, Default, Clone, Copy)] +struct Ingredient { + quantity: &'static str, + name: &'static str, +} + +impl Ingredient { + fn height(&self) -> u16 { + self.name.lines().count() as u16 + } +} + +impl<'a> From for Row<'a> { + fn from(i: Ingredient) -> Self { + Row::new(vec![i.quantity, i.name]).height(i.height()) + } +} + +// https://www.realsimple.com/food-recipes/browse-all-recipes/ratatouille +const RECIPE: &[(&str, &str)] = &[ + ( + "Step 1: ", + "Over medium-low heat, add the oil to a large skillet with the onion, garlic, and bay \ + leaf, stirring occasionally, until the onion has softened.", + ), + ( + "Step 2: ", + "Add the eggplant and cook, stirring occasionally, for 8 minutes or until the eggplant \ + has softened. Stir in the zucchini, red bell pepper, tomatoes, and salt, and cook over \ + medium heat, stirring occasionally, for 5 to 7 minutes or until the vegetables are \ + tender. Stir in the basil and few grinds of pepper to taste.", + ), +]; + +const INGREDIENTS: &[Ingredient] = &[ + Ingredient { + quantity: "4 tbsp", + name: "olive oil", + }, + Ingredient { + quantity: "1", + name: "onion thinly sliced", + }, + Ingredient { + quantity: "4", + name: "cloves garlic\npeeled and sliced", + }, + Ingredient { + quantity: "1", + name: "small bay leaf", + }, + Ingredient { + quantity: "1", + name: "small eggplant cut\ninto 1/2 inch cubes", + }, + Ingredient { + quantity: "1", + name: "small zucchini halved\nlengthwise and cut\ninto thin slices", + }, + Ingredient { + quantity: "1", + name: "red bell pepper cut\ninto slivers", + }, + Ingredient { + quantity: "4", + name: "plum tomatoes\ncoarsely chopped", + }, + Ingredient { + quantity: "1 tsp", + name: "kosher salt", + }, + Ingredient { + quantity: "1/4 cup", + name: "shredded fresh basil\nleaves", + }, + Ingredient { + quantity: "", + name: "freshly ground black\npepper", + }, +]; #[derive(Debug)] pub struct RecipeTab { @@ -10,88 +91,82 @@ pub struct RecipeTab { impl RecipeTab { pub fn new(selected_row: usize) -> Self { - const INGREDIENT_COUNT: usize = 11; // TODO: derive this from the table Self { - selected_row: selected_row % INGREDIENT_COUNT, + selected_row: selected_row % INGREDIENTS.len(), } } } -impl Tab for RecipeTab { - fn title(&self) -> String { - "Recipe".to_string() - } - - fn select(&mut self, row: usize) { - self.selected_row = row; - } - - fn render(&self, area: Rect, buf: &mut Buffer) { - colors::render_rgb_colors(area, buf); +impl Widget for RecipeTab { + fn render(self, area: Rect, buf: &mut Buffer) { + colors::render_rgb_swatch(area, buf); let area = area.inner(&Margin { vertical: 1, horizontal: 2, }); Clear.render(area, buf); - Block::new().style(styles::APP).render(area, buf); + Block::new() + .title("Ratatouille Recipe") + .style(THEME.content) + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(THEME.borders) + .padding(Padding::new(2, 2, 1, 1)) + .render(area, buf); - let area = tui::layout(area, Direction::Vertical, vec![8, 0]); + let scrollbar_area = Rect { + y: area.y + 2, + height: area.height - 3, + ..area + }; + render_scrollbar(self.selected_row, scrollbar_area, buf); - let lines: Vec = vec![ - Line::from(vec![ - "Step 1: ".white().bold(), - "Over medium-low heat, add the oil to a large skillet with the onion, garlic, and bay \ - leaf, stirring occasionally, until the onion has softened." - .into(), - ]), - Line::from(vec![ - "Step 2: ".white().bold(), - "Add the eggplant and cook, stirring occasionally, for 8 minutes or until the \ - eggplant has softened. Stir in the zucchini, red bell pepper, tomatoes, and salt, and \ - cook over medium heat, stirring occasionally, for 5 to 7 minutes or until the \ - vegetables are tender. Stir in the basil and few grinds of pepper to taste." - .into(), - ]), - Line::from(vec!["Ingredients:".white().bold()]), - ]; - Paragraph::new(lines) - .wrap(Wrap { trim: true }) - .render(area[0], buf); + let area = area.inner(&Margin { + horizontal: 2, + vertical: 1, + }); + let area = layout(area, Direction::Horizontal, vec![44, 0]); - let mut state = TableState::default().with_selected(Some(self.selected_row)); - // https://www.realsimple.com/food-recipes/browse-all-recipes/ratatouille - StatefulWidget::render( - Table::new(vec![ - Row::new(vec!["4 tbsp", "olive oil", ""]), - Row::new(vec!["1", "onion", "thinly sliced"]), - Row::new(vec!["4", "cloves garlic", "peeled and sliced"]), - Row::new(vec!["1", "small bay leaf", ""]), - Row::new(vec!["1", "small eggplant", "cut into 1/2 inch cubes"]), - Row::new(vec![ - "1".into(), - "small zucchini".into(), - Text::raw("halved lengthwise and cut into\nthin slices"), - ]) - .height(2), - Row::new(vec!["1", "red bell pepper", "cut into slivers"]), - Row::new(vec!["4", "plum tomatoes", "coarsely chopped"]), - Row::new(vec!["1 tsp", "kosher salt", ""]), - Row::new(vec!["1/4 cup", "shredded fresh basil leaves", ""]), - Row::new(vec!["", "freshly ground black pepper", ""]), - ]) - .header( - Row::new(vec!["Qty", "Ingredient", "Notes"]) - .style(Style::new().white().underlined()), - ) - .widths(&[ - Constraint::Length(7), - Constraint::Length(30), - Constraint::Length(450), - ]) - .highlight_style(Style::new().light_yellow()), - area[1], - buf, - &mut state, - ); + render_recipe(area[0], buf); + render_ingredients(self.selected_row, area[1], buf); } } + +fn render_recipe(area: Rect, buf: &mut Buffer) { + let lines = RECIPE + .iter() + .map(|(step, text)| Line::from(vec![step.white().bold(), text.gray()])) + .collect_vec(); + Paragraph::new(lines) + .wrap(Wrap { trim: true }) + .block(Block::new().padding(Padding::new(0, 1, 0, 0))) + .render(area, buf); +} + +fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) { + let mut state = TableState::default().with_selected(Some(selected_row)); + let rows = INGREDIENTS.iter().map(|&i| i.into()).collect_vec(); + let theme = THEME.recipe; + StatefulWidget::render( + Table::new(rows) + .block(Block::new().style(theme.ingredients)) + .header(Row::new(vec!["Qty", "Ingredient"]).style(theme.ingredients_header)) + .widths(&[Constraint::Length(7), Constraint::Length(30)]) + .highlight_style(Style::new().light_yellow()), + area, + buf, + &mut state, + ); +} + +fn render_scrollbar(position: usize, area: Rect, buf: &mut Buffer) { + let mut state = ScrollbarState::default() + .content_length(INGREDIENTS.len()) + .viewport_content_length(6) + .position(position); + Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(None) + .end_symbol(None) + .track_symbol(None) + .render(area, buf, &mut state) +} diff --git a/examples/demo2/tabs/traceroute.rs b/examples/demo2/tabs/traceroute.rs index f7da25a46..db222f43a 100644 --- a/examples/demo2/tabs/traceroute.rs +++ b/examples/demo2/tabs/traceroute.rs @@ -1,31 +1,10 @@ use itertools::Itertools; use ratatui::{ prelude::*, - widgets::{ - canvas::{Canvas, Map, Points}, - *, - }, + widgets::{canvas::*, *}, }; -use super::Tab; -use crate::{colors, styles, tui::layout}; - -#[derive(Debug)] -struct Hop { - host: &'static str, - address: &'static str, - location: (f64, f64), -} - -impl Hop { - const fn new(name: &'static str, address: &'static str, location: (f64, f64)) -> Self { - Self { - host: name, - address, - location, - } - } -} +use crate::{colors, layout, THEME}; #[derive(Debug)] pub struct TracerouteTab { @@ -40,94 +19,136 @@ impl TracerouteTab { } } -impl Tab for TracerouteTab { - fn title(&self) -> String { - "Traceroute".to_string() - } - - fn render(&self, area: Rect, buf: &mut Buffer) { - self.render_traceroute_tab(area, buf); - } - - fn select(&mut self, row: usize) { - self.selected_row = row; - } -} - -impl TracerouteTab { - fn render_traceroute_tab(&self, area: Rect, buf: &mut Buffer) { - colors::render_rgb_colors(area, buf); +impl Widget for TracerouteTab { + fn render(self, area: Rect, buf: &mut Buffer) { + colors::render_rgb_swatch(area, buf); let area = area.inner(&Margin { vertical: 1, horizontal: 2, }); Clear.render(area, buf); - Block::new().style(styles::APP).render(area, buf); + Block::new().style(THEME.content).render(area, buf); let area = Layout::default() .direction(Direction::Horizontal) .constraints(vec![Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) .split(area); let left_area = layout(area[0], Direction::Vertical, vec![0, 4]); - self.render_hops(left_area[0], buf); + render_hops(self.selected_row, left_area[0], buf); render_ping(self.selected_row, left_area[1], buf); - self.render_map(area[1], buf); + render_map(self.selected_row, area[1], buf); } +} - fn render_hops(&self, area: Rect, buf: &mut Buffer) { - let mut state = TableState::default().with_selected(Some(self.selected_row)); - let rows = HOPS - .iter() - .map(|hop| Row::new(vec![hop.host, hop.address])) - .collect_vec(); - let block = Block::default() - .title("Traceroute bad.horse") - .borders(Borders::ALL) - .border_type(BorderType::Rounded); - StatefulWidget::render( - Table::new(rows) - .header(Row::new(vec!["Host", "Address"]).bold().underlined()) - .widths(&[Constraint::Max(100), Constraint::Length(15)]) - .highlight_style(Style::new().dark_gray().on_white()) - .block(block), - area, - buf, - &mut state, - ); - } +fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) { + let mut state = TableState::default().with_selected(Some(selected_row)); + let rows = HOPS + .iter() + .map(|hop| Row::new(vec![hop.host, hop.address])) + .collect_vec(); + let block = Block::default() + .title("Traceroute bad.horse") + .borders(Borders::ALL) + .border_type(BorderType::Rounded); + StatefulWidget::render( + Table::new(rows) + .header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header)) + .widths(&[Constraint::Max(100), Constraint::Length(15)]) + .highlight_style(THEME.traceroute.selected) + .block(block), + area, + buf, + &mut state, + ); + let mut scrollbar_state = ScrollbarState::default() + .content_length(HOPS.len()) + .position(selected_row); + let area = Rect { + y: area.y + 2, + height: area.height - 3, + ..area + }; + Scrollbar::default() + .begin_symbol(None) + .end_symbol(None) + .track_symbol(Some(symbols::line::VERTICAL)) + .render(area, buf, &mut scrollbar_state); +} - fn render_map(&self, area: Rect, buf: &mut Buffer) { - let path: Option<(&Hop, &Hop)> = HOPS.iter().tuple_windows().nth(self.selected_row); - let block = Block::new().title("Map").borders(Borders::ALL); - let map = Map { - resolution: canvas::MapResolution::High, - color: Color::Gray, - }; - Canvas::default() - .marker(Marker::Dot) - .x_bounds([113.0, 154.0]) // australia - .y_bounds([-42.0, -11.0]) // australia - .paint(|context| { - context.draw(&map); - if let Some(path) = path { - context.draw(&canvas::Line::new( - path.0.location.0, - path.0.location.1, - path.1.location.0, - path.1.location.1, - Color::Blue, - )); - context.draw(&Points { - color: Color::Green, - coords: &[path.0.location], // sydney - }); - context.draw(&Points { - color: Color::Red, - coords: &[path.1.location], // perth - }); - } - }) - .block(block) - .render(area, buf); +pub fn render_ping(progress: usize, area: Rect, buf: &mut Buffer) { + let mut data = [ + 8, 8, 8, 8, 7, 7, 7, 6, 6, 5, 4, 3, 3, 2, 2, 1, 1, 1, 2, 2, 3, 4, 5, 6, 7, 7, 8, 8, 8, 7, + 7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 2, 4, 6, 7, 8, 8, 8, 8, 6, 4, 2, 1, 1, 1, 1, 2, 2, 2, 3, + 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, + ]; + let mid = progress % data.len(); + data.rotate_left(mid); + Sparkline::default() + .block( + Block::new() + .title("Ping") + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ) + .data(&data) + .style(THEME.traceroute.ping) + .render(area, buf); +} + +fn render_map(selected_row: usize, area: Rect, buf: &mut Buffer) { + let theme = THEME.traceroute.map; + let path: Option<(&Hop, &Hop)> = HOPS.iter().tuple_windows().nth(selected_row); + let map = Map { + resolution: canvas::MapResolution::High, + color: theme.color, + }; + Canvas::default() + .background_color(theme.background_color) + .block( + Block::new() + .title("Map") + .borders(Borders::ALL) + .style(theme.style), + ) + .marker(Marker::Dot) + .x_bounds([113.0, 154.0]) // australia + .y_bounds([-42.0, -11.0]) // australia + .paint(|context| { + context.draw(&map); + if let Some(path) = path { + context.draw(&canvas::Line::new( + path.0.location.0, + path.0.location.1, + path.1.location.0, + path.1.location.1, + theme.path, + )); + context.draw(&Points { + color: theme.source, + coords: &[path.0.location], // sydney + }); + context.draw(&Points { + color: theme.destination, + coords: &[path.1.location], // perth + }); + } + }) + .render(area, buf); +} + +#[derive(Debug)] +struct Hop { + host: &'static str, + address: &'static str, + location: (f64, f64), +} + +impl Hop { + const fn new(name: &'static str, address: &'static str, location: (f64, f64)) -> Self { + Self { + host: name, + address, + location, + } } } @@ -172,23 +193,3 @@ const HOPS: &[Hop] = &[ Hop::new("it-s.hi-ho.silver", "162.252.205.156", SYDNEY), Hop::new("signed.bad.horse", "162.252.205.157", CANBERRA), ]; - -pub fn render_ping(progress: usize, area: Rect, buf: &mut Buffer) { - let mut data = [ - 8, 8, 8, 8, 7, 7, 7, 6, 6, 5, 4, 3, 3, 2, 2, 1, 1, 1, 2, 2, 3, 4, 5, 6, 7, 7, 8, 8, 8, 7, - 7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 2, 4, 6, 7, 8, 8, 8, 8, 6, 4, 2, 1, 1, 1, 1, 2, 2, 2, 3, - 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, - ]; - let mid = progress % data.len(); - data.rotate_left(mid); - Sparkline::default() - .block( - Block::new() - .title("Ping") - .borders(Borders::ALL) - .border_type(BorderType::Rounded), - ) - .data(&data) - .style(Style::new().white()) - .render(area, buf); -} diff --git a/examples/demo2/term.rs b/examples/demo2/term.rs new file mode 100644 index 000000000..7d12b868d --- /dev/null +++ b/examples/demo2/term.rs @@ -0,0 +1,91 @@ +use std::{ + io::{self, stdout, Stdout}, + ops::{Deref, DerefMut}, + rc::Rc, + time::Duration, +}; + +use anyhow::{Context, Result}; +use crossterm::{ + event::{self, Event}, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + ExecutableCommand, +}; +use itertools::Itertools; +use ratatui::prelude::*; + +/// A wrapper around the terminal that handles setting up and tearing down the terminal +/// and provides a helper method to read events from the terminal. +#[derive(Debug)] +pub struct Term { + terminal: Terminal>, +} + +impl Term { + pub fn start() -> Result { + // this size is to match the size of the terminal when running the demo + // using vhs in a 1280x640 sized window (github social preview size) + let options = TerminalOptions { + viewport: Viewport::Fixed(Rect::new(0, 0, 81, 18)), + }; + let terminal = Terminal::with_options(CrosstermBackend::new(io::stdout()), options)?; + enable_raw_mode().context("enable raw mode")?; + stdout() + .execute(EnterAlternateScreen) + .context("enter alternate screen")?; + Ok(Self { terminal }) + } + + pub fn stop() -> Result<()> { + disable_raw_mode().context("disable raw mode")?; + stdout() + .execute(LeaveAlternateScreen) + .context("leave alternate screen")?; + Ok(()) + } + + pub fn next_event(timeout: Duration) -> io::Result> { + if !event::poll(timeout)? { + return Ok(None); + } + let event = event::read()?; + Ok(Some(event)) + } +} + +impl Deref for Term { + type Target = Terminal>; + fn deref(&self) -> &Self::Target { + &self.terminal + } +} + +impl DerefMut for Term { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.terminal + } +} + +impl Drop for Term { + fn drop(&mut self) { + let _ = Term::stop(); + } +} + +/// simple helper method to split an area into multiple sub-areas +pub fn layout(area: Rect, direction: Direction, heights: Vec) -> Rc<[Rect]> { + let constraints = heights + .iter() + .map(|&h| { + if h > 0 { + Constraint::Length(h) + } else { + Constraint::Min(0) + } + }) + .collect_vec(); + Layout::default() + .direction(direction) + .constraints(constraints) + .split(area) +} diff --git a/examples/demo2/theme.rs b/examples/demo2/theme.rs new file mode 100644 index 000000000..73c6d041c --- /dev/null +++ b/examples/demo2/theme.rs @@ -0,0 +1,133 @@ +#![allow(unused)] +use palette::{named::LIGHTGRAY, white_point::B}; +use ratatui::prelude::*; + +pub struct Theme { + pub root: Style, + pub content: Style, + pub app_title: Style, + pub tabs: Style, + pub tabs_selected: Style, + pub borders: Style, + pub description: Style, + pub description_title: Style, + pub key_binding: KeyBinding, + pub logo: Logo, + pub email: Email, + pub traceroute: Traceroute, + pub recipe: Recipe, +} + +pub struct KeyBinding { + pub key: Style, + pub description: Style, +} + +pub struct Logo { + pub rat: Color, + pub rat_eye: Color, + pub rat_eye_alt: Color, + pub term: Color, +} + +pub struct Email { + pub tabs: Style, + pub tabs_selected: Style, + pub inbox: Style, + pub item: Style, + pub selected_item: Style, + pub header: Style, + pub header_value: Style, + pub body: Style, +} + +pub struct Traceroute { + pub header: Style, + pub selected: Style, + pub ping: Style, + pub map: Map, +} + +pub struct Map { + pub style: Style, + pub color: Color, + pub path: Color, + pub source: Color, + pub destination: Color, + pub background_color: Color, +} + +pub struct Recipe { + pub ingredients: Style, + pub ingredients_header: Style, +} + +pub const THEME: Theme = Theme { + root: Style::new().bg(DARK_BLUE), + content: Style::new().bg(DARK_BLUE).fg(LIGHT_GRAY), + app_title: Style::new() + .fg(WHITE) + .bg(DARK_BLUE) + .add_modifier(Modifier::BOLD), + tabs: Style::new().fg(MID_GRAY).bg(DARK_BLUE), + tabs_selected: Style::new().fg(LIGHT_GRAY).bg(DARK_GRAY), + borders: Style::new().fg(LIGHT_GRAY), + description: Style::new().fg(LIGHT_GRAY).bg(DARK_BLUE), + description_title: Style::new().fg(LIGHT_GRAY).add_modifier(Modifier::BOLD), + logo: Logo { + rat: WHITE, + rat_eye: BLACK, + rat_eye_alt: RED, + term: BLACK, + }, + key_binding: KeyBinding { + key: Style::new().fg(BLACK).bg(DARK_GRAY), + description: Style::new().fg(DARK_GRAY).bg(BLACK), + }, + email: Email { + tabs: Style::new().fg(MID_GRAY).bg(BLACK), + tabs_selected: Style::new().fg(BLACK).bg(LIGHT_BLUE), + inbox: Style::new().bg(DARK_BLUE).fg(LIGHT_GRAY), + item: Style::new().fg(LIGHT_GRAY), + selected_item: Style::new().fg(LIGHT_YELLOW), + header: Style::new().add_modifier(Modifier::BOLD), + header_value: Style::new().fg(LIGHT_GRAY), + body: Style::new().fg(LIGHT_GRAY).bg(DARK_GRAY), + }, + traceroute: Traceroute { + header: Style::new() + .bg(DARK_BLUE) + .add_modifier(Modifier::BOLD) + .add_modifier(Modifier::UNDERLINED), + selected: Style::new().fg(LIGHT_YELLOW), + ping: Style::new().fg(WHITE), + map: Map { + style: Style::new().bg(DARK_BLUE), + background_color: DARK_BLUE, + color: LIGHT_GRAY, + path: LIGHT_BLUE, + source: LIGHT_GREEN, + destination: LIGHT_RED, + }, + }, + recipe: Recipe { + ingredients: Style::new().fg(BLACK).bg(DARK_GRAY), + ingredients_header: Style::new() + .fg(LIGHT_GRAY) + .bg(DARK_GRAY) + .add_modifier(Modifier::BOLD) + .add_modifier(Modifier::UNDERLINED), + }, +}; + +const DARK_BLUE: Color = Color::Rgb(16, 24, 48); +const LIGHT_BLUE: Color = Color::Rgb(64, 96, 192); +const LIGHT_YELLOW: Color = Color::Rgb(192, 192, 96); +const LIGHT_GREEN: Color = Color::Rgb(64, 192, 96); +const LIGHT_RED: Color = Color::Rgb(192, 96, 96); +const RED: Color = Color::Indexed(160); +const BLACK: Color = Color::Indexed(232); // not really black, often #080808 +const DARK_GRAY: Color = Color::Indexed(238); +const MID_GRAY: Color = Color::Indexed(244); +const LIGHT_GRAY: Color = Color::Indexed(250); +const WHITE: Color = Color::Indexed(255); // not really white, often #eeeeee diff --git a/examples/demo2/tui.rs b/examples/demo2/tui.rs deleted file mode 100644 index 16aab0c34..000000000 --- a/examples/demo2/tui.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::{ - io::{self, stdout, Stdout}, - rc::Rc, - time::Duration, -}; - -use anyhow::{Context, Result}; -use crossterm::{ - event::{self, Event}, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, -}; -use itertools::Itertools; -use ratatui::prelude::*; - -pub fn create_terminal() -> Result>> { - // this size is to match the size of the terminal when running the demo - // using vhs in a 1280x640 sized window (github social preview size) - let options = TerminalOptions { - viewport: Viewport::Fixed(Rect::new(0, 0, 81, 18)), - // viewport: Viewport::Fullscreen, - }; - let terminal = Terminal::with_options(CrosstermBackend::new(io::stdout()), options)?; - Ok(terminal) -} - -pub fn setup() -> Result<()> { - enable_raw_mode().context("enable raw mode")?; - stdout() - .execute(EnterAlternateScreen) - .context("enter alternate screen")?; - Ok(()) -} - -pub fn restore() -> Result<()> { - disable_raw_mode().context("disable raw mode")?; - stdout() - .execute(LeaveAlternateScreen) - .context("leave alternate screen")?; - Ok(()) -} - -pub fn next_event(timeout: Duration) -> io::Result> { - if !event::poll(timeout)? { - return Ok(None); - } - let event = event::read()?; - Ok(Some(event)) -} - -/// helper method to split an area into multiple sub-areas -pub fn layout(area: Rect, direction: Direction, heights: Vec) -> Rc<[Rect]> { - let constraints = heights - .iter() - .map(|&h| { - if h > 0 { - Constraint::Length(h) - } else { - Constraint::Min(0) - } - }) - .collect_vec(); - Layout::default() - .direction(direction) - .constraints(constraints) - .split(area) -} diff --git a/src/lib.rs b/src/lib.rs index 07c01d41c..b131e6536 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ //! [ratatui](https://github.com/ratatui-org/ratatui) is a library that is all about cooking up terminal user //! interfaces (TUIs). //! -//! ![Demo](https://vhs.charm.sh/vhs-tF0QbuPbtHgUeG0sTVgFr.gif) +//! ![Demo](https://github.com/ratatui-org/ratatui/blob/ed851fb5bb3673b3213d1511e77be82fa6719ea0/examples/demo2-noborders.gif?raw=true) //! //! # Get started //!