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.
pull/233/head
Florian Dehau 4 years ago
parent 67dd1ac608
commit 6cb57f5d2a

@ -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()? {

@ -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]);
}
})?;

@ -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()? {

@ -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()? {

@ -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<I> {
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') => {

@ -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 {

@ -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()? {

@ -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<I> {
pub items: Vec<I>,
pub selected: usize,
}
impl<I> ListState<I> {
fn new(items: Vec<I>) -> ListState<I> {
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<RandomSignal>,
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<Server<'a>>,
@ -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) {

@ -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<B: Backend>(terminal: &mut Terminal<B>, 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<B: Backend>(f: &mut Frame<B>, 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<B>(f: &mut Frame<B>, app: &App, area: Rect)
fn draw_first_tab<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
@ -51,7 +47,7 @@ where
draw_text(f, chunks[2]);
}
fn draw_gauges<B>(f: &mut Frame<B>, app: &App, area: Rect)
fn draw_gauges<B>(f: &mut Frame<B>, 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<B>(f: &mut Frame<B>, app: &App, area: Rect)
fn draw_charts<B>(f: &mut Frame<B>, 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<B>(f: &mut Frame<B>, app: &App, area: Rect)
fn draw_second_tab<B>(f: &mut Frame<B>, 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]);
}

@ -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()? {

@ -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()? {

@ -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<usize>,
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();
}
_ => {}
},

@ -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;

@ -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) => {

@ -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()? {

@ -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()? {

@ -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()? {

@ -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) => {

@ -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

@ -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<T> {
pub state: ListState,
pub items: Vec<T>,
}
impl<T> StatefulList<T> {
pub fn new() -> StatefulList<T> {
StatefulList {
state: ListState::default(),
items: Vec::new(),
}
}
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
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);
}
}

@ -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<S>(&mut self, x: u16, y: u16, string: S, width: usize, style: Style)
pub fn set_stringn<S>(
&mut self,
x: u16,
y: u16,
string: S,
width: usize,
style: Style,
) -> (u16, u16)
where
S: AsRef<str>,
{
@ -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

@ -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]);
//! })
//! }
//! ```

@ -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<W>(&mut self, widget: &mut W, area: Rect)
pub fn render_widget<W>(&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<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
where
W: StatefulWidget,
{
widget.render(area, self.terminal.current_buffer_mut(), state);
}
}

@ -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

@ -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) {

@ -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,

@ -404,10 +404,10 @@ where
LX: AsRef<str>,
LY: AsRef<str>,
{
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,

@ -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();

@ -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<usize>,
}
impl Default for ListState {
fn default() -> ListState {
ListState {
offset: 0,
selected: None,
}
}
}
impl ListState {
pub fn selected(&self) -> Option<usize> {
self.selected
}
pub fn select(&mut self, index: Option<usize>) {
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<Item = Text<'b>>,
{
block: Option<Block<'b>>,
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<Item = Text<'b>>,
{
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::<String>();
// 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<Block<'b>>,
/// Items to be displayed
items: Vec<&'b str>,
/// Index of the one selected
selected: Option<usize>,
/// 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<I>(mut self, items: &'b [I]) -> SelectableList<'b>
where
I: AsRef<str> + 'b,
{
self.items = items.iter().map(AsRef::as_ref).collect::<Vec<&str>>();
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<usize>) -> 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::<String>();
// 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<Item = Text<'b>>,
{
fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = ListState::default();
StatefulWidget::render(self, area, buf, &mut state);
}
}

@ -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<B>(&mut self, f: &mut Frame<B>, 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);
}

@ -105,10 +105,10 @@ impl<'a, 't, 'b, T> Widget for Paragraph<'a, 't, T>
where
T: Iterator<Item = &'t Text<'t>>,
{
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 {

@ -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);
}
}

@ -176,18 +176,17 @@ where
D: Iterator<Item = I>,
R: Iterator<Item = Row<D, I>>,
{
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();

@ -94,10 +94,10 @@ impl<'a, T> Widget for Tabs<'a, T>
where
T: AsRef<str>,
{
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();

@ -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![

@ -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();
}

@ -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![

@ -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();

@ -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()

Loading…
Cancel
Save