From 56aeb41825fb27bd1fc7adea398213705f39f84f Mon Sep 17 00:00:00 2001 From: Florian Dehau Date: Sun, 15 Dec 2019 21:38:18 +0100 Subject: [PATCH] feat: add stateful widgets Most widgets can be drawn directly based on the input parameters. However, some features may require some kind of associated state to be implemented. For example, the `List` widget can highlight the item currently selected. This can be translated in an offset, which is the number of elements to skip in order to have the selected item within the viewport currently allocated to this widget. The widget can therefore only provide the following behavior: whenever the selected item is out of the viewport scroll to a predefined position (make the selected item the last viewable item or the one in the middle). Nonetheless, if the widget has access to the last computed offset then it can implement a natural scrolling experience where the last offset is reused until the selected item is out of the viewport. To allow such behavior within the widgets, this commit introduces the following changes: - Add a `StatefulWidget` trait with an associated `State` type. Widgets that can take advantage of having a "memory" between two draw calls needs to implement this trait. - Add a `render_stateful_widget` method on `Frame` where the associated state is given as a parameter. The chosen approach is thus to let the developers manage their widgets' states themselves as they are already responsible for the lifecycle of the wigets (given that the crate exposes an immediate mode api). The following changes were also introduced: - `Widget::render` has been deleted. Developers should use `Frame::render_widget` instead. - `Widget::background` has been deleted. Developers should use `Buffer::set_background` instead. - `SelectableList` has been deleted. Developers can directly use `List` where `SelectableList` features have been back-ported. --- examples/barchart.rs | 55 ++++---- examples/block.rs | 43 +++--- examples/canvas.rs | 14 +- examples/chart.rs | 42 +++--- examples/crossterm_demo.rs | 18 +-- examples/curses_demo.rs | 2 +- examples/custom_widget.rs | 7 +- examples/demo/app.rs | 35 +---- examples/demo/ui.rs | 167 ++++++++++++----------- examples/gauge.rs | 31 +++-- examples/layout.rs | 14 +- examples/list.rs | 86 +++++------- examples/paragraph.rs | 32 ++--- examples/rustbox_demo.rs | 2 +- examples/sparkline.rs | 20 +-- examples/table.rs | 9 +- examples/tabs.rs | 40 ++---- examples/termion_demo.rs | 9 +- examples/user_input.rs | 23 ++-- examples/util/mod.rs | 54 ++++++++ src/buffer.rs | 18 ++- src/lib.rs | 18 +-- src/terminal.rs | 13 +- src/widgets/barchart.rs | 6 +- src/widgets/block.rs | 4 +- src/widgets/canvas/mod.rs | 4 +- src/widgets/chart.rs | 10 +- src/widgets/gauge.rs | 6 +- src/widgets/list.rs | 271 ++++++++++++++++++------------------- src/widgets/mod.rs | 29 ++-- src/widgets/paragraph.rs | 6 +- src/widgets/sparkline.rs | 12 +- src/widgets/table.rs | 7 +- src/widgets/tabs.rs | 6 +- tests/block.rs | 24 ++-- tests/chart.rs | 31 +++-- tests/gauge.rs | 14 +- tests/paragraph.rs | 20 +-- tests/table.rs | 26 ++-- 39 files changed, 623 insertions(+), 605 deletions(-) diff --git a/examples/barchart.rs b/examples/barchart.rs index 733a732c..23a5bfbe 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 2dc65d1e..e5090a6c 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 8628e762..ae456815 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 aae717e0..f2139477 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 03aed932..f3ac3824 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 eed4b906..e0cb83b6 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 c3a9a0e5..f4c47440 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 6ffba07e..158a3b07 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 9d83e73c..9b749f4b 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 fed39a09..8327def7 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 05dc4cbd..bb3e1adb 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 8a3ee193..5729b8af 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 197c15ae..515d0497 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 9cf5ffcd..23712092 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 369085a0..9e3ef413 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 77bfb45e..9a2419b5 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 f2abfc33..9d4e846b 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 41660dac..cf37dece 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 7c6a7dd7..5e240481 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 a1c7e5c9..3167b05a 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 55379446..827de1de 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 6e1ac786..bc3e65ef 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 aea83598..6413bc3c 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 971d2b38..d38e750f 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 fa7a20ad..69591270 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 55247ee8..84a166db 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 62e5b72e..f091cab0 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 89255e79..381cea9f 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 b9a4e783..2fb81ff8 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 0afb5d26..ce8740bf 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 19829a42..d89486dc 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 5dfa9853..c724b2f8 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 1027ada4..1f56cda4 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 8ebfab0f..37abf027 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 e289a0e4..6138314c 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 f7c47f51..3c30bfbc 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 df743dd8..82df65f1 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 220ebaa7..788a8226 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 ec17167b..abb498c2 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()