diff --git a/Cargo.toml b/Cargo.toml index f8e9f99..55d6195 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,9 @@ name = "demo_legacy" [[example]] name = "simple_legacy" +[[example]] +name = "variants" + [features] default = ["unstable-widget-ref"] unstable-widget-ref = ["ratatui/unstable-widget-ref"] diff --git a/examples/common/item_container.rs b/examples/common/item_container.rs new file mode 100644 index 0000000..8ec0206 --- /dev/null +++ b/examples/common/item_container.rs @@ -0,0 +1,49 @@ +use ratatui::{ + buffer::Buffer, + layout::Rect, + style::{Color, Style, Styled}, + text::Line, + widgets::{Padding, Widget}, +}; +#[allow(dead_code)] +pub type PaddedLine<'a> = ListItemContainer<'a, Line<'a>>; + +pub struct ListItemContainer<'a, W> { + child: W, + block: ratatui::widgets::Block<'a>, + style: Style, +} + +impl<'a, W> ListItemContainer<'a, W> { + pub fn new(child: W, padding: Padding) -> Self { + let block = ratatui::widgets::Block::default().padding(padding); + let style = Style::default().fg(Color::White); + Self { + child, + block, + style, + } + } +} + +impl Styled for ListItemContainer<'_, T> { + type Item = Self; + + fn style(&self) -> Style { + self.style + } + + fn set_style>(mut self, style: S) -> Self::Item { + self.style = style.into(); + self + } +} + +impl Widget for ListItemContainer<'_, W> { + fn render(self, area: Rect, buf: &mut Buffer) { + let inner_area = self.block.inner(area); + buf.set_style(area, self.style); + self.block.render(area, buf); + self.child.render(inner_area, buf); + } +} diff --git a/examples/common/lib.rs b/examples/common/lib.rs index a3da800..bfb09c5 100644 --- a/examples/common/lib.rs +++ b/examples/common/lib.rs @@ -1,14 +1,13 @@ +pub mod item_container; +#[allow(unused_imports)] use crossterm::{ event::{DisableMouseCapture, EnableMouseCapture}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use ratatui::{ - buffer::Buffer, - layout::Rect, prelude::CrosstermBackend, style::{Color, Style}, - text::Line, - widgets::{BorderType, Borders, Padding, StatefulWidget, Widget}, + widgets::{BorderType, Borders, StatefulWidget}, }; use std::{ error::Error, @@ -29,29 +28,6 @@ impl Colors { pub const TEAL: Color = Color::Rgb(0, 128, 128); } -pub struct PaddedLine<'a> { - pub line: Line<'a>, - pub block: ratatui::widgets::Block<'a>, - pub style: Style, -} - -impl<'a> PaddedLine<'a> { - pub fn new(line: Line<'a>, padding: Padding) -> Self { - let block = ratatui::widgets::Block::default().padding(padding); - let style = Style::default().fg(Color::White); - Self { line, block, style } - } -} - -impl Widget for PaddedLine<'_> { - fn render(self, area: Rect, buf: &mut Buffer) { - let inner_area = self.block.inner(area); - buf.set_style(area, self.style); - self.block.render(area, buf); - self.line.render(inner_area, buf); - } -} - pub struct Block; impl Block { pub fn disabled() -> ratatui::widgets::Block<'static> { diff --git a/examples/scroll_config.rs b/examples/scroll_config.rs deleted file mode 100644 index 72c571b..0000000 --- a/examples/scroll_config.rs +++ /dev/null @@ -1,187 +0,0 @@ -#[path = "common/lib.rs"] -mod common; -use common::{Block, Colors, PaddedLine, Result, Terminal}; -use crossterm::event::{self, Event, KeyCode, KeyEventKind}; -use ratatui::{ - buffer::Buffer, - layout::{Alignment, Constraint, Layout, Rect}, - text::Line, - widgets::{Padding, StatefulWidget}, -}; -use tui_widget_list::{ListBuilder, ListState, ListView}; - -fn main() -> Result<()> { - let mut terminal = Terminal::init()?; - App::default().run(&mut terminal).unwrap(); - - Terminal::reset()?; - terminal.show_cursor()?; - - Ok(()) -} - -#[derive(Default, Clone)] -pub struct App; - -#[derive(Default)] -pub struct AppState { - selected_tab: Tab, - scroll_config_state: ListState, - list_state: ListState, -} - -impl AppState { - fn new() -> Self { - let mut scroll_config_state = ListState::default(); - scroll_config_state.select(Some(0)); - Self { - scroll_config_state, - ..AppState::default() - } - } -} - -#[derive(PartialEq, Eq, Default)] -enum Tab { - #[default] - Selection, - List, -} - -impl Tab { - fn next(&mut self) { - match self { - Self::Selection => *self = Tab::List, - Self::List => *self = Tab::Selection, - } - } -} - -#[derive(PartialEq, Eq, Default, Clone)] -enum ScrollConfig { - #[default] - Default, - // Fixed, -} -impl ScrollConfig { - pub const COUNT: usize = 1; - pub fn from_index(index: usize) -> Self { - match index { - // 1 => ScrollConfig::Fixed, - _ => ScrollConfig::Default, - } - } -} - -impl std::fmt::Display for ScrollConfig { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ScrollConfig::Default => write!(f, "Default"), - // ScrollConfig::Fixed => write!(f, "Fixed"), - } - } -} - -impl App { - pub fn run(&self, terminal: &mut Terminal) -> Result<()> { - let mut state = AppState::new(); - loop { - terminal.draw_app(self, &mut state)?; - if Self::handle_events(&mut state)? { - return Ok(()); - } - } - } - - /// Handles app events. - /// Returns true if the app should quit. - fn handle_events(state: &mut AppState) -> Result { - if let Event::Key(key) = event::read()? { - if key.kind == KeyEventKind::Press { - let list_state = match state.selected_tab { - Tab::Selection => &mut state.scroll_config_state, - Tab::List => &mut state.list_state, - }; - match key.code { - KeyCode::Char('q') => return Ok(true), - KeyCode::Up | KeyCode::Char('k') => list_state.previous(), - KeyCode::Down | KeyCode::Char('j') => list_state.next(), - KeyCode::Left | KeyCode::Char('h') | KeyCode::Right | KeyCode::Char('l') => { - state.selected_tab.next() - } - _ => {} - } - } - return Ok(false); - } - return Ok(false); - } -} - -impl StatefulWidget for &App { - type State = AppState; - fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { - use Constraint::{Min, Percentage}; - let [left, right] = Layout::horizontal([Percentage(25), Min(0)]).areas(area); - - // Scroll config selection - let block = match state.selected_tab { - Tab::Selection => Block::selected(), - _ => Block::disabled(), - }; - ConfigListView::new() - .block(block) - .render(left, buf, &mut state.scroll_config_state); - - // List demo - let block = match state.selected_tab { - Tab::List => Block::selected(), - _ => Block::disabled(), - }; - DemoListView::new() - .block(block) - .render(right, buf, &mut state.list_state); - } -} -struct ConfigListView; -impl ConfigListView { - fn new<'a>() -> ListView<'a, PaddedLine<'a>> { - let builder = ListBuilder::new(move |context| { - let config = ScrollConfig::from_index(context.index); - let line = Line::from(format!("{config}")).alignment(Alignment::Center); - let mut item = PaddedLine::new(line, Padding::vertical(1)); - - if context.is_selected { - item.style = item.style.bg(Colors::ORANGE).fg(Colors::CHARCOAL); - }; - - return (item, 3); - }); - - return ListView::new(builder, ScrollConfig::COUNT); - } -} - -struct DemoListView; -impl DemoListView { - fn new<'a>() -> ListView<'a, PaddedLine<'a>> { - let builder = ListBuilder::new(|context| { - let line = Line::from(format!("Item {0}", context.index)).alignment(Alignment::Center); - let mut item = PaddedLine::new(line, Padding::vertical(1)); - - if context.index % 2 == 0 { - item.style = item.style.bg(Colors::CHARCOAL); - } else { - item.style = item.style.bg(Colors::BLACK); - }; - - if context.is_selected { - item.style = item.style.bg(Colors::ORANGE).fg(Colors::CHARCOAL); - }; - - return (item, 3); - }); - - return ListView::new(builder, 100); - } -} diff --git a/examples/tapes/variants.gif b/examples/tapes/variants.gif new file mode 100644 index 0000000..0946154 Binary files /dev/null and b/examples/tapes/variants.gif differ diff --git a/examples/tapes/variants.tape b/examples/tapes/variants.tape new file mode 100644 index 0000000..cbe493b --- /dev/null +++ b/examples/tapes/variants.tape @@ -0,0 +1,32 @@ +# To run this script, install vhs and run `vhs ./examples/tapes/variants.tape` +Output "examples/tapes/variants.gif" +Set Margin 10 +Set Padding 2 +Set BorderRadius 10 +Set Width 2000 +Set Height 1100 +Set PlaybackSpeed 1.0 + +Hide +Type "cargo run --example variants" +Enter +Sleep 0.5s +Show + +Sleep 1.5s +Right@0.5s 1 +Down@0.5s 15 + +Left@0.5s 1 +Down@0.5s 1 + +Right@0.5s 1 +Down@0.5s 20 + +Left@0.5s 1 +Down@0.5s 1 + +Right@0.5s 1 +Down@0.5s 10 + +Sleep 1.5s diff --git a/examples/variants.rs b/examples/variants.rs new file mode 100644 index 0000000..841d159 --- /dev/null +++ b/examples/variants.rs @@ -0,0 +1,155 @@ +#[path = "common/lib.rs"] +mod common; +#[path = "variants/config.rs"] +mod config; +#[path = "variants/horizontal.rs"] +mod horizontal; +#[path = "variants/padded.rs"] +mod padded; +#[path = "variants/simple.rs"] +mod simple; +use common::{Block, Result, Terminal}; +use config::{Controls, Variant, VariantsListView}; +use crossterm::event::{self, Event, KeyCode, KeyEventKind}; +use horizontal::HorizontalListView; +use padded::PaddedListView; +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Layout, Rect}, + widgets::{StatefulWidget, Widget}, +}; +use simple::SimpleListView; +use tui_widget_list::ListState; + +fn main() -> Result<()> { + let mut terminal = Terminal::init()?; + App::default().run(&mut terminal).unwrap(); + + Terminal::reset()?; + terminal.show_cursor()?; + + Ok(()) +} + +#[derive(Default, Clone)] +pub struct App; + +#[derive(Default)] +pub struct AppState { + selected_tab: Tab, + variant_state: ListState, + list_state: ListState, +} + +impl AppState { + fn new() -> Self { + let mut scroll_config_state = ListState::default(); + scroll_config_state.select(Some(0)); + Self { + variant_state: scroll_config_state, + ..AppState::default() + } + } +} + +#[derive(PartialEq, Eq, Default)] +enum Tab { + #[default] + Selection, + List, +} + +impl Tab { + fn next(&mut self) { + match self { + Self::Selection => *self = Tab::List, + Self::List => *self = Tab::Selection, + } + } +} + +impl App { + pub fn run(&self, terminal: &mut Terminal) -> Result<()> { + let mut state = AppState::new(); + loop { + terminal.draw_app(self, &mut state)?; + if Self::handle_events(&mut state)? { + return Ok(()); + } + } + } + + /// Handles app events. + /// Returns true if the app should quit. + fn handle_events(state: &mut AppState) -> Result { + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press { + let list_state = match state.selected_tab { + Tab::Selection => &mut state.variant_state, + Tab::List => &mut state.list_state, + }; + match key.code { + KeyCode::Char('q') => return Ok(true), + KeyCode::Up | KeyCode::Char('k') => list_state.previous(), + KeyCode::Down | KeyCode::Char('j') => list_state.next(), + KeyCode::Tab + | KeyCode::Left + | KeyCode::Char('h') + | KeyCode::Right + | KeyCode::Char('l') => { + state.list_state.select(None); + state.selected_tab.next() + } + _ => {} + } + } + return Ok(false); + } + return Ok(false); + } +} + +impl StatefulWidget for &App { + type State = AppState; + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + use Constraint::{Length, Min, Percentage}; + + let [top, main] = Layout::vertical([Length(1), Min(0)]).areas(area); + let [left, right] = Layout::horizontal([Percentage(25), Min(0)]).areas(main); + + // Key mappings + Controls::default().render(top, buf); + + // Scroll config selection + let block = match state.selected_tab { + Tab::Selection => Block::selected(), + _ => Block::disabled(), + }; + VariantsListView::new() + .block(block) + .render(left, buf, &mut state.variant_state); + + // List demo + let block = match state.selected_tab { + Tab::List => Block::selected(), + _ => Block::disabled(), + }; + match Variant::from_index(state.variant_state.selected.unwrap_or(0)) { + Variant::Simple => { + SimpleListView::new() + .block(block) + .render(right, buf, &mut state.list_state) + } + Variant::Padded => { + PaddedListView::new() + .block(block) + .render(right, buf, &mut state.list_state) + } + Variant::Horizontal => { + HorizontalListView::new() + .block(block) + .render(right, buf, &mut state.list_state) + } + }; + } +} diff --git a/examples/variants/config.rs b/examples/variants/config.rs new file mode 100644 index 0000000..94191aa --- /dev/null +++ b/examples/variants/config.rs @@ -0,0 +1,65 @@ +use ratatui::{ + layout::Alignment, + style::Stylize, + text::Line, + widgets::{Padding, Widget}, +}; +use tui_widget_list::{ListBuilder, ListView}; + +use crate::common::{item_container::PaddedLine, Colors}; + +#[derive(PartialEq, Eq, Default, Clone)] +pub enum Variant { + #[default] + Simple, + Padded, + Horizontal, +} + +impl Variant { + pub const COUNT: usize = 3; + pub fn from_index(index: usize) -> Self { + match index { + 1 => Variant::Padded, + 2 => Variant::Horizontal, + _ => Variant::Simple, + } + } +} + +impl std::fmt::Display for Variant { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Variant::Simple => write!(f, "Simple"), + Variant::Padded => write!(f, "Padded"), + Variant::Horizontal => write!(f, "Horizontal"), + } + } +} + +pub struct VariantsListView; +impl VariantsListView { + pub fn new<'a>() -> ListView<'a, PaddedLine<'a>> { + let builder = ListBuilder::new(move |context| { + let config = Variant::from_index(context.index); + let line = Line::from(format!("{config}")).alignment(Alignment::Center); + let mut item = PaddedLine::new(line, Padding::vertical(1)); + + if context.is_selected { + item = item.bg(Colors::ORANGE).fg(Colors::CHARCOAL); + }; + + return (item, 3); + }); + + return ListView::new(builder, Variant::COUNT); + } +} + +#[derive(Default)] +pub struct Controls; +impl Widget for Controls { + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { + Line::from("k: Up | j: Down | Tab: Left/Right").render(area, buf); + } +} diff --git a/examples/variants/horizontal.rs b/examples/variants/horizontal.rs new file mode 100644 index 0000000..2a91362 --- /dev/null +++ b/examples/variants/horizontal.rs @@ -0,0 +1,25 @@ +use crate::common::{item_container::PaddedLine, Colors}; +use ratatui::{layout::Alignment, style::Stylize, text::Line, widgets::Padding}; +use tui_widget_list::{ListBuilder, ListView, ScrollAxis}; + +pub(crate) struct HorizontalListView; + +impl HorizontalListView { + pub(crate) fn new<'a>() -> ListView<'a, PaddedLine<'a>> { + let builder = ListBuilder::new(|context| { + let mut line = PaddedLine::new( + Line::from(format!("Item {0}", context.index)).alignment(Alignment::Center), + Padding::vertical(1), + ); + line = match context.is_selected { + true => line.bg(Colors::ORANGE).fg(Colors::CHARCOAL), + false if context.index % 2 == 0 => line.bg(Colors::CHARCOAL), + false => line.bg(Colors::BLACK), + }; + + return (line, 20); + }); + + return ListView::new(builder, 10).scroll_axis(ScrollAxis::Horizontal); + } +} diff --git a/examples/variants/padded.rs b/examples/variants/padded.rs new file mode 100644 index 0000000..ade293b --- /dev/null +++ b/examples/variants/padded.rs @@ -0,0 +1,25 @@ +use crate::common::{item_container::PaddedLine, Colors}; +use ratatui::{layout::Alignment, style::Stylize, text::Line, widgets::Padding}; +use tui_widget_list::{ListBuilder, ListView}; + +pub(crate) struct PaddedListView; + +impl PaddedListView { + pub(crate) fn new<'a>() -> ListView<'a, PaddedLine<'a>> { + let builder = ListBuilder::new(|context| { + let mut line = PaddedLine::new( + Line::from(format!("Item {0}", context.index)).alignment(Alignment::Center), + Padding::vertical(1), + ); + line = match context.is_selected { + true => line.bg(Colors::ORANGE).fg(Colors::CHARCOAL), + false if context.index % 2 == 0 => line.bg(Colors::CHARCOAL), + false => line.bg(Colors::BLACK), + }; + + return (line, 3); + }); + + return ListView::new(builder, 50); + } +} diff --git a/examples/variants/simple.rs b/examples/variants/simple.rs new file mode 100644 index 0000000..48a88a4 --- /dev/null +++ b/examples/variants/simple.rs @@ -0,0 +1,24 @@ +use ratatui::{layout::Alignment, style::Stylize, text::Line}; +use tui_widget_list::{ListBuilder, ListView}; + +use crate::common::Colors; + +pub(crate) struct SimpleListView; + +impl SimpleListView { + pub(crate) fn new<'a>() -> ListView<'a, Line<'a>> { + let builder = ListBuilder::new(|context| { + let mut line = + Line::from(format!("Item {0}", context.index)).alignment(Alignment::Center); + line = match context.is_selected { + true => line.bg(Colors::ORANGE).fg(Colors::CHARCOAL), + false if context.index % 2 == 0 => line.bg(Colors::CHARCOAL).fg(Colors::WHITE), + false => line.bg(Colors::BLACK).fg(Colors::WHITE), + }; + + return (line, 1); + }); + + return ListView::new(builder, 50); + } +}