diff --git a/examples/barchart.rs b/examples/barchart.rs index 733a732..23a5bfb 100644 --- a/examples/barchart.rs +++ b/examples/barchart.rs @@ -10,7 +10,7 @@ use termion::screen::AlternateScreen; use tui::backend::TermionBackend; use tui::layout::{Constraint, Direction, Layout}; use tui::style::{Color, Modifier, Style}; -use tui::widgets::{BarChart, Block, Borders, Widget}; +use tui::widgets::{BarChart, Block, Borders}; use tui::Terminal; use crate::util::event::{Event, Events}; @@ -79,36 +79,37 @@ fn main() -> Result<(), failure::Error> { .margin(2) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .split(f.size()); - BarChart::default() + let barchart = BarChart::default() .block(Block::default().title("Data1").borders(Borders::ALL)) .data(&app.data) .bar_width(9) .style(Style::default().fg(Color::Yellow)) - .value_style(Style::default().fg(Color::Black).bg(Color::Yellow)) - .render(&mut f, chunks[0]); - { - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(chunks[1]); - BarChart::default() - .block(Block::default().title("Data2").borders(Borders::ALL)) - .data(&app.data) - .bar_width(5) - .bar_gap(3) - .style(Style::default().fg(Color::Green)) - .value_style(Style::default().bg(Color::Green).modifier(Modifier::BOLD)) - .render(&mut f, chunks[0]); - BarChart::default() - .block(Block::default().title("Data3").borders(Borders::ALL)) - .data(&app.data) - .style(Style::default().fg(Color::Red)) - .bar_width(7) - .bar_gap(0) - .value_style(Style::default().bg(Color::Red)) - .label_style(Style::default().fg(Color::Cyan).modifier(Modifier::ITALIC)) - .render(&mut f, chunks[1]); - } + .value_style(Style::default().fg(Color::Black).bg(Color::Yellow)); + f.render_widget(barchart, chunks[0]); + + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(chunks[1]); + + let barchart = BarChart::default() + .block(Block::default().title("Data2").borders(Borders::ALL)) + .data(&app.data) + .bar_width(5) + .bar_gap(3) + .style(Style::default().fg(Color::Green)) + .value_style(Style::default().bg(Color::Green).modifier(Modifier::BOLD)); + f.render_widget(barchart, chunks[0]); + + let barchart = BarChart::default() + .block(Block::default().title("Data3").borders(Borders::ALL)) + .data(&app.data) + .style(Style::default().fg(Color::Red)) + .bar_width(7) + .bar_gap(0) + .value_style(Style::default().bg(Color::Red)) + .label_style(Style::default().fg(Color::Cyan).modifier(Modifier::ITALIC)); + f.render_widget(barchart, chunks[1]); })?; match events.next()? { diff --git a/examples/block.rs b/examples/block.rs index 2dc65d1..e5090a6 100644 --- a/examples/block.rs +++ b/examples/block.rs @@ -9,7 +9,7 @@ use termion::screen::AlternateScreen; use tui::backend::TermionBackend; use tui::layout::{Constraint, Direction, Layout}; use tui::style::{Color, Modifier, Style}; -use tui::widgets::{Block, BorderType, Borders, Widget}; +use tui::widgets::{Block, BorderType, Borders}; use tui::Terminal; use crate::util::event::{Event, Events}; @@ -32,11 +32,11 @@ fn main() -> Result<(), failure::Error> { // Just draw the block and the group on the same area and build the group // with at least a margin of 1 let size = f.size(); - Block::default() + let block = Block::default() .borders(Borders::ALL) .title("Main block with round corners") - .border_type(BorderType::Rounded) - .render(&mut f, size); + .border_type(BorderType::Rounded); + f.render_widget(block, size); let chunks = Layout::default() .direction(Direction::Vertical) .margin(4) @@ -47,36 +47,33 @@ fn main() -> Result<(), failure::Error> { .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .split(chunks[0]); - Block::default() + let block = Block::default() .title("With background") .title_style(Style::default().fg(Color::Yellow)) - .style(Style::default().bg(Color::Green)) - .render(&mut f, chunks[0]); - Block::default() + .style(Style::default().bg(Color::Green)); + f.render_widget(block, chunks[0]); + let title_style = Style::default() + .fg(Color::White) + .bg(Color::Red) + .modifier(Modifier::BOLD); + let block = Block::default() .title("Styled title") - .title_style( - Style::default() - .fg(Color::White) - .bg(Color::Red) - .modifier(Modifier::BOLD), - ) - .render(&mut f, chunks[1]); + .title_style(title_style); + f.render_widget(block, chunks[1]); } { let chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .split(chunks[1]); - Block::default() - .title("With borders") - .borders(Borders::ALL) - .render(&mut f, chunks[0]); - Block::default() - .title("With styled and double borders") + let block = Block::default().title("With borders").borders(Borders::ALL); + f.render_widget(block, chunks[0]); + let block = Block::default() + .title("With styled borders and doubled borders") .border_style(Style::default().fg(Color::Cyan)) .borders(Borders::LEFT | Borders::RIGHT) - .border_type(BorderType::Double) - .render(&mut f, chunks[1]); + .border_type(BorderType::Double); + f.render_widget(block, chunks[1]); } })?; diff --git a/examples/canvas.rs b/examples/canvas.rs index 8628e76..ae45681 100644 --- a/examples/canvas.rs +++ b/examples/canvas.rs @@ -12,7 +12,7 @@ use tui::backend::TermionBackend; use tui::layout::{Constraint, Direction, Layout, Rect}; use tui::style::Color; use tui::widgets::canvas::{Canvas, Map, MapResolution, Rectangle}; -use tui::widgets::{Block, Borders, Widget}; +use tui::widgets::{Block, Borders}; use tui::Terminal; use crate::util::event::{Config, Event, Events}; @@ -91,7 +91,7 @@ fn main() -> Result<(), failure::Error> { .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .split(f.size()); - Canvas::default() + let canvas = Canvas::default() .block(Block::default().borders(Borders::ALL).title("World")) .paint(|ctx| { ctx.draw(&Map { @@ -101,9 +101,9 @@ fn main() -> Result<(), failure::Error> { ctx.print(app.x, -app.y, "You are here", Color::Yellow); }) .x_bounds([-180.0, 180.0]) - .y_bounds([-90.0, 90.0]) - .render(&mut f, chunks[0]); - Canvas::default() + .y_bounds([-90.0, 90.0]); + f.render_widget(canvas, chunks[0]); + let canvas = Canvas::default() .block(Block::default().borders(Borders::ALL).title("Pong")) .paint(|ctx| { ctx.draw(&Rectangle { @@ -112,8 +112,8 @@ fn main() -> Result<(), failure::Error> { }); }) .x_bounds([10.0, 110.0]) - .y_bounds([10.0, 110.0]) - .render(&mut f, chunks[1]); + .y_bounds([10.0, 110.0]); + f.render_widget(canvas, chunks[1]); })?; match events.next()? { diff --git a/examples/chart.rs b/examples/chart.rs index aae717e..f213947 100644 --- a/examples/chart.rs +++ b/examples/chart.rs @@ -9,7 +9,7 @@ use termion::raw::IntoRawMode; use termion::screen::AlternateScreen; use tui::backend::TermionBackend; use tui::style::{Color, Modifier, Style}; -use tui::widgets::{Axis, Block, Borders, Chart, Dataset, Marker, Widget}; +use tui::widgets::{Axis, Block, Borders, Chart, Dataset, Marker}; use tui::Terminal; use crate::util::event::{Event, Events}; @@ -69,7 +69,24 @@ fn main() -> Result<(), failure::Error> { loop { terminal.draw(|mut f| { let size = f.size(); - Chart::default() + let x_labels = [ + format!("{}", app.window[0]), + format!("{}", (app.window[0] + app.window[1]) / 2.0), + format!("{}", app.window[1]), + ]; + let datasets = [ + Dataset::default() + .name("data2") + .marker(Marker::Dot) + .style(Style::default().fg(Color::Cyan)) + .data(&app.data1), + Dataset::default() + .name("data3") + .marker(Marker::Braille) + .style(Style::default().fg(Color::Yellow)) + .data(&app.data2), + ]; + let chart = Chart::default() .block( Block::default() .title("Chart") @@ -82,11 +99,7 @@ fn main() -> Result<(), failure::Error> { .style(Style::default().fg(Color::Gray)) .labels_style(Style::default().modifier(Modifier::ITALIC)) .bounds(app.window) - .labels(&[ - &format!("{}", app.window[0]), - &format!("{}", (app.window[0] + app.window[1]) / 2.0), - &format!("{}", app.window[1]), - ]), + .labels(&x_labels), ) .y_axis( Axis::default() @@ -96,19 +109,8 @@ fn main() -> Result<(), failure::Error> { .bounds([-20.0, 20.0]) .labels(&["-20", "0", "20"]), ) - .datasets(&[ - Dataset::default() - .name("data2") - .marker(Marker::Dot) - .style(Style::default().fg(Color::Cyan)) - .data(&app.data1), - Dataset::default() - .name("data3") - .marker(Marker::Braille) - .style(Style::default().fg(Color::Yellow)) - .data(&app.data2), - ]) - .render(&mut f, size); + .datasets(&datasets); + f.render_widget(chart, size); })?; match events.next()? { diff --git a/examples/crossterm_demo.rs b/examples/crossterm_demo.rs index 03aed93..f3ac382 100644 --- a/examples/crossterm_demo.rs +++ b/examples/crossterm_demo.rs @@ -3,25 +3,21 @@ mod demo; #[allow(dead_code)] mod util; +use crate::demo::{ui, App}; +use crossterm::{ + event::{self, Event as CEvent, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; use std::{ io::{stdout, Write}, sync::mpsc, thread, time::Duration, }; - -use crossterm::{ - event::{self, Event as CEvent, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen}, -}; - use structopt::StructOpt; use tui::{backend::CrosstermBackend, Terminal}; -use crate::demo::{ui, App}; -use crossterm::terminal::LeaveAlternateScreen; - enum Event { Input(I), Tick, @@ -70,7 +66,7 @@ fn main() -> Result<(), failure::Error> { terminal.clear()?; loop { - ui::draw(&mut terminal, &app)?; + terminal.draw(|mut f| ui::draw(&mut f, &mut app))?; match rx.recv()? { Event::Input(event) => match event.code { KeyCode::Char('q') => { diff --git a/examples/curses_demo.rs b/examples/curses_demo.rs index eed4b90..e0cb83b 100644 --- a/examples/curses_demo.rs +++ b/examples/curses_demo.rs @@ -38,7 +38,7 @@ fn main() -> Result<(), failure::Error> { let mut last_tick = Instant::now(); let tick_rate = Duration::from_millis(cli.tick_rate); loop { - ui::draw(&mut terminal, &app)?; + terminal.draw(|mut f| ui::draw(&mut f, &mut app))?; match terminal.backend_mut().get_curses_mut().get_input() { Some(input) => { match input { diff --git a/examples/custom_widget.rs b/examples/custom_widget.rs index c3a9a0e..f4c4744 100644 --- a/examples/custom_widget.rs +++ b/examples/custom_widget.rs @@ -27,13 +27,13 @@ impl<'a> Default for Label<'a> { } impl<'a> Widget for Label<'a> { - fn draw(&mut self, area: Rect, buf: &mut Buffer) { + fn render(self, area: Rect, buf: &mut Buffer) { buf.set_string(area.left(), area.top(), self.text, Style::default()); } } impl<'a> Label<'a> { - fn text(&mut self, text: &'a str) -> &mut Label<'a> { + fn text(mut self, text: &'a str) -> Label<'a> { self.text = text; self } @@ -52,7 +52,8 @@ fn main() -> Result<(), failure::Error> { loop { terminal.draw(|mut f| { let size = f.size(); - Label::default().text("Test").render(&mut f, size); + let label = Label::default().text("Test"); + f.render_widget(label, size); })?; match events.next()? { diff --git a/examples/demo/app.rs b/examples/demo/app.rs index 6ffba07..158a3b0 100644 --- a/examples/demo/app.rs +++ b/examples/demo/app.rs @@ -1,4 +1,4 @@ -use crate::util::{RandomSignal, SinSignal, TabsState}; +use crate::util::{RandomSignal, SinSignal, StatefulList, TabsState}; const TASKS: [&'static str; 24] = [ "Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10", @@ -96,27 +96,6 @@ impl Signals { } } -pub struct ListState { - pub items: Vec, - pub selected: usize, -} - -impl ListState { - fn new(items: Vec) -> ListState { - ListState { items, selected: 0 } - } - fn select_previous(&mut self) { - if self.selected > 0 { - self.selected -= 1; - } - } - fn select_next(&mut self) { - if self.selected < self.items.len() - 1 { - self.selected += 1 - } - } -} - pub struct Server<'a> { pub name: &'a str, pub location: &'a str, @@ -131,8 +110,8 @@ pub struct App<'a> { pub show_chart: bool, pub progress: u16, pub sparkline: Signal, - pub tasks: ListState<&'a str>, - pub logs: ListState<(&'a str, &'a str)>, + pub tasks: StatefulList<&'a str>, + pub logs: StatefulList<(&'a str, &'a str)>, pub signals: Signals, pub barchart: Vec<(&'a str, u64)>, pub servers: Vec>, @@ -157,8 +136,8 @@ impl<'a> App<'a> { points: sparkline_points, tick_rate: 1, }, - tasks: ListState::new(TASKS.to_vec()), - logs: ListState::new(LOGS.to_vec()), + tasks: StatefulList::with_items(TASKS.to_vec()), + logs: StatefulList::with_items(LOGS.to_vec()), signals: Signals { sin1: Signal { source: sin_signal, @@ -203,11 +182,11 @@ impl<'a> App<'a> { } pub fn on_up(&mut self) { - self.tasks.select_previous(); + self.tasks.previous(); } pub fn on_down(&mut self) { - self.tasks.select_next(); + self.tasks.next(); } pub fn on_right(&mut self) { diff --git a/examples/demo/ui.rs b/examples/demo/ui.rs index 9d83e73..9b749f4 100644 --- a/examples/demo/ui.rs +++ b/examples/demo/ui.rs @@ -1,38 +1,34 @@ -use std::io; - use tui::backend::Backend; use tui::layout::{Constraint, Direction, Layout, Rect}; use tui::style::{Color, Modifier, Style}; use tui::widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle}; use tui::widgets::{ - Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, List, Marker, Paragraph, Row, - SelectableList, Sparkline, Table, Tabs, Text, Widget, + Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, List, Marker, Paragraph, Row, Sparkline, + Table, Tabs, Text, }; -use tui::{Frame, Terminal}; +use tui::Frame; use crate::demo::App; -pub fn draw(terminal: &mut Terminal, app: &App) -> Result<(), io::Error> { - terminal.draw(|mut f| { - let chunks = Layout::default() - .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) - .split(f.size()); - Tabs::default() - .block(Block::default().borders(Borders::ALL).title(app.title)) - .titles(&app.tabs.titles) - .style(Style::default().fg(Color::Green)) - .highlight_style(Style::default().fg(Color::Yellow)) - .select(app.tabs.index) - .render(&mut f, chunks[0]); - match app.tabs.index { - 0 => draw_first_tab(&mut f, &app, chunks[1]), - 1 => draw_second_tab(&mut f, &app, chunks[1]), - _ => {} - }; - }) +pub fn draw(f: &mut Frame, app: &mut App) { + let chunks = Layout::default() + .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) + .split(f.size()); + let tabs = Tabs::default() + .block(Block::default().borders(Borders::ALL).title(app.title)) + .titles(&app.tabs.titles) + .style(Style::default().fg(Color::Green)) + .highlight_style(Style::default().fg(Color::Yellow)) + .select(app.tabs.index); + f.render_widget(tabs, chunks[0]); + match app.tabs.index { + 0 => draw_first_tab(f, app, chunks[1]), + 1 => draw_second_tab(f, app, chunks[1]), + _ => {} + }; } -fn draw_first_tab(f: &mut Frame, app: &App, area: Rect) +fn draw_first_tab(f: &mut Frame, app: &mut App, area: Rect) where B: Backend, { @@ -51,7 +47,7 @@ where draw_text(f, chunks[2]); } -fn draw_gauges(f: &mut Frame, app: &App, area: Rect) +fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) where B: Backend, { @@ -59,11 +55,11 @@ where .constraints([Constraint::Length(2), Constraint::Length(3)].as_ref()) .margin(1) .split(area); - Block::default() - .borders(Borders::ALL) - .title("Graphs") - .render(f, area); - Gauge::default() + let block = Block::default().borders(Borders::ALL).title("Graphs"); + f.render_widget(block, area); + + let label = format!("{} / 100", app.progress); + let gauge = Gauge::default() .block(Block::default().title("Gauge:")) .style( Style::default() @@ -71,17 +67,18 @@ where .bg(Color::Black) .modifier(Modifier::ITALIC | Modifier::BOLD), ) - .label(&format!("{} / 100", app.progress)) - .percent(app.progress) - .render(f, chunks[0]); - Sparkline::default() + .label(&label) + .percent(app.progress); + f.render_widget(gauge, chunks[0]); + + let sparkline = Sparkline::default() .block(Block::default().title("Sparkline:")) .style(Style::default().fg(Color::Green)) - .data(&app.sparkline.points) - .render(f, chunks[1]); + .data(&app.sparkline.points); + f.render_widget(sparkline, chunks[1]); } -fn draw_charts(f: &mut Frame, app: &App, area: Rect) +fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) where B: Backend, { @@ -103,18 +100,21 @@ where .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .direction(Direction::Horizontal) .split(chunks[0]); - SelectableList::default() + + // Draw tasks + let tasks = app.tasks.items.iter().map(|i| Text::raw(*i)); + let tasks = List::new(tasks) .block(Block::default().borders(Borders::ALL).title("List")) - .items(&app.tasks.items) - .select(Some(app.tasks.selected)) .highlight_style(Style::default().fg(Color::Yellow).modifier(Modifier::BOLD)) - .highlight_symbol(">") - .render(f, chunks[0]); + .highlight_symbol(">"); + f.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state); + + // Draw logs let info_style = Style::default().fg(Color::White); let warning_style = Style::default().fg(Color::Yellow); let error_style = Style::default().fg(Color::Magenta); let critical_style = Style::default().fg(Color::Red); - let events = app.logs.items.iter().map(|&(evt, level)| { + let logs = app.logs.items.iter().map(|&(evt, level)| { Text::styled( format!("{}: {}", level, evt), match level { @@ -125,11 +125,11 @@ where }, ) }); - List::new(events) - .block(Block::default().borders(Borders::ALL).title("List")) - .render(f, chunks[1]); + let logs = List::new(logs).block(Block::default().borders(Borders::ALL).title("List")); + f.render_stateful_widget(logs, chunks[1], &mut app.logs.state); } - BarChart::default() + + let barchart = BarChart::default() .block(Block::default().borders(Borders::ALL).title("Bar chart")) .data(&app.barchart) .bar_width(3) @@ -141,11 +141,28 @@ where .modifier(Modifier::ITALIC), ) .label_style(Style::default().fg(Color::Yellow)) - .style(Style::default().fg(Color::Green)) - .render(f, chunks[1]); + .style(Style::default().fg(Color::Green)); + f.render_widget(barchart, chunks[1]); } if app.show_chart { - Chart::default() + let x_labels = [ + format!("{}", app.signals.window[0]), + format!("{}", (app.signals.window[0] + app.signals.window[1]) / 2.0), + format!("{}", app.signals.window[1]), + ]; + let datasets = [ + Dataset::default() + .name("data2") + .marker(Marker::Dot) + .style(Style::default().fg(Color::Cyan)) + .data(&app.signals.sin1.points), + Dataset::default() + .name("data3") + .marker(Marker::Braille) + .style(Style::default().fg(Color::Yellow)) + .data(&app.signals.sin2.points), + ]; + let chart = Chart::default() .block( Block::default() .title("Chart") @@ -158,11 +175,7 @@ where .style(Style::default().fg(Color::Gray)) .labels_style(Style::default().modifier(Modifier::ITALIC)) .bounds(app.signals.window) - .labels(&[ - &format!("{}", app.signals.window[0]), - &format!("{}", (app.signals.window[0] + app.signals.window[1]) / 2.0), - &format!("{}", app.signals.window[1]), - ]), + .labels(&x_labels), ) .y_axis( Axis::default() @@ -172,19 +185,8 @@ where .bounds([-20.0, 20.0]) .labels(&["-20", "0", "20"]), ) - .datasets(&[ - Dataset::default() - .name("data2") - .marker(Marker::Dot) - .style(Style::default().fg(Color::Cyan)) - .data(&app.signals.sin1.points), - Dataset::default() - .name("data3") - .marker(Marker::Braille) - .style(Style::default().fg(Color::Yellow)) - .data(&app.signals.sin2.points), - ]) - .render(f, chunks[1]); + .datasets(&datasets); + f.render_widget(chart, chunks[1]); } } @@ -209,18 +211,15 @@ where Text::styled("text", Style::default().modifier(Modifier::UNDERLINED)), Text::raw(".\nOne more thing is that it should display unicode characters: 10€") ]; - Paragraph::new(text.iter()) - .block( - Block::default() - .borders(Borders::ALL) - .title("Footer") - .title_style(Style::default().fg(Color::Magenta).modifier(Modifier::BOLD)), - ) - .wrap(true) - .render(f, area); + let block = Block::default() + .borders(Borders::ALL) + .title("Footer") + .title_style(Style::default().fg(Color::Magenta).modifier(Modifier::BOLD)); + let paragraph = Paragraph::new(text.iter()).block(block).wrap(true); + f.render_widget(paragraph, area); } -fn draw_second_tab(f: &mut Frame, app: &App, area: Rect) +fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) where B: Backend, { @@ -241,17 +240,17 @@ where }; Row::StyledData(vec![s.name, s.location, s.status].into_iter(), style) }); - Table::new(header.iter(), rows) + let table = Table::new(header.iter(), rows) .block(Block::default().title("Servers").borders(Borders::ALL)) .header_style(Style::default().fg(Color::Yellow)) .widths(&[ Constraint::Length(15), Constraint::Length(15), Constraint::Length(10), - ]) - .render(f, chunks[0]); + ]); + f.render_widget(table, chunks[0]); - Canvas::default() + let map = Canvas::default() .block(Block::default().title("World").borders(Borders::ALL)) .paint(|ctx| { ctx.draw(&Map { @@ -289,6 +288,6 @@ where } }) .x_bounds([-180.0, 180.0]) - .y_bounds([-90.0, 90.0]) - .render(f, chunks[1]); + .y_bounds([-90.0, 90.0]); + f.render_widget(map, chunks[1]); } diff --git a/examples/gauge.rs b/examples/gauge.rs index fed39a0..8327def 100644 --- a/examples/gauge.rs +++ b/examples/gauge.rs @@ -10,7 +10,7 @@ use termion::screen::AlternateScreen; use tui::backend::TermionBackend; use tui::layout::{Constraint, Direction, Layout}; use tui::style::{Color, Modifier, Style}; -use tui::widgets::{Block, Borders, Gauge, Widget}; +use tui::widgets::{Block, Borders, Gauge}; use tui::Terminal; use crate::util::event::{Event, Events}; @@ -81,28 +81,33 @@ fn main() -> Result<(), failure::Error> { ) .split(f.size()); - Gauge::default() + let gauge = Gauge::default() .block(Block::default().title("Gauge1").borders(Borders::ALL)) .style(Style::default().fg(Color::Yellow)) - .percent(app.progress1) - .render(&mut f, chunks[0]); - Gauge::default() + .percent(app.progress1); + f.render_widget(gauge, chunks[0]); + + let label = format!("{}/100", app.progress2); + let gauge = Gauge::default() .block(Block::default().title("Gauge2").borders(Borders::ALL)) .style(Style::default().fg(Color::Magenta).bg(Color::Green)) .percent(app.progress2) - .label(&format!("{}/100", app.progress2)) - .render(&mut f, chunks[1]); - Gauge::default() + .label(&label); + f.render_widget(gauge, chunks[1]); + + let gauge = Gauge::default() .block(Block::default().title("Gauge3").borders(Borders::ALL)) .style(Style::default().fg(Color::Yellow)) - .ratio(app.progress3) - .render(&mut f, chunks[2]); - Gauge::default() + .ratio(app.progress3); + f.render_widget(gauge, chunks[2]); + + let label = format!("{}/100", app.progress2); + let gauge = Gauge::default() .block(Block::default().title("Gauge4").borders(Borders::ALL)) .style(Style::default().fg(Color::Cyan).modifier(Modifier::ITALIC)) .percent(app.progress4) - .label(&format!("{}/100", app.progress2)) - .render(&mut f, chunks[3]); + .label(&label); + f.render_widget(gauge, chunks[3]); })?; match events.next()? { diff --git a/examples/layout.rs b/examples/layout.rs index 05dc4cb..bb3e1ad 100644 --- a/examples/layout.rs +++ b/examples/layout.rs @@ -9,7 +9,7 @@ use termion::raw::IntoRawMode; use termion::screen::AlternateScreen; use tui::backend::TermionBackend; use tui::layout::{Constraint, Direction, Layout}; -use tui::widgets::{Block, Borders, Widget}; +use tui::widgets::{Block, Borders}; use tui::Terminal; use crate::util::event::{Event, Events}; @@ -39,14 +39,10 @@ fn main() -> Result<(), failure::Error> { ) .split(f.size()); - Block::default() - .title("Block") - .borders(Borders::ALL) - .render(&mut f, chunks[0]); - Block::default() - .title("Block 2") - .borders(Borders::ALL) - .render(&mut f, chunks[2]); + let block = Block::default().title("Block").borders(Borders::ALL); + f.render_widget(block, chunks[0]); + let block = Block::default().title("Block 2").borders(Borders::ALL); + f.render_widget(block, chunks[2]); })?; match events.next()? { diff --git a/examples/list.rs b/examples/list.rs index 8a3ee19..5729b8a 100644 --- a/examples/list.rs +++ b/examples/list.rs @@ -10,14 +10,16 @@ use termion::screen::AlternateScreen; use tui::backend::TermionBackend; use tui::layout::{Constraint, Corner, Direction, Layout}; use tui::style::{Color, Modifier, Style}; -use tui::widgets::{Block, Borders, List, SelectableList, Text, Widget}; +use tui::widgets::{Block, Borders, List, Text}; use tui::Terminal; -use crate::util::event::{Event, Events}; +use crate::util::{ + event::{Event, Events}, + StatefulList, +}; struct App<'a> { - items: Vec<&'a str>, - selected: Option, + items: StatefulList<&'a str>, events: Vec<(&'a str, &'a str)>, info_style: Style, warning_style: Style, @@ -28,12 +30,11 @@ struct App<'a> { impl<'a> App<'a> { fn new() -> App<'a> { App { - items: vec![ - "Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", - "Item10", "Item11", "Item12", "Item13", "Item14", "Item15", "Item16", "Item17", - "Item18", "Item19", "Item20", "Item21", "Item22", "Item23", "Item24", - ], - selected: None, + items: StatefulList::with_items(vec![ + "Item0", "Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", + "Item9", "Item10", "Item11", "Item12", "Item13", "Item14", "Item15", "Item16", + "Item17", "Item18", "Item19", "Item20", "Item21", "Item22", "Item23", "Item24", + ]), events: vec![ ("Event1", "INFO"), ("Event2", "INFO"), @@ -97,31 +98,30 @@ fn main() -> Result<(), failure::Error> { .split(f.size()); let style = Style::default().fg(Color::Black).bg(Color::White); - SelectableList::default() + + let items = app.items.items.iter().map(|i| Text::raw(*i)); + let items = List::new(items) .block(Block::default().borders(Borders::ALL).title("List")) - .items(&app.items) - .select(app.selected) .style(style) .highlight_style(style.fg(Color::LightGreen).modifier(Modifier::BOLD)) - .highlight_symbol(">") - .render(&mut f, chunks[0]); - { - let events = app.events.iter().map(|&(evt, level)| { - Text::styled( - format!("{}: {}", level, evt), - match level { - "ERROR" => app.error_style, - "CRITICAL" => app.critical_style, - "WARNING" => app.warning_style, - _ => app.info_style, - }, - ) - }); - List::new(events) - .block(Block::default().borders(Borders::ALL).title("List")) - .start_corner(Corner::BottomLeft) - .render(&mut f, chunks[1]); - } + .highlight_symbol(">"); + f.render_stateful_widget(items, chunks[0], &mut app.items.state); + + let events = app.events.iter().map(|&(evt, level)| { + Text::styled( + format!("{}: {}", level, evt), + match level { + "ERROR" => app.error_style, + "CRITICAL" => app.critical_style, + "WARNING" => app.warning_style, + _ => app.info_style, + }, + ) + }); + let events_list = List::new(events) + .block(Block::default().borders(Borders::ALL).title("List")) + .start_corner(Corner::BottomLeft); + f.render_widget(events_list, chunks[1]); })?; match events.next()? { @@ -130,29 +130,13 @@ fn main() -> Result<(), failure::Error> { break; } Key::Left => { - app.selected = None; + app.items.unselect(); } Key::Down => { - app.selected = if let Some(selected) = app.selected { - if selected >= app.items.len() - 1 { - Some(0) - } else { - Some(selected + 1) - } - } else { - Some(0) - } + app.items.next(); } Key::Up => { - app.selected = if let Some(selected) = app.selected { - if selected > 0 { - Some(selected - 1) - } else { - Some(app.items.len() - 1) - } - } else { - Some(0) - } + app.items.previous(); } _ => {} }, diff --git a/examples/paragraph.rs b/examples/paragraph.rs index 197c15a..515d049 100644 --- a/examples/paragraph.rs +++ b/examples/paragraph.rs @@ -10,7 +10,7 @@ use termion::screen::AlternateScreen; use tui::backend::TermionBackend; use tui::layout::{Alignment, Constraint, Direction, Layout}; use tui::style::{Color, Modifier, Style}; -use tui::widgets::{Block, Borders, Paragraph, Text, Widget}; +use tui::widgets::{Block, Borders, Paragraph, Text}; use tui::Terminal; use crate::util::event::{Event, Events}; @@ -36,9 +36,9 @@ fn main() -> Result<(), failure::Error> { let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4); long_line.push('\n'); - Block::default() - .style(Style::default().bg(Color::White)) - .render(&mut f, size); + let block = Block::default() + .style(Style::default().bg(Color::White)); + f.render_widget(block, size); let chunks = Layout::default() .direction(Direction::Vertical) @@ -72,26 +72,26 @@ fn main() -> Result<(), failure::Error> { let block = Block::default() .borders(Borders::ALL) .title_style(Style::default().modifier(Modifier::BOLD)); - Paragraph::new(text.iter()) + let paragraph = Paragraph::new(text.iter()) .block(block.clone().title("Left, no wrap")) - .alignment(Alignment::Left) - .render(&mut f, chunks[0]); - Paragraph::new(text.iter()) + .alignment(Alignment::Left); + f.render_widget(paragraph, chunks[0]); + let paragraph = Paragraph::new(text.iter()) .block(block.clone().title("Left, wrap")) .alignment(Alignment::Left) - .wrap(true) - .render(&mut f, chunks[1]); - Paragraph::new(text.iter()) + .wrap(true); + f.render_widget(paragraph, chunks[1]); + let paragraph = Paragraph::new(text.iter()) .block(block.clone().title("Center, wrap")) .alignment(Alignment::Center) .wrap(true) - .scroll(scroll) - .render(&mut f, chunks[2]); - Paragraph::new(text.iter()) + .scroll(scroll); + f.render_widget(paragraph, chunks[2]); + let paragraph = Paragraph::new(text.iter()) .block(block.clone().title("Right, wrap")) .alignment(Alignment::Right) - .wrap(true) - .render(&mut f, chunks[3]); + .wrap(true); + f.render_widget(paragraph, chunks[3]); })?; scroll += 1; diff --git a/examples/rustbox_demo.rs b/examples/rustbox_demo.rs index 9cf5ffc..2371209 100644 --- a/examples/rustbox_demo.rs +++ b/examples/rustbox_demo.rs @@ -32,7 +32,7 @@ fn main() -> Result<(), failure::Error> { let mut last_tick = Instant::now(); let tick_rate = Duration::from_millis(cli.tick_rate); loop { - ui::draw(&mut terminal, &app)?; + terminal.draw(|mut f| ui::draw(&mut f, &mut app))?; match terminal.backend().rustbox().peek_event(tick_rate, false) { Ok(rustbox::Event::KeyEvent(key)) => match key { Key::Char(c) => { diff --git a/examples/sparkline.rs b/examples/sparkline.rs index 369085a..9e3ef41 100644 --- a/examples/sparkline.rs +++ b/examples/sparkline.rs @@ -10,7 +10,7 @@ use termion::screen::AlternateScreen; use tui::backend::TermionBackend; use tui::layout::{Constraint, Direction, Layout}; use tui::style::{Color, Style}; -use tui::widgets::{Block, Borders, Sparkline, Widget}; +use tui::widgets::{Block, Borders, Sparkline}; use tui::Terminal; use crate::util::event::{Event, Events}; @@ -80,34 +80,34 @@ fn main() -> Result<(), failure::Error> { .as_ref(), ) .split(f.size()); - Sparkline::default() + let sparkline = Sparkline::default() .block( Block::default() .title("Data1") .borders(Borders::LEFT | Borders::RIGHT), ) .data(&app.data1) - .style(Style::default().fg(Color::Yellow)) - .render(&mut f, chunks[0]); - Sparkline::default() + .style(Style::default().fg(Color::Yellow)); + f.render_widget(sparkline, chunks[0]); + let sparkline = Sparkline::default() .block( Block::default() .title("Data2") .borders(Borders::LEFT | Borders::RIGHT), ) .data(&app.data2) - .style(Style::default().bg(Color::Green)) - .render(&mut f, chunks[1]); + .style(Style::default().bg(Color::Green)); + f.render_widget(sparkline, chunks[1]); // Multiline - Sparkline::default() + let sparkline = Sparkline::default() .block( Block::default() .title("Data3") .borders(Borders::LEFT | Borders::RIGHT), ) .data(&app.data3) - .style(Style::default().fg(Color::Red)) - .render(&mut f, chunks[2]); + .style(Style::default().fg(Color::Red)); + f.render_widget(sparkline, chunks[2]); })?; match events.next()? { diff --git a/examples/table.rs b/examples/table.rs index 77bfb45..9a2419b 100644 --- a/examples/table.rs +++ b/examples/table.rs @@ -10,7 +10,7 @@ use termion::screen::AlternateScreen; use tui::backend::TermionBackend; use tui::layout::{Constraint, Layout}; use tui::style::{Color, Modifier, Style}; -use tui::widgets::{Block, Borders, Row, Table, Widget}; +use tui::widgets::{Block, Borders, Row, Table}; use tui::Terminal; use crate::util::event::{Event, Events}; @@ -68,14 +68,15 @@ fn main() -> Result<(), failure::Error> { .constraints([Constraint::Percentage(100)].as_ref()) .margin(5) .split(f.size()); - Table::new(header.iter(), rows) + + let table = Table::new(header.iter(), rows) .block(Block::default().borders(Borders::ALL).title("Table")) .widths(&[ Constraint::Percentage(50), Constraint::Length(30), Constraint::Max(10), - ]) - .render(&mut f, rects[0]); + ]); + f.render_widget(table, rects[0]); })?; match events.next()? { diff --git a/examples/tabs.rs b/examples/tabs.rs index f2abfc3..9d4e846 100644 --- a/examples/tabs.rs +++ b/examples/tabs.rs @@ -10,7 +10,7 @@ use termion::screen::AlternateScreen; use tui::backend::TermionBackend; use tui::layout::{Constraint, Direction, Layout}; use tui::style::{Color, Style}; -use tui::widgets::{Block, Borders, Tabs, Widget}; +use tui::widgets::{Block, Borders, Tabs}; use tui::Terminal; use crate::util::event::{Event, Events}; @@ -46,35 +46,23 @@ fn main() -> Result<(), failure::Error> { .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) .split(size); - Block::default() - .style(Style::default().bg(Color::White)) - .render(&mut f, size); - Tabs::default() + let block = Block::default().style(Style::default().bg(Color::White)); + f.render_widget(block, size); + let tabs = Tabs::default() .block(Block::default().borders(Borders::ALL).title("Tabs")) .titles(&app.tabs.titles) .select(app.tabs.index) .style(Style::default().fg(Color::Cyan)) - .highlight_style(Style::default().fg(Color::Yellow)) - .render(&mut f, chunks[0]); - match app.tabs.index { - 0 => Block::default() - .title("Inner 0") - .borders(Borders::ALL) - .render(&mut f, chunks[1]), - 1 => Block::default() - .title("Inner 1") - .borders(Borders::ALL) - .render(&mut f, chunks[1]), - 2 => Block::default() - .title("Inner 2") - .borders(Borders::ALL) - .render(&mut f, chunks[1]), - 3 => Block::default() - .title("Inner 3") - .borders(Borders::ALL) - .render(&mut f, chunks[1]), - _ => {} - } + .highlight_style(Style::default().fg(Color::Yellow)); + f.render_widget(tabs, chunks[0]); + let inner = match app.tabs.index { + 0 => Block::default().title("Inner 0").borders(Borders::ALL), + 1 => Block::default().title("Inner 1").borders(Borders::ALL), + 2 => Block::default().title("Inner 2").borders(Borders::ALL), + 3 => Block::default().title("Inner 3").borders(Borders::ALL), + _ => unreachable!(), + }; + f.render_widget(inner, chunks[1]); })?; match events.next()? { diff --git a/examples/termion_demo.rs b/examples/termion_demo.rs index 41660da..cf37dec 100644 --- a/examples/termion_demo.rs +++ b/examples/termion_demo.rs @@ -13,8 +13,10 @@ use termion::screen::AlternateScreen; use tui::backend::TermionBackend; use tui::Terminal; -use crate::demo::{ui, App}; -use crate::util::event::{Config, Event, Events}; +use crate::{ + demo::{ui, App}, + util::event::{Config, Event, Events}, +}; #[derive(Debug, StructOpt)] struct Cli { @@ -42,7 +44,8 @@ fn main() -> Result<(), failure::Error> { let mut app = App::new("Termion demo"); loop { - ui::draw(&mut terminal, &app)?; + terminal.draw(|mut f| ui::draw(&mut f, &mut app))?; + match events.next()? { Event::Input(key) => match key { Key::Char(c) => { diff --git a/examples/user_input.rs b/examples/user_input.rs index 7c6a7dd..5e24048 100644 --- a/examples/user_input.rs +++ b/examples/user_input.rs @@ -23,7 +23,7 @@ use termion::screen::AlternateScreen; use tui::backend::TermionBackend; use tui::layout::{Constraint, Direction, Layout}; use tui::style::{Color, Style}; -use tui::widgets::{Block, Borders, List, Paragraph, Text, Widget}; +use tui::widgets::{Block, Borders, List, Paragraph, Text}; use tui::Terminal; use unicode_width::UnicodeWidthStr; @@ -83,23 +83,28 @@ fn main() -> Result<(), failure::Error> { .as_ref(), ) .split(f.size()); - let help_message = match app.input_mode { + + let msg = match app.input_mode { InputMode::Normal => "Press q to exit, e to start editing.", InputMode::Editing => "Press Esc to stop editing, Enter to record the message", }; - Paragraph::new([Text::raw(help_message)].iter()).render(&mut f, chunks[0]); - Paragraph::new([Text::raw(&app.input)].iter()) + let text = [Text::raw(msg)]; + let help_message = Paragraph::new(text.iter()); + f.render_widget(help_message, chunks[0]); + + let text = [Text::raw(&app.input)]; + let input = Paragraph::new(text.iter()) .style(Style::default().fg(Color::Yellow)) - .block(Block::default().borders(Borders::ALL).title("Input")) - .render(&mut f, chunks[1]); + .block(Block::default().borders(Borders::ALL).title("Input")); + f.render_widget(input, chunks[1]); let messages = app .messages .iter() .enumerate() .map(|(i, m)| Text::raw(format!("{}: {}", i, m))); - List::new(messages) - .block(Block::default().borders(Borders::ALL).title("Messages")) - .render(&mut f, chunks[2]); + let messages = + List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages")); + f.render_widget(messages, chunks[2]); })?; // Put the cursor back inside the input box diff --git a/examples/util/mod.rs b/examples/util/mod.rs index a1c7e5c..3167b05 100644 --- a/examples/util/mod.rs +++ b/examples/util/mod.rs @@ -3,6 +3,7 @@ pub mod event; use rand::distributions::{Distribution, Uniform}; use rand::rngs::ThreadRng; +use tui::widgets::ListState; #[derive(Clone)] pub struct RandomSignal { @@ -75,3 +76,56 @@ impl<'a> TabsState<'a> { } } } + +pub struct StatefulList { + pub state: ListState, + pub items: Vec, +} + +impl StatefulList { + pub fn new() -> StatefulList { + StatefulList { + state: ListState::default(), + items: Vec::new(), + } + } + + pub fn with_items(items: Vec) -> StatefulList { + StatefulList { + state: ListState::default(), + items: items, + } + } + + pub fn next(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i >= self.items.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + pub fn previous(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i == 0 { + self.items.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + pub fn unselect(&mut self) { + self.state.select(None); + } +} diff --git a/src/buffer.rs b/src/buffer.rs index 5537944..827de1d 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -305,7 +305,14 @@ impl Buffer { /// Print at most the first n characters of a string if enough space is available /// until the end of the line - pub fn set_stringn(&mut self, x: u16, y: u16, string: S, width: usize, style: Style) + pub fn set_stringn( + &mut self, + x: u16, + y: u16, + string: S, + width: usize, + style: Style, + ) -> (u16, u16) where S: AsRef, { @@ -330,6 +337,15 @@ impl Buffer { index += width; x_offset += width; } + (x_offset as u16, y) + } + + pub fn set_background(&mut self, area: Rect, color: Color) { + for y in area.top()..area.bottom() { + for x in area.left()..area.right() { + self.get_mut(x, y).set_bg(color); + } + } } /// Resize the buffer so that the mapped area matches the given area and that the buffer diff --git a/src/lib.rs b/src/lib.rs index 6e1ac78..bc3e65e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,10 +88,10 @@ //! let mut terminal = Terminal::new(backend)?; //! terminal.draw(|mut f| { //! let size = f.size(); -//! Block::default() +//! let block = Block::default() //! .title("Block") -//! .borders(Borders::ALL) -//! .render(&mut f, size); +//! .borders(Borders::ALL); +//! f.render_widget(block, size); //! }) //! } //! ``` @@ -126,14 +126,14 @@ //! ].as_ref() //! ) //! .split(f.size()); -//! Block::default() +//! let block = Block::default() //! .title("Block") -//! .borders(Borders::ALL) -//! .render(&mut f, chunks[0]); -//! Block::default() +//! .borders(Borders::ALL); +//! f.render_widget(block, chunks[0]); +//! let block = Block::default() //! .title("Block 2") -//! .borders(Borders::ALL) -//! .render(&mut f, chunks[2]); +//! .borders(Borders::ALL); +//! f.render_widget(block, chunks[1]); //! }) //! } //! ``` diff --git a/src/terminal.rs b/src/terminal.rs index aea8359..6413bc3 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -4,7 +4,7 @@ use std::io; use crate::backend::Backend; use crate::buffer::Buffer; use crate::layout::Rect; -use crate::widgets::Widget; +use crate::widgets::{StatefulWidget, Widget}; /// Interface to the terminal backed by Termion #[derive(Debug)] @@ -42,11 +42,18 @@ where } /// Calls the draw method of a given widget on the current buffer - pub fn render(&mut self, widget: &mut W, area: Rect) + pub fn render_widget(&mut self, widget: W, area: Rect) where W: Widget, { - widget.draw(area, self.terminal.current_buffer_mut()); + widget.render(area, self.terminal.current_buffer_mut()); + } + + pub fn render_stateful_widget(&mut self, widget: W, area: Rect, state: &mut W::State) + where + W: StatefulWidget, + { + widget.render(area, self.terminal.current_buffer_mut(), state); } } diff --git a/src/widgets/barchart.rs b/src/widgets/barchart.rs index 971d2b3..d38e750 100644 --- a/src/widgets/barchart.rs +++ b/src/widgets/barchart.rs @@ -105,10 +105,10 @@ impl<'a> BarChart<'a> { } impl<'a> Widget for BarChart<'a> { - fn draw(&mut self, area: Rect, buf: &mut Buffer) { + fn render(mut self, area: Rect, buf: &mut Buffer) { let chart_area = match self.block { Some(ref mut b) => { - b.draw(area, buf); + b.render(area, buf); b.inner(area) } None => area, @@ -118,7 +118,7 @@ impl<'a> Widget for BarChart<'a> { return; } - self.background(chart_area, buf, self.style.bg); + buf.set_background(chart_area, self.style.bg); let max = self .max diff --git a/src/widgets/block.rs b/src/widgets/block.rs index fa7a20a..6959127 100644 --- a/src/widgets/block.rs +++ b/src/widgets/block.rs @@ -113,12 +113,12 @@ impl<'a> Block<'a> { } impl<'a> Widget for Block<'a> { - fn draw(&mut self, area: Rect, buf: &mut Buffer) { + fn render(self, area: Rect, buf: &mut Buffer) { if area.width < 2 || area.height < 2 { return; } - self.background(area, buf, self.style.bg); + buf.set_background(area, self.style.bg); // Sides if self.borders.intersects(Borders::LEFT) { diff --git a/src/widgets/canvas/mod.rs b/src/widgets/canvas/mod.rs index 55247ee..84a166d 100644 --- a/src/widgets/canvas/mod.rs +++ b/src/widgets/canvas/mod.rs @@ -225,10 +225,10 @@ impl<'a, F> Widget for Canvas<'a, F> where F: Fn(&mut Context), { - fn draw(&mut self, area: Rect, buf: &mut Buffer) { + fn render(mut self, area: Rect, buf: &mut Buffer) { let canvas_area = match self.block { Some(ref mut b) => { - b.draw(area, buf); + b.render(area, buf); b.inner(area) } None => area, diff --git a/src/widgets/chart.rs b/src/widgets/chart.rs index 62e5b72..f091cab 100644 --- a/src/widgets/chart.rs +++ b/src/widgets/chart.rs @@ -404,10 +404,10 @@ where LX: AsRef, LY: AsRef, { - fn draw(&mut self, area: Rect, buf: &mut Buffer) { + fn render(mut self, area: Rect, buf: &mut Buffer) { let chart_area = match self.block { Some(ref mut b) => { - b.draw(area, buf); + b.render(area, buf); b.inner(area) } None => area, @@ -419,7 +419,7 @@ where return; } - self.background(chart_area, buf, self.style.bg); + buf.set_background(chart_area, self.style.bg); if let Some((x, y)) = layout.title_x { let title = self.x_axis.title.unwrap(); @@ -532,7 +532,7 @@ where } } }) - .draw(graph_area, buf); + .render(graph_area, buf); } } } @@ -540,7 +540,7 @@ where if let Some(legend_area) = layout.legend_area { Block::default() .borders(Borders::ALL) - .draw(legend_area, buf); + .render(legend_area, buf); for (i, dataset) in self.datasets.iter().enumerate() { buf.set_string( legend_area.x + 1, diff --git a/src/widgets/gauge.rs b/src/widgets/gauge.rs index 89255e7..381cea9 100644 --- a/src/widgets/gauge.rs +++ b/src/widgets/gauge.rs @@ -72,10 +72,10 @@ impl<'a> Gauge<'a> { } impl<'a> Widget for Gauge<'a> { - fn draw(&mut self, area: Rect, buf: &mut Buffer) { + fn render(mut self, area: Rect, buf: &mut Buffer) { let gauge_area = match self.block { Some(ref mut b) => { - b.draw(area, buf); + b.render(area, buf); b.inner(area) } None => area, @@ -85,7 +85,7 @@ impl<'a> Widget for Gauge<'a> { } if self.style.bg != Color::Reset { - self.background(gauge_area, buf, self.style.bg); + buf.set_background(gauge_area, self.style.bg); } let center = gauge_area.height / 2 + gauge_area.top(); diff --git a/src/widgets/list.rs b/src/widgets/list.rs index b9a4e78..2fb81ff 100644 --- a/src/widgets/list.rs +++ b/src/widgets/list.rs @@ -1,4 +1,3 @@ -use std::convert::AsRef; use std::iter::{self, Iterator}; use unicode_width::UnicodeWidthStr; @@ -6,16 +5,64 @@ use unicode_width::UnicodeWidthStr; use crate::buffer::Buffer; use crate::layout::{Corner, Rect}; use crate::style::Style; -use crate::widgets::{Block, Text, Widget}; +use crate::widgets::{Block, StatefulWidget, Text, Widget}; +pub struct ListState { + offset: usize, + selected: Option, +} + +impl Default for ListState { + fn default() -> ListState { + ListState { + offset: 0, + selected: None, + } + } +} + +impl ListState { + pub fn selected(&self) -> Option { + self.selected + } + + pub fn select(&mut self, index: Option) { + self.selected = index; + if index.is_none() { + self.offset = 0; + } + } +} + +/// A widget to display several items among which one can be selected (optional) +/// +/// # Examples +/// +/// ``` +/// # use tui::widgets::{Block, Borders, List, Text}; +/// # use tui::style::{Style, Color, Modifier}; +/// # fn main() { +/// let items = ["Item 1", "Item 2", "Item 3"].iter().map(|i| Text::raw(*i)); +/// List::new(items) +/// .block(Block::default().title("List").borders(Borders::ALL)) +/// .style(Style::default().fg(Color::White)) +/// .highlight_style(Style::default().modifier(Modifier::ITALIC)) +/// .highlight_symbol(">>"); +/// # } +/// ``` pub struct List<'b, L> where L: Iterator>, { block: Option>, items: L, - style: Style, start_corner: Corner, + /// Base style of the widget + style: Style, + /// Style used to render selected item + highlight_style: Style, + /// Symbol in front of the selected item (Shift all items to the right) + highlight_symbol: Option<&'b str>, } impl<'b, L> Default for List<'b, L> @@ -28,6 +75,8 @@ where items: L::default(), style: Default::default(), start_corner: Corner::TopLeft, + highlight_style: Style::default(), + highlight_symbol: None, } } } @@ -42,6 +91,8 @@ where items, style: Default::default(), start_corner: Corner::TopLeft, + highlight_style: Style::default(), + highlight_symbol: None, } } @@ -63,20 +114,32 @@ where self } + pub fn highlight_symbol(mut self, highlight_symbol: &'b str) -> List<'b, L> { + self.highlight_symbol = Some(highlight_symbol); + self + } + + pub fn highlight_style(mut self, highlight_style: Style) -> List<'b, L> { + self.highlight_style = highlight_style; + self + } + pub fn start_corner(mut self, corner: Corner) -> List<'b, L> { self.start_corner = corner; self } } -impl<'b, L> Widget for List<'b, L> +impl<'b, L> StatefulWidget for List<'b, L> where L: Iterator>, { - fn draw(&mut self, area: Rect, buf: &mut Buffer) { + type State = ListState; + + fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { let list_area = match self.block { Some(ref mut b) => { - b.draw(area, buf); + b.render(area, buf); b.inner(area) } None => area, @@ -86,11 +149,36 @@ where return; } - self.background(list_area, buf, self.style.bg); + let list_height = list_area.height as usize; + + buf.set_background(list_area, self.style.bg); + + // Use highlight_style only if something is selected + let (selected, highlight_style) = match state.selected { + Some(i) => (Some(i), self.highlight_style), + None => (None, self.style), + }; + let highlight_symbol = self.highlight_symbol.unwrap_or(""); + let blank_symbol = iter::repeat(" ") + .take(highlight_symbol.width()) + .collect::(); + + // Make sure the list show the selected item + state.offset = if let Some(selected) = selected { + if selected >= list_height + state.offset - 1 { + selected + 1 - list_height + } else if selected < state.offset { + selected + } else { + state.offset + } + } else { + 0 + }; for (i, item) in self .items - .by_ref() + .skip(state.offset) .enumerate() .take(list_area.height as usize) { @@ -100,144 +188,53 @@ where // Not supported _ => (list_area.left(), list_area.top() + i as u16), }; + let (x, style) = if let Some(s) = selected { + if s == i + state.offset { + let (x, _) = buf.set_stringn( + x, + y, + highlight_symbol, + list_area.width as usize, + highlight_style, + ); + (x + 1, Some(highlight_style)) + } else { + let (x, _) = buf.set_stringn( + x, + y, + &blank_symbol, + list_area.width as usize, + highlight_style, + ); + (x + 1, None) + } + } else { + (x, None) + }; match item { Text::Raw(ref v) => { - buf.set_stringn(x, y, v, list_area.width as usize, Style::default()); + buf.set_stringn( + x, + y, + v, + list_area.width as usize, + style.unwrap_or(self.style), + ); } Text::Styled(ref v, s) => { - buf.set_stringn(x, y, v, list_area.width as usize, s); + buf.set_stringn(x, y, v, list_area.width as usize, style.unwrap_or(s)); } }; } } } -/// A widget to display several items among which one can be selected (optional) -/// -/// # Examples -/// -/// ``` -/// # use tui::widgets::{Block, Borders, SelectableList}; -/// # use tui::style::{Style, Color, Modifier}; -/// SelectableList::default() -/// .block(Block::default().title("SelectableList").borders(Borders::ALL)) -/// .items(&["Item 1", "Item 2", "Item 3"]) -/// .select(Some(1)) -/// .style(Style::default().fg(Color::White)) -/// .highlight_style(Style::default().modifier(Modifier::ITALIC)) -/// .highlight_symbol(">>"); -/// ``` -pub struct SelectableList<'b> { - block: Option>, - /// Items to be displayed - items: Vec<&'b str>, - /// Index of the one selected - selected: Option, - /// Base style of the widget - style: Style, - /// Style used to render selected item - highlight_style: Style, - /// Symbol in front of the selected item (Shift all items to the right) - highlight_symbol: Option<&'b str>, -} - -impl<'b> Default for SelectableList<'b> { - fn default() -> SelectableList<'b> { - SelectableList { - block: None, - items: Vec::new(), - selected: None, - style: Default::default(), - highlight_style: Default::default(), - highlight_symbol: None, - } - } -} - -impl<'b> SelectableList<'b> { - pub fn block(mut self, block: Block<'b>) -> SelectableList<'b> { - self.block = Some(block); - self - } - - pub fn items(mut self, items: &'b [I]) -> SelectableList<'b> - where - I: AsRef + 'b, - { - self.items = items.iter().map(AsRef::as_ref).collect::>(); - self - } - - pub fn style(mut self, style: Style) -> SelectableList<'b> { - self.style = style; - self - } - - pub fn highlight_symbol(mut self, highlight_symbol: &'b str) -> SelectableList<'b> { - self.highlight_symbol = Some(highlight_symbol); - self - } - - pub fn highlight_style(mut self, highlight_style: Style) -> SelectableList<'b> { - self.highlight_style = highlight_style; - self - } - - pub fn select(mut self, index: Option) -> SelectableList<'b> { - self.selected = index; - self - } -} - -impl<'b> Widget for SelectableList<'b> { - fn draw(&mut self, area: Rect, buf: &mut Buffer) { - let list_area = match self.block { - Some(ref mut b) => b.inner(area), - None => area, - }; - - let list_height = list_area.height as usize; - - // Use highlight_style only if something is selected - let (selected, highlight_style) = match self.selected { - Some(i) => (Some(i), self.highlight_style), - None => (None, self.style), - }; - let highlight_symbol = self.highlight_symbol.unwrap_or(""); - let blank_symbol = iter::repeat(" ") - .take(highlight_symbol.width()) - .collect::(); - // Make sure the list show the selected item - let offset = if let Some(selected) = selected { - if selected >= list_height { - selected - list_height + 1 - } else { - 0 - } - } else { - 0 - }; - - // Render items - let items = self - .items - .iter() - .enumerate() - .map(|(i, &item)| { - if let Some(s) = selected { - if i == s { - Text::styled(format!("{} {}", highlight_symbol, item), highlight_style) - } else { - Text::styled(format!("{} {}", blank_symbol, item), self.style) - } - } else { - Text::styled(item, self.style) - } - }) - .skip(offset as usize); - List::new(items) - .block(self.block.unwrap_or_default()) - .style(self.style) - .draw(area, buf); +impl<'b, L> Widget for List<'b, L> +where + L: Iterator>, +{ + fn render(self, area: Rect, buf: &mut Buffer) { + let mut state = ListState::default(); + StatefulWidget::render(self, area, buf, &mut state); } } diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 0afb5d2..ce8740b 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -17,17 +17,15 @@ pub use self::barchart::BarChart; pub use self::block::{Block, BorderType}; pub use self::chart::{Axis, Chart, Dataset, GraphType, Marker}; pub use self::gauge::Gauge; -pub use self::list::{List, SelectableList}; +pub use self::list::{List, ListState}; pub use self::paragraph::Paragraph; pub use self::sparkline::Sparkline; pub use self::table::{Row, Table}; pub use self::tabs::Tabs; -use crate::backend::Backend; use crate::buffer::Buffer; use crate::layout::Rect; -use crate::style::{Color, Style}; -use crate::terminal::Frame; +use crate::style::Style; bitflags! { /// Bitflags that can be composed to set the visible borders essentially on the block widget. @@ -67,21 +65,10 @@ impl<'b> Text<'b> { pub trait Widget { /// Draws the current state of the widget in the given buffer. That the only method required to /// implement a custom widget. - fn draw(&mut self, area: Rect, buf: &mut Buffer); - /// Helper method to quickly set the background of all cells inside the specified area. - fn background(&self, area: Rect, buf: &mut Buffer, color: Color) { - for y in area.top()..area.bottom() { - for x in area.left()..area.right() { - buf.get_mut(x, y).set_bg(color); - } - } - } - /// Helper method that can be chained with a widget's builder methods to render it. - fn render(&mut self, f: &mut Frame, area: Rect) - where - Self: Sized, - B: Backend, - { - f.render(self, area); - } + fn render(self, area: Rect, buf: &mut Buffer); +} + +pub trait StatefulWidget { + type State; + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State); } diff --git a/src/widgets/paragraph.rs b/src/widgets/paragraph.rs index 19829a4..d89486d 100644 --- a/src/widgets/paragraph.rs +++ b/src/widgets/paragraph.rs @@ -105,10 +105,10 @@ impl<'a, 't, 'b, T> Widget for Paragraph<'a, 't, T> where T: Iterator>, { - fn draw(&mut self, area: Rect, buf: &mut Buffer) { + fn render(mut self, area: Rect, buf: &mut Buffer) { let text_area = match self.block { Some(ref mut b) => { - b.draw(area, buf); + b.render(area, buf); b.inner(area) } None => area, @@ -118,7 +118,7 @@ where return; } - self.background(text_area, buf, self.style.bg); + buf.set_background(text_area, self.style.bg); let style = self.style; let mut styled = self.text.by_ref().flat_map(|t| match *t { diff --git a/src/widgets/sparkline.rs b/src/widgets/sparkline.rs index 5dfa985..c724b2f 100644 --- a/src/widgets/sparkline.rs +++ b/src/widgets/sparkline.rs @@ -65,10 +65,10 @@ impl<'a> Sparkline<'a> { } impl<'a> Widget for Sparkline<'a> { - fn draw(&mut self, area: Rect, buf: &mut Buffer) { + fn render(mut self, area: Rect, buf: &mut Buffer) { let spark_area = match self.block { Some(ref mut b) => { - b.draw(area, buf); + b.render(area, buf); b.inner(area) } None => area, @@ -129,17 +129,17 @@ mod tests { #[test] fn it_does_not_panic_if_max_is_zero() { - let mut widget = Sparkline::default().data(&[0, 0, 0]); + let widget = Sparkline::default().data(&[0, 0, 0]); let area = Rect::new(0, 0, 3, 1); let mut buffer = Buffer::empty(area); - widget.draw(area, &mut buffer); + widget.render(area, &mut buffer); } #[test] fn it_does_not_panic_if_max_is_set_to_zero() { - let mut widget = Sparkline::default().data(&[0, 1, 2]).max(0); + let widget = Sparkline::default().data(&[0, 1, 2]).max(0); let area = Rect::new(0, 0, 3, 1); let mut buffer = Buffer::empty(area); - widget.draw(area, &mut buffer); + widget.render(area, &mut buffer); } } diff --git a/src/widgets/table.rs b/src/widgets/table.rs index 1027ada..1f56cda 100644 --- a/src/widgets/table.rs +++ b/src/widgets/table.rs @@ -176,18 +176,17 @@ where D: Iterator, R: Iterator>, { - fn draw(&mut self, area: Rect, buf: &mut Buffer) { + fn render(mut self, area: Rect, buf: &mut Buffer) { // Render block if necessary and get the drawing area let table_area = match self.block { Some(ref mut b) => { - b.draw(area, buf); + b.render(area, buf); b.inner(area) } None => area, }; - // Set the background - self.background(table_area, buf, self.style.bg); + buf.set_background(table_area, self.style.bg); let mut solver = Solver::new(); let mut var_indices = HashMap::new(); diff --git a/src/widgets/tabs.rs b/src/widgets/tabs.rs index 8ebfab0..37abf02 100644 --- a/src/widgets/tabs.rs +++ b/src/widgets/tabs.rs @@ -94,10 +94,10 @@ impl<'a, T> Widget for Tabs<'a, T> where T: AsRef, { - fn draw(&mut self, area: Rect, buf: &mut Buffer) { + fn render(mut self, area: Rect, buf: &mut Buffer) { let tabs_area = match self.block { Some(ref mut b) => { - b.draw(area, buf); + b.render(area, buf); b.inner(area) } None => area, @@ -107,7 +107,7 @@ where return; } - self.background(tabs_area, buf, self.style.bg); + buf.set_background(tabs_area, self.style.bg); let mut x = tabs_area.left(); let titles_length = self.titles.len(); diff --git a/tests/block.rs b/tests/block.rs index e289a0e..6138314 100644 --- a/tests/block.rs +++ b/tests/block.rs @@ -2,7 +2,7 @@ use tui::backend::TestBackend; use tui::buffer::Buffer; use tui::layout::Rect; use tui::style::{Color, Style}; -use tui::widgets::{Block, Borders, Widget}; +use tui::widgets::{Block, Borders}; use tui::Terminal; #[test] @@ -11,19 +11,19 @@ fn it_draws_a_block() { let mut terminal = Terminal::new(backend).unwrap(); terminal .draw(|mut f| { - Block::default() + let block = Block::default() .title("Title") .borders(Borders::ALL) - .title_style(Style::default().fg(Color::LightBlue)) - .render( - &mut f, - Rect { - x: 0, - y: 0, - width: 8, - height: 8, - }, - ); + .title_style(Style::default().fg(Color::LightBlue)); + f.render_widget( + block, + Rect { + x: 0, + y: 0, + width: 8, + height: 8, + }, + ); }) .unwrap(); let mut expected = Buffer::with_lines(vec![ diff --git a/tests/chart.rs b/tests/chart.rs index f7c47f5..3c30bfb 100644 --- a/tests/chart.rs +++ b/tests/chart.rs @@ -1,7 +1,7 @@ use tui::backend::TestBackend; use tui::layout::Rect; use tui::style::{Color, Style}; -use tui::widgets::{Axis, Block, Borders, Chart, Dataset, Marker, Widget}; +use tui::widgets::{Axis, Block, Borders, Chart, Dataset, Marker}; use tui::Terminal; #[test] @@ -11,23 +11,24 @@ fn zero_axes_ok() { terminal .draw(|mut f| { - Chart::default() + let datasets = [Dataset::default() + .marker(Marker::Braille) + .style(Style::default().fg(Color::Magenta)) + .data(&[(0.0, 0.0)])]; + let chart = Chart::default() .block(Block::default().title("Plot").borders(Borders::ALL)) .x_axis(Axis::default().bounds([0.0, 0.0]).labels(&["0.0", "1.0"])) .y_axis(Axis::default().bounds([0.0, 1.0]).labels(&["0.0", "1.0"])) - .datasets(&[Dataset::default() - .marker(Marker::Braille) - .style(Style::default().fg(Color::Magenta)) - .data(&[(0.0, 0.0)])]) - .render( - &mut f, - Rect { - x: 0, - y: 0, - width: 100, - height: 100, - }, - ); + .datasets(&datasets); + f.render_widget( + chart, + Rect { + x: 0, + y: 0, + width: 100, + height: 100, + }, + ); }) .unwrap(); } diff --git a/tests/gauge.rs b/tests/gauge.rs index df743dd..82df65f 100644 --- a/tests/gauge.rs +++ b/tests/gauge.rs @@ -1,7 +1,7 @@ use tui::backend::TestBackend; use tui::buffer::Buffer; use tui::layout::{Constraint, Direction, Layout}; -use tui::widgets::{Block, Borders, Gauge, Widget}; +use tui::widgets::{Block, Borders, Gauge}; use tui::Terminal; #[test] @@ -16,14 +16,14 @@ fn gauge_render() { .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .split(f.size()); - Gauge::default() + let gauge = Gauge::default() .block(Block::default().title("Percentage").borders(Borders::ALL)) - .percent(43) - .render(&mut f, chunks[0]); - Gauge::default() + .percent(43); + f.render_widget(gauge, chunks[0]); + let gauge = Gauge::default() .block(Block::default().title("Ratio").borders(Borders::ALL)) - .ratio(0.211_313_934_313_1) - .render(&mut f, chunks[1]); + .ratio(0.211_313_934_313_1); + f.render_widget(gauge, chunks[1]); }) .unwrap(); let expected = Buffer::with_lines(vec![ diff --git a/tests/paragraph.rs b/tests/paragraph.rs index 220ebaa..788a822 100644 --- a/tests/paragraph.rs +++ b/tests/paragraph.rs @@ -1,7 +1,7 @@ use tui::backend::TestBackend; use tui::buffer::Buffer; use tui::layout::Alignment; -use tui::widgets::{Block, Borders, Paragraph, Text, Widget}; +use tui::widgets::{Block, Borders, Paragraph, Text}; use tui::Terminal; const SAMPLE_STRING: &str = "The library is based on the principle of immediate rendering with \ @@ -19,11 +19,11 @@ fn paragraph_render_wrap() { .draw(|mut f| { let size = f.size(); let text = [Text::raw(SAMPLE_STRING)]; - Paragraph::new(text.iter()) + let paragraph = Paragraph::new(text.iter()) .block(Block::default().borders(Borders::ALL)) .alignment(alignment) - .wrap(true) - .render(&mut f, size); + .wrap(true); + f.render_widget(paragraph, size); }) .unwrap(); terminal.backend().buffer().clone() @@ -86,10 +86,10 @@ fn paragraph_render_double_width() { .draw(|mut f| { let size = f.size(); let text = [Text::raw(s)]; - Paragraph::new(text.iter()) + let paragraph = Paragraph::new(text.iter()) .block(Block::default().borders(Borders::ALL)) - .wrap(true) - .render(&mut f, size); + .wrap(true); + f.render_widget(paragraph, size); }) .unwrap(); @@ -118,10 +118,10 @@ fn paragraph_render_mixed_width() { .draw(|mut f| { let size = f.size(); let text = [Text::raw(s)]; - Paragraph::new(text.iter()) + let paragraph = Paragraph::new(text.iter()) .block(Block::default().borders(Borders::ALL)) - .wrap(true) - .render(&mut f, size); + .wrap(true); + f.render_widget(paragraph, size); }) .unwrap(); diff --git a/tests/table.rs b/tests/table.rs index ec17167..abb498c 100644 --- a/tests/table.rs +++ b/tests/table.rs @@ -1,7 +1,7 @@ use tui::backend::TestBackend; use tui::buffer::Buffer; use tui::layout::Constraint; -use tui::widgets::{Block, Borders, Row, Table, Widget}; +use tui::widgets::{Block, Borders, Row, Table}; use tui::Terminal; #[test] @@ -13,7 +13,7 @@ fn table_column_spacing() { terminal .draw(|mut f| { let size = f.size(); - Table::new( + let table = Table::new( ["Head1", "Head2", "Head3"].iter(), vec![ Row::Data(["Row11", "Row12", "Row13"].iter()), @@ -29,8 +29,8 @@ fn table_column_spacing() { Constraint::Length(5), Constraint::Length(5), ]) - .column_spacing(column_spacing) - .render(&mut f, size); + .column_spacing(column_spacing); + f.render_widget(table, size); }) .unwrap(); terminal.backend().buffer().clone() @@ -114,7 +114,7 @@ fn table_widths() { terminal .draw(|mut f| { let size = f.size(); - Table::new( + let table = Table::new( ["Head1", "Head2", "Head3"].iter(), vec![ Row::Data(["Row11", "Row12", "Row13"].iter()), @@ -125,8 +125,8 @@ fn table_widths() { .into_iter(), ) .block(Block::default().borders(Borders::ALL)) - .widths(widths) - .render(&mut f, size); + .widths(widths); + f.render_widget(table, size); }) .unwrap(); terminal.backend().buffer().clone() @@ -205,7 +205,7 @@ fn table_percentage_widths() { terminal .draw(|mut f| { let size = f.size(); - Table::new( + let table = Table::new( ["Head1", "Head2", "Head3"].iter(), vec![ Row::Data(["Row11", "Row12", "Row13"].iter()), @@ -217,8 +217,8 @@ fn table_percentage_widths() { ) .block(Block::default().borders(Borders::ALL)) .widths(widths) - .column_spacing(0) - .render(&mut f, size); + .column_spacing(0); + f.render_widget(table, size); }) .unwrap(); terminal.backend().buffer().clone() @@ -314,7 +314,7 @@ fn table_mixed_widths() { terminal .draw(|mut f| { let size = f.size(); - Table::new( + let table = Table::new( ["Head1", "Head2", "Head3"].iter(), vec![ Row::Data(["Row11", "Row12", "Row13"].iter()), @@ -325,8 +325,8 @@ fn table_mixed_widths() { .into_iter(), ) .block(Block::default().borders(Borders::ALL)) - .widths(widths) - .render(&mut f, size); + .widths(widths); + f.render_widget(table, size); }) .unwrap(); terminal.backend().buffer().clone()