feat: automatic state management

state
Florian Dehau 3 years ago
parent 8832281dcf
commit a8c75508e3

@ -5,7 +5,10 @@ use crate::util::event::{Event, Events};
use std::{error::Error, io}; use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{ use tui::{
backend::TermionBackend, buffer::Buffer, layout::Rect, style::Style, widgets::Widget, Terminal, backend::TermionBackend,
style::Style,
widgets::{RenderContext, Widget},
Terminal,
}; };
struct Label<'a> { struct Label<'a> {
@ -19,8 +22,11 @@ impl<'a> Default for Label<'a> {
} }
impl<'a> Widget for Label<'a> { impl<'a> Widget for Label<'a> {
fn render(self, area: Rect, buf: &mut Buffer) { type State = ();
buf.set_string(area.left(), area.top(), self.text, Style::default());
fn render(self, ctx: &mut RenderContext<Self::State>) {
ctx.buffer
.set_string(ctx.area.left(), ctx.area.top(), self.text, Style::default());
} }
} }

@ -1,4 +1,4 @@
use crate::util::{RandomSignal, SinSignal, StatefulList, TabsState}; use crate::util::{RandomSignal, SelectableList, SinSignal, TabsState};
const TASKS: [&str; 24] = [ const TASKS: [&str; 24] = [
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10", "Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
@ -110,8 +110,8 @@ pub struct App<'a> {
pub show_chart: bool, pub show_chart: bool,
pub progress: f64, pub progress: f64,
pub sparkline: Signal<RandomSignal>, pub sparkline: Signal<RandomSignal>,
pub tasks: StatefulList<&'a str>, pub tasks: SelectableList<&'a str>,
pub logs: StatefulList<(&'a str, &'a str)>, pub logs: Vec<(&'a str, &'a str)>,
pub signals: Signals, pub signals: Signals,
pub barchart: Vec<(&'a str, u64)>, pub barchart: Vec<(&'a str, u64)>,
pub servers: Vec<Server<'a>>, pub servers: Vec<Server<'a>>,
@ -137,8 +137,8 @@ impl<'a> App<'a> {
points: sparkline_points, points: sparkline_points,
tick_rate: 1, tick_rate: 1,
}, },
tasks: StatefulList::with_items(TASKS.to_vec()), tasks: SelectableList::with_items(TASKS.to_vec()),
logs: StatefulList::with_items(LOGS.to_vec()), logs: Vec::from(LOGS),
signals: Signals { signals: Signals {
sin1: Signal { sin1: Signal {
source: sin_signal, source: sin_signal,
@ -221,8 +221,8 @@ impl<'a> App<'a> {
self.sparkline.on_tick(); self.sparkline.on_tick();
self.signals.on_tick(); self.signals.on_tick();
let log = self.logs.items.pop().unwrap(); let log = self.logs.pop().unwrap();
self.logs.items.insert(0, log); self.logs.insert(0, log);
let event = self.barchart.pop().unwrap(); let event = self.barchart.pop().unwrap();
self.barchart.insert(0, event); self.barchart.insert(0, event);

@ -140,10 +140,11 @@ where
.map(|i| ListItem::new(vec![Spans::from(Span::raw(*i))])) .map(|i| ListItem::new(vec![Spans::from(Span::raw(*i))]))
.collect(); .collect();
let tasks = List::new(tasks) let tasks = List::new(tasks)
.block(Block::default().borders(Borders::ALL).title("List")) .select(app.tasks.selected)
.block(Block::default().borders(Borders::ALL).title("Tasks"))
.highlight_style(Style::default().add_modifier(Modifier::BOLD)) .highlight_style(Style::default().add_modifier(Modifier::BOLD))
.highlight_symbol("> "); .highlight_symbol("> ");
f.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state); f.render_widget(tasks, chunks[0]);
// Draw logs // Draw logs
let info_style = Style::default().fg(Color::Blue); let info_style = Style::default().fg(Color::Blue);
@ -152,7 +153,6 @@ where
let critical_style = Style::default().fg(Color::Red); let critical_style = Style::default().fg(Color::Red);
let logs: Vec<ListItem> = app let logs: Vec<ListItem> = app
.logs .logs
.items
.iter() .iter()
.map(|&(evt, level)| { .map(|&(evt, level)| {
let s = match level { let s = match level {
@ -168,8 +168,8 @@ where
ListItem::new(content) ListItem::new(content)
}) })
.collect(); .collect();
let logs = List::new(logs).block(Block::default().borders(Borders::ALL).title("List")); let logs = List::new(logs).block(Block::default().borders(Borders::ALL).title("Logs"));
f.render_stateful_widget(logs, chunks[1], &mut app.logs.state); f.render_widget(logs, chunks[1]);
} }
let barchart = BarChart::default() let barchart = BarChart::default()

@ -3,7 +3,7 @@ mod util;
use crate::util::{ use crate::util::{
event::{Event, Events}, event::{Event, Events},
StatefulList, SelectableList,
}; };
use std::{error::Error, io}; use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
@ -23,14 +23,14 @@ use tui::{
/// Check the event handling at the bottom to see how to change the state on incoming events. /// Check the event handling at the bottom to see how to change the state on incoming events.
/// Check the drawing logic for items on how to specify the highlighting style for selected items. /// Check the drawing logic for items on how to specify the highlighting style for selected items.
struct App<'a> { struct App<'a> {
items: StatefulList<(&'a str, usize)>, items: SelectableList<(&'a str, usize)>,
events: Vec<(&'a str, &'a str)>, events: Vec<(&'a str, &'a str)>,
} }
impl<'a> App<'a> { impl<'a> App<'a> {
fn new() -> App<'a> { fn new() -> App<'a> {
App { App {
items: StatefulList::with_items(vec![ items: SelectableList::with_items(vec![
("Item0", 1), ("Item0", 1),
("Item1", 2), ("Item1", 2),
("Item2", 1), ("Item2", 1),
@ -137,6 +137,7 @@ fn main() -> Result<(), Box<dyn Error>> {
// Create a List from all list items and highlight the currently selected one // Create a List from all list items and highlight the currently selected one
let items = List::new(items) let items = List::new(items)
.block(Block::default().borders(Borders::ALL).title("List")) .block(Block::default().borders(Borders::ALL).title("List"))
.select(app.items.selected)
.highlight_style( .highlight_style(
Style::default() Style::default()
.bg(Color::LightGreen) .bg(Color::LightGreen)
@ -145,7 +146,7 @@ fn main() -> Result<(), Box<dyn Error>> {
.highlight_symbol(">> "); .highlight_symbol(">> ");
// We can now render the item list // We can now render the item list
f.render_stateful_widget(items, chunks[0], &mut app.items.state); f.render_widget(items, chunks[0]);
// Let's do the same for the events. // Let's do the same for the events.
// The event list doesn't have any state and only displays the current state of the list. // The event list doesn't have any state and only displays the current state of the list.

@ -8,19 +8,19 @@ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Constraint, Layout}, layout::{Constraint, Layout},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
widgets::{Block, Borders, Cell, Row, Table, TableState}, widgets::{Block, Borders, Cell, Row, Table},
Terminal, Terminal,
}; };
pub struct StatefulTable<'a> { pub struct SelectableTable<'a> {
state: TableState, selected: Option<usize>,
items: Vec<Vec<&'a str>>, items: Vec<Vec<&'a str>>,
} }
impl<'a> StatefulTable<'a> { impl<'a> SelectableTable<'a> {
fn new() -> StatefulTable<'a> { fn new() -> SelectableTable<'a> {
StatefulTable { SelectableTable {
state: TableState::default(), selected: None,
items: vec![ items: vec![
vec!["Row11", "Row12", "Row13"], vec!["Row11", "Row12", "Row13"],
vec!["Row21", "Row22", "Row23"], vec!["Row21", "Row22", "Row23"],
@ -44,32 +44,19 @@ impl<'a> StatefulTable<'a> {
], ],
} }
} }
pub fn next(&mut self) { pub fn next(&mut self) {
let i = match self.state.selected() { self.selected = self
Some(i) => { .selected
if i >= self.items.len() - 1 { .map(|i| if i >= self.items.len() - 1 { 0 } else { i + 1 })
0 .or(Some(0));
} else {
i + 1
}
}
None => 0,
};
self.state.select(Some(i));
} }
pub fn previous(&mut self) { pub fn previous(&mut self) {
let i = match self.state.selected() { self.selected = self
Some(i) => { .selected
if i == 0 { .map(|i| if i == 0 { self.items.len() - 1 } else { i - 1 })
self.items.len() - 1 .or(Some(0));
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(i));
} }
} }
@ -83,7 +70,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let events = Events::new(); let events = Events::new();
let mut table = StatefulTable::new(); let mut table = SelectableTable::new();
// Input // Input
loop { loop {
@ -117,12 +104,13 @@ fn main() -> Result<(), Box<dyn Error>> {
.block(Block::default().borders(Borders::ALL).title("Table")) .block(Block::default().borders(Borders::ALL).title("Table"))
.highlight_style(selected_style) .highlight_style(selected_style)
.highlight_symbol(">> ") .highlight_symbol(">> ")
.select(table.selected)
.widths(&[ .widths(&[
Constraint::Percentage(50), Constraint::Percentage(50),
Constraint::Length(30), Constraint::Length(30),
Constraint::Max(10), Constraint::Max(10),
]); ]);
f.render_stateful_widget(t, rects[0], &mut table.state); f.render_widget(t, rects[0]);
})?; })?;
if let Event::Input(key) = events.next()? { if let Event::Input(key) = events.next()? {

@ -3,7 +3,6 @@ pub mod event;
use rand::distributions::{Distribution, Uniform}; use rand::distributions::{Distribution, Uniform};
use rand::rngs::ThreadRng; use rand::rngs::ThreadRng;
use tui::widgets::ListState;
#[derive(Clone)] #[derive(Clone)]
pub struct RandomSignal { pub struct RandomSignal {
@ -77,55 +76,53 @@ impl<'a> TabsState<'a> {
} }
} }
pub struct StatefulList<T> { pub struct SelectableList<T> {
pub state: ListState, pub selected: Option<usize>,
pub items: Vec<T>, pub items: Vec<T>,
} }
impl<T> StatefulList<T> { impl<T> SelectableList<T> {
pub fn new() -> StatefulList<T> { pub fn new() -> SelectableList<T> {
StatefulList { Self {
state: ListState::default(), selected: None,
items: Vec::new(), items: Vec::new(),
} }
} }
pub fn with_items(items: Vec<T>) -> StatefulList<T> { pub fn with_items(items: Vec<T>) -> SelectableList<T> {
StatefulList { Self {
state: ListState::default(), selected: None,
items, items,
} }
} }
pub fn next(&mut self) { pub fn next(&mut self) {
let i = match self.state.selected() { self.selected = self
Some(i) => { .selected
if i >= self.items.len() - 1 { .map(|i| {
0 if i < self.items.len().saturating_sub(1) {
} else {
i + 1 i + 1
} else {
0
} }
} })
None => 0, .or(Some(0));
};
self.state.select(Some(i));
} }
pub fn previous(&mut self) { pub fn previous(&mut self) {
let i = match self.state.selected() { self.selected = self
Some(i) => { .selected
if i == 0 { .map(|i| {
self.items.len() - 1 if i > 0 {
} else {
i - 1 i - 1
} else {
self.items.len().saturating_sub(1)
} }
} })
None => 0, .or(Some(0));
};
self.state.select(Some(i));
} }
pub fn unselect(&mut self) { pub fn unselect(&mut self) {
self.state.select(None); self.selected = None;
} }
} }

@ -156,4 +156,4 @@ pub mod terminal;
pub mod text; pub mod text;
pub mod widgets; pub mod widgets;
pub use self::terminal::{Frame, Terminal, TerminalOptions, Viewport}; pub use self::terminal::{Frame, RenderArgs, Terminal, TerminalOptions, Viewport};

@ -2,9 +2,9 @@ use crate::{
backend::Backend, backend::Backend,
buffer::Buffer, buffer::Buffer,
layout::Rect, layout::Rect,
widgets::{StatefulWidget, Widget}, widgets::{RenderContext, Widget},
}; };
use std::io; use std::{any::Any, collections::HashMap, hash::Hash, io, panic::Location};
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
/// UNSTABLE /// UNSTABLE
@ -30,6 +30,46 @@ impl Viewport {
} }
} }
#[derive(Clone, Copy, Debug)]
pub(crate) struct CallLocation(&'static Location<'static>);
impl CallLocation {
fn as_ptr(&self) -> *const Location<'static> {
self.0
}
}
impl Hash for CallLocation {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.as_ptr().hash(state)
}
}
impl PartialEq for CallLocation {
fn eq(&self, other: &Self) -> bool {
self.as_ptr() == other.as_ptr()
}
}
impl Eq for CallLocation {}
/// StateEntry is used to link a [`Frame::render_widget`] to [`Widget::State`].
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct StateKey {
/// Location of the call to [`Frame::render_widget`].
call_location: CallLocation,
/// Optional id that can be used to have multiple widgets state at the same call location.
id: Option<String>,
}
/// StateEntry holds the state of a [`Widget`].
struct StateEntry {
/// State of a [`Widget`].
state: Box<dyn Any>,
/// Index of the frame where the state was used for the last time.
frame_index: usize,
}
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
/// Options to pass to [`Terminal::with_options`] /// Options to pass to [`Terminal::with_options`]
pub struct TerminalOptions { pub struct TerminalOptions {
@ -38,7 +78,6 @@ pub struct TerminalOptions {
} }
/// Interface to the terminal backed by Termion /// Interface to the terminal backed by Termion
#[derive(Debug)]
pub struct Terminal<B> pub struct Terminal<B>
where where
B: Backend, B: Backend,
@ -53,6 +92,11 @@ where
hidden_cursor: bool, hidden_cursor: bool,
/// Viewport /// Viewport
viewport: Viewport, viewport: Viewport,
/// State of the widgets rendered in the previous frame.
widget_states: HashMap<StateKey, StateEntry>,
/// Index of the current frame. Incremented each time [`Terminal::draw`] is called and wraps
/// when it is greater than [`std::usize::MAX`].
frame_index: usize,
} }
/// Represents a consistent terminal interface for rendering. /// Represents a consistent terminal interface for rendering.
@ -69,6 +113,31 @@ where
cursor_position: Option<(u16, u16)>, cursor_position: Option<(u16, u16)>,
} }
/// RenderArgs are the arguments required to render a [`Widget`].
pub struct RenderArgs {
/// Area where the widget will be rendered.
area: Rect,
/// Optional id that can be used to uniquely identify the provided [`Widget`].
id: Option<String>,
}
impl From<Rect> for RenderArgs {
fn from(area: Rect) -> RenderArgs {
RenderArgs { area, id: None }
}
}
impl RenderArgs {
/// Set the [`Widget`] id.
pub fn id<S>(mut self, id: S) -> Self
where
S: Into<String>,
{
self.id = Some(id.into());
self
}
}
impl<'a, B> Frame<'a, B> impl<'a, B> Frame<'a, B>
where where
B: Backend, B: Backend,
@ -96,45 +165,79 @@ where
/// let mut frame = terminal.get_frame(); /// let mut frame = terminal.get_frame();
/// frame.render_widget(block, area); /// frame.render_widget(block, area);
/// ``` /// ```
pub fn render_widget<W>(&mut self, widget: W, area: Rect)
where
W: Widget,
{
widget.render(area, self.terminal.current_buffer_mut());
}
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
/// ///
/// The last argument should be an instance of the [`StatefulWidget::State`] associated to the /// If you happen to render two or more widgets using the same render call, you may want to
/// given [`StatefulWidget`]. /// associate them with a unique id so they do not share any internal state.
///
/// # Examples
/// ///
/// For example, let say your app shows a list of songs of a given album:
/// ```rust,no_run /// ```rust,no_run
/// # use std::io; /// # use std::{collections::HashMap, io};
/// # use tui::Terminal; /// # use tui::{Terminal, RenderArgs};
/// # use tui::backend::TermionBackend; /// # use tui::backend::TermionBackend;
/// # use tui::layout::Rect; /// # use tui::layout::Rect;
/// # use tui::widgets::{List, ListItem, ListState}; /// # use tui::widgets::{Block, List, ListItem};
/// # let stdout = io::stdout(); /// # let stdout = io::stdout();
/// # let backend = TermionBackend::new(stdout); /// # let backend = TermionBackend::new(stdout);
/// # let mut terminal = Terminal::new(backend).unwrap(); /// # let mut terminal = Terminal::new(backend).unwrap();
/// let mut state = ListState::default(); /// struct App {
/// state.select(Some(1)); /// albums: HashMap<String, Vec<String>>,
/// let items = vec![ /// selected_album: String
/// ListItem::new("Item 1"), /// }
/// ListItem::new("Item 2"), /// # let app = App {
/// ]; /// # albums: HashMap::new(),
/// let list = List::new(items); /// # selected_album: String::new(),
/// let area = Rect::new(0, 0, 5, 5); /// # };
/// let mut frame = terminal.get_frame(); /// terminal.draw(|f| {
/// frame.render_stateful_widget(list, area, &mut state); /// let songs: Vec<ListItem> = app.albums[&app.selected_album]
/// .iter()
/// .map(|song| ListItem::new(song.as_ref()))
/// .collect();
/// let song_list = List::new(songs)
/// .block(Block::default().title(app.selected_album.as_ref()));
/// // Giving a unique id here makes sure the list state is reset whenever the album
/// // currently displayed changes.
/// let args = RenderArgs::from(f.size()).id(app.selected_album.clone());
/// f.render_widget(song_list, args);
/// });
/// ``` /// ```
pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State) #[track_caller]
pub fn render_widget<W, R>(&mut self, widget: W, args: R)
where where
W: StatefulWidget, W: Widget,
W::State: 'static + Default,
R: Into<RenderArgs>,
{ {
widget.render(area, self.terminal.current_buffer_mut(), state); // Fetch the previous internal state of the widget (or initialize it with a default value).
let args: RenderArgs = args.into();
let location = Location::caller();
let key = StateKey {
call_location: CallLocation(location),
id: args.id,
};
let entry = self
.terminal
.widget_states
.entry(key)
.or_insert_with(|| StateEntry {
state: Box::new(<W::State>::default()),
frame_index: 0,
});
let state: &mut W::State = entry
.state
.downcast_mut()
.expect("The state associated to a widget is not of an expected type");
// Update the frame index to communicate that it was used during the current draw call.
entry.frame_index = self.terminal.frame_index;
// Render the widget
let buffer = &mut self.terminal.buffers[self.terminal.current];
let mut context = RenderContext {
area: args.area,
buffer,
state,
};
widget.render(&mut context);
} }
/// After drawing this frame, make the cursor visible and put it at the specified (x, y) /// After drawing this frame, make the cursor visible and put it at the specified (x, y)
@ -200,6 +303,8 @@ where
current: 0, current: 0,
hidden_cursor: false, hidden_cursor: false,
viewport: options.viewport, viewport: options.viewport,
widget_states: HashMap::new(),
frame_index: 0,
}) })
} }
@ -285,6 +390,12 @@ where
self.buffers[1 - self.current].reset(); self.buffers[1 - self.current].reset();
self.current = 1 - self.current; self.current = 1 - self.current;
// Clean states that were not used in this frame
let frame_index = self.frame_index;
self.widget_states
.retain(|_, v| v.frame_index == frame_index);
self.frame_index = self.frame_index.wrapping_add(1);
// Flush // Flush
self.backend.flush()?; self.backend.flush()?;
Ok(CompletedFrame { Ok(CompletedFrame {

@ -1,9 +1,7 @@
use crate::{ use crate::{
buffer::Buffer,
layout::Rect,
style::Style, style::Style,
symbols, symbols,
widgets::{Block, Widget}, widgets::{Block, RenderContext, Widget},
}; };
use std::cmp::min; use std::cmp::min;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
@ -127,16 +125,22 @@ impl<'a> BarChart<'a> {
} }
impl<'a> Widget for BarChart<'a> { impl<'a> Widget for BarChart<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) { type State = ();
buf.set_style(area, self.style);
fn render(mut self, ctx: &mut RenderContext<Self::State>) {
ctx.buffer.set_style(ctx.area, self.style);
let chart_area = match self.block.take() { let chart_area = match self.block.take() {
Some(b) => { Some(b) => {
let inner_area = b.inner(area); let inner_area = b.inner(ctx.area);
b.render(area, buf); b.render(&mut RenderContext {
area: ctx.area,
buffer: ctx.buffer,
state: &mut (),
});
inner_area inner_area
} }
None => area, None => ctx.area,
}; };
if chart_area.height < 2 { if chart_area.height < 2 {
@ -176,12 +180,13 @@ impl<'a> Widget for BarChart<'a> {
}; };
for x in 0..self.bar_width { for x in 0..self.bar_width {
buf.get_mut( ctx.buffer
chart_area.left() + i as u16 * (self.bar_width + self.bar_gap) + x, .get_mut(
chart_area.top() + j, chart_area.left() + i as u16 * (self.bar_width + self.bar_gap) + x,
) chart_area.top() + j,
.set_symbol(symbol) )
.set_style(self.bar_style); .set_symbol(symbol)
.set_style(self.bar_style);
} }
if d.1 > 8 { if d.1 > 8 {
@ -197,7 +202,7 @@ impl<'a> Widget for BarChart<'a> {
let value_label = &self.values[i]; let value_label = &self.values[i];
let width = value_label.width() as u16; let width = value_label.width() as u16;
if width < self.bar_width { if width < self.bar_width {
buf.set_string( ctx.buffer.set_string(
chart_area.left() chart_area.left()
+ i as u16 * (self.bar_width + self.bar_gap) + i as u16 * (self.bar_width + self.bar_gap)
+ (self.bar_width - width) / 2, + (self.bar_width - width) / 2,
@ -207,7 +212,7 @@ impl<'a> Widget for BarChart<'a> {
); );
} }
} }
buf.set_stringn( ctx.buffer.set_stringn(
chart_area.left() + i as u16 * (self.bar_width + self.bar_gap), chart_area.left() + i as u16 * (self.bar_width + self.bar_gap),
chart_area.bottom() - 1, chart_area.bottom() - 1,
label, label,

@ -1,10 +1,9 @@
use crate::{ use crate::{
buffer::Buffer,
layout::Rect, layout::Rect,
style::Style, style::Style,
symbols::line, symbols::line,
text::{Span, Spans}, text::{Span, Spans},
widgets::{Borders, Widget}, widgets::{Borders, RenderContext, Widget},
}; };
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
@ -131,40 +130,46 @@ impl<'a> Block<'a> {
} }
impl<'a> Widget for Block<'a> { impl<'a> Widget for Block<'a> {
fn render(self, area: Rect, buf: &mut Buffer) { type State = ();
if area.area() == 0 {
fn render(self, ctx: &mut RenderContext<Self::State>) {
if ctx.area.area() == 0 {
return; return;
} }
buf.set_style(area, self.style); ctx.buffer.set_style(ctx.area, self.style);
let symbols = BorderType::line_symbols(self.border_type); let symbols = BorderType::line_symbols(self.border_type);
// Sides // Sides
if self.borders.intersects(Borders::LEFT) { if self.borders.intersects(Borders::LEFT) {
for y in area.top()..area.bottom() { for y in ctx.area.top()..ctx.area.bottom() {
buf.get_mut(area.left(), y) ctx.buffer
.get_mut(ctx.area.left(), y)
.set_symbol(symbols.vertical) .set_symbol(symbols.vertical)
.set_style(self.border_style); .set_style(self.border_style);
} }
} }
if self.borders.intersects(Borders::TOP) { if self.borders.intersects(Borders::TOP) {
for x in area.left()..area.right() { for x in ctx.area.left()..ctx.area.right() {
buf.get_mut(x, area.top()) ctx.buffer
.get_mut(x, ctx.area.top())
.set_symbol(symbols.horizontal) .set_symbol(symbols.horizontal)
.set_style(self.border_style); .set_style(self.border_style);
} }
} }
if self.borders.intersects(Borders::RIGHT) { if self.borders.intersects(Borders::RIGHT) {
let x = area.right() - 1; let x = ctx.area.right() - 1;
for y in area.top()..area.bottom() { for y in ctx.area.top()..ctx.area.bottom() {
buf.get_mut(x, y) ctx.buffer
.get_mut(x, y)
.set_symbol(symbols.vertical) .set_symbol(symbols.vertical)
.set_style(self.border_style); .set_style(self.border_style);
} }
} }
if self.borders.intersects(Borders::BOTTOM) { if self.borders.intersects(Borders::BOTTOM) {
let y = area.bottom() - 1; let y = ctx.area.bottom() - 1;
for x in area.left()..area.right() { for x in ctx.area.left()..ctx.area.right() {
buf.get_mut(x, y) ctx.buffer
.get_mut(x, y)
.set_symbol(symbols.horizontal) .set_symbol(symbols.horizontal)
.set_style(self.border_style); .set_style(self.border_style);
} }
@ -172,22 +177,26 @@ impl<'a> Widget for Block<'a> {
// Corners // Corners
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) { if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
buf.get_mut(area.right() - 1, area.bottom() - 1) ctx.buffer
.get_mut(ctx.area.right() - 1, ctx.area.bottom() - 1)
.set_symbol(symbols.bottom_right) .set_symbol(symbols.bottom_right)
.set_style(self.border_style); .set_style(self.border_style);
} }
if self.borders.contains(Borders::RIGHT | Borders::TOP) { if self.borders.contains(Borders::RIGHT | Borders::TOP) {
buf.get_mut(area.right() - 1, area.top()) ctx.buffer
.get_mut(ctx.area.right() - 1, ctx.area.top())
.set_symbol(symbols.top_right) .set_symbol(symbols.top_right)
.set_style(self.border_style); .set_style(self.border_style);
} }
if self.borders.contains(Borders::LEFT | Borders::BOTTOM) { if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
buf.get_mut(area.left(), area.bottom() - 1) ctx.buffer
.get_mut(ctx.area.left(), ctx.area.bottom() - 1)
.set_symbol(symbols.bottom_left) .set_symbol(symbols.bottom_left)
.set_style(self.border_style); .set_style(self.border_style);
} }
if self.borders.contains(Borders::LEFT | Borders::TOP) { if self.borders.contains(Borders::LEFT | Borders::TOP) {
buf.get_mut(area.left(), area.top()) ctx.buffer
.get_mut(ctx.area.left(), ctx.area.top())
.set_symbol(symbols.top_left) .set_symbol(symbols.top_left)
.set_style(self.border_style); .set_style(self.border_style);
} }
@ -203,8 +212,9 @@ impl<'a> Widget for Block<'a> {
} else { } else {
0 0
}; };
let width = area.width.saturating_sub(lx).saturating_sub(rx); let width = ctx.area.width.saturating_sub(lx).saturating_sub(rx);
buf.set_spans(area.left() + lx, area.top(), &title, width); ctx.buffer
.set_spans(ctx.area.left() + lx, ctx.area.top(), &title, width);
} }
} }
} }

@ -10,11 +10,9 @@ pub use self::points::Points;
pub use self::rectangle::Rectangle; pub use self::rectangle::Rectangle;
use crate::{ use crate::{
buffer::Buffer,
layout::Rect,
style::{Color, Style}, style::{Color, Style},
symbols, symbols,
widgets::{Block, Widget}, widgets::{Block, RenderContext, Widget},
}; };
use std::fmt::Debug; use std::fmt::Debug;
@ -423,14 +421,20 @@ impl<'a, F> Widget for Canvas<'a, F>
where where
F: Fn(&mut Context), F: Fn(&mut Context),
{ {
fn render(mut self, area: Rect, buf: &mut Buffer) { type State = ();
fn render(mut self, ctx: &mut RenderContext<Self::State>) {
let canvas_area = match self.block.take() { let canvas_area = match self.block.take() {
Some(b) => { Some(b) => {
let inner_area = b.inner(area); let inner_area = b.inner(ctx.area);
b.render(area, buf); b.render(&mut RenderContext {
area: ctx.area,
buffer: ctx.buffer,
state: &mut (),
});
inner_area inner_area
} }
None => area, None => ctx.area,
}; };
let width = canvas_area.width as usize; let width = canvas_area.width as usize;
@ -441,7 +445,7 @@ where
}; };
// Create a blank context that match the size of the canvas // Create a blank context that match the size of the canvas
let mut ctx = Context::new( let mut canvas_ctx = Context::new(
canvas_area.width, canvas_area.width,
canvas_area.height, canvas_area.height,
self.x_bounds, self.x_bounds,
@ -449,11 +453,11 @@ where
self.marker, self.marker,
); );
// Paint to this context // Paint to this context
painter(&mut ctx); painter(&mut canvas_ctx);
ctx.finish(); canvas_ctx.finish();
// Retreive painted points for each layer // Retreive painted points for each layer
for layer in ctx.layers { for layer in canvas_ctx.layers {
for (i, (ch, color)) in layer for (i, (ch, color)) in layer
.string .string
.chars() .chars()
@ -462,7 +466,8 @@ where
{ {
if ch != ' ' && ch != '\u{2800}' { if ch != ' ' && ch != '\u{2800}' {
let (x, y) = (i % width, i / width); let (x, y) = (i % width, i / width);
buf.get_mut(x as u16 + canvas_area.left(), y as u16 + canvas_area.top()) ctx.buffer
.get_mut(x as u16 + canvas_area.left(), y as u16 + canvas_area.top())
.set_char(ch) .set_char(ch)
.set_fg(color) .set_fg(color)
.set_bg(self.background_color); .set_bg(self.background_color);
@ -483,14 +488,14 @@ where
let height = f64::from(canvas_area.height - 1); let height = f64::from(canvas_area.height - 1);
(width, height) (width, height)
}; };
for label in ctx for label in canvas_ctx
.labels .labels
.iter() .iter()
.filter(|l| l.x >= left && l.x <= right && l.y <= top && l.y >= bottom) .filter(|l| l.x >= left && l.x <= right && l.y <= top && l.y >= bottom)
{ {
let x = ((label.x - left) * resolution.0 / width) as u16 + canvas_area.left(); let x = ((label.x - left) * resolution.0 / width) as u16 + canvas_area.left();
let y = ((top - label.y) * resolution.1 / height) as u16 + canvas_area.top(); let y = ((top - label.y) * resolution.1 / height) as u16 + canvas_area.top();
buf.set_stringn( ctx.buffer.set_stringn(
x, x,
y, y,
label.text, label.text,

@ -1,12 +1,11 @@
use crate::{ use crate::{
buffer::Buffer,
layout::{Constraint, Rect}, layout::{Constraint, Rect},
style::{Color, Style}, style::{Color, Style},
symbols, symbols,
text::{Span, Spans}, text::{Span, Spans},
widgets::{ widgets::{
canvas::{Canvas, Line, Points}, canvas::{Canvas, Line, Points},
Block, Borders, Widget, Block, Borders, RenderContext, Widget,
}, },
}; };
use std::{borrow::Cow, cmp::max}; use std::{borrow::Cow, cmp::max};
@ -365,23 +364,29 @@ impl<'a> Chart<'a> {
} }
impl<'a> Widget for Chart<'a> { impl<'a> Widget for Chart<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) { type State = ();
if area.area() == 0 {
fn render(mut self, ctx: &mut RenderContext<Self::State>) {
if ctx.area.area() == 0 {
return; return;
} }
buf.set_style(area, self.style); ctx.buffer.set_style(ctx.area, self.style);
// Sample the style of the entire widget. This sample will be used to reset the style of // Sample the style of the entire widget. This sample will be used to reset the style of
// the cells that are part of the components put on top of the grah area (i.e legend and // the cells that are part of the components put on top of the grah area (i.e legend and
// axis names). // axis names).
let original_style = buf.get(area.left(), area.top()).style(); let original_style = ctx.buffer.get(ctx.area.left(), ctx.area.top()).style();
let chart_area = match self.block.take() { let chart_area = match self.block.take() {
Some(b) => { Some(b) => {
let inner_area = b.inner(area); let inner_area = b.inner(ctx.area);
b.render(area, buf); b.render(&mut RenderContext {
area: ctx.area,
buffer: ctx.buffer,
state: &mut (),
});
inner_area inner_area
} }
None => area, None => ctx.area,
}; };
let layout = self.layout(chart_area); let layout = self.layout(chart_area);
@ -396,7 +401,7 @@ impl<'a> Widget for Chart<'a> {
let labels_len = labels.len() as u16; let labels_len = labels.len() as u16;
if total_width < graph_area.width && labels_len > 1 { if total_width < graph_area.width && labels_len > 1 {
for (i, label) in labels.iter().enumerate() { for (i, label) in labels.iter().enumerate() {
buf.set_span( ctx.buffer.set_span(
graph_area.left() + i as u16 * (graph_area.width - 1) / (labels_len - 1) graph_area.left() + i as u16 * (graph_area.width - 1) / (labels_len - 1)
- label.content.width() as u16, - label.content.width() as u16,
y, y,
@ -413,14 +418,20 @@ impl<'a> Widget for Chart<'a> {
for (i, label) in labels.iter().enumerate() { for (i, label) in labels.iter().enumerate() {
let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1); let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
if dy < graph_area.bottom() { if dy < graph_area.bottom() {
buf.set_span(x, graph_area.bottom() - 1 - dy, label, label.width() as u16); ctx.buffer.set_span(
x,
graph_area.bottom() - 1 - dy,
label,
label.width() as u16,
);
} }
} }
} }
if let Some(y) = layout.axis_x { if let Some(y) = layout.axis_x {
for x in graph_area.left()..graph_area.right() { for x in graph_area.left()..graph_area.right() {
buf.get_mut(x, y) ctx.buffer
.get_mut(x, y)
.set_symbol(symbols::line::HORIZONTAL) .set_symbol(symbols::line::HORIZONTAL)
.set_style(self.x_axis.style); .set_style(self.x_axis.style);
} }
@ -428,7 +439,8 @@ impl<'a> Widget for Chart<'a> {
if let Some(x) = layout.axis_y { if let Some(x) = layout.axis_y {
for y in graph_area.top()..graph_area.bottom() { for y in graph_area.top()..graph_area.bottom() {
buf.get_mut(x, y) ctx.buffer
.get_mut(x, y)
.set_symbol(symbols::line::VERTICAL) .set_symbol(symbols::line::VERTICAL)
.set_style(self.y_axis.style); .set_style(self.y_axis.style);
} }
@ -436,7 +448,8 @@ impl<'a> Widget for Chart<'a> {
if let Some(y) = layout.axis_x { if let Some(y) = layout.axis_x {
if let Some(x) = layout.axis_y { if let Some(x) = layout.axis_y {
buf.get_mut(x, y) ctx.buffer
.get_mut(x, y)
.set_symbol(symbols::line::BOTTOM_LEFT) .set_symbol(symbols::line::BOTTOM_LEFT)
.set_style(self.x_axis.style); .set_style(self.x_axis.style);
} }
@ -465,16 +478,24 @@ impl<'a> Widget for Chart<'a> {
} }
} }
}) })
.render(graph_area, buf); .render(&mut RenderContext {
area: graph_area,
buffer: ctx.buffer,
state: &mut (),
});
} }
if let Some(legend_area) = layout.legend_area { if let Some(legend_area) = layout.legend_area {
buf.set_style(legend_area, original_style); ctx.buffer.set_style(legend_area, original_style);
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.render(legend_area, buf); .render(&mut RenderContext {
area: legend_area,
buffer: ctx.buffer,
state: &mut (),
});
for (i, dataset) in self.datasets.iter().enumerate() { for (i, dataset) in self.datasets.iter().enumerate() {
buf.set_string( ctx.buffer.set_string(
legend_area.x + 1, legend_area.x + 1,
legend_area.y + 1 + i as u16, legend_area.y + 1 + i as u16,
&dataset.name, &dataset.name,
@ -486,7 +507,7 @@ impl<'a> Widget for Chart<'a> {
if let Some((x, y)) = layout.title_x { if let Some((x, y)) = layout.title_x {
let title = self.x_axis.title.unwrap(); let title = self.x_axis.title.unwrap();
let width = graph_area.right().saturating_sub(x); let width = graph_area.right().saturating_sub(x);
buf.set_style( ctx.buffer.set_style(
Rect { Rect {
x, x,
y, y,
@ -495,13 +516,13 @@ impl<'a> Widget for Chart<'a> {
}, },
original_style, original_style,
); );
buf.set_spans(x, y, &title, width); ctx.buffer.set_spans(x, y, &title, width);
} }
if let Some((x, y)) = layout.title_y { if let Some((x, y)) = layout.title_y {
let title = self.y_axis.title.unwrap(); let title = self.y_axis.title.unwrap();
let width = graph_area.right().saturating_sub(x); let width = graph_area.right().saturating_sub(x);
buf.set_style( ctx.buffer.set_style(
Rect { Rect {
x, x,
y, y,
@ -510,7 +531,7 @@ impl<'a> Widget for Chart<'a> {
}, },
original_style, original_style,
); );
buf.set_spans(x, y, &title, width); ctx.buffer.set_spans(x, y, &title, width);
} }
} }
} }

@ -1,6 +1,4 @@
use crate::buffer::Buffer; use crate::widgets::{RenderContext, Widget};
use crate::layout::Rect;
use crate::widgets::Widget;
/// A widget to to clear/reset a certain area to allow overdrawing (e.g. for popups) /// A widget to to clear/reset a certain area to allow overdrawing (e.g. for popups)
/// ///
@ -26,10 +24,12 @@ use crate::widgets::Widget;
pub struct Clear; pub struct Clear;
impl Widget for Clear { impl Widget for Clear {
fn render(self, area: Rect, buf: &mut Buffer) { type State = ();
for x in area.left()..area.right() {
for y in area.top()..area.bottom() { fn render(self, ctx: &mut RenderContext<Self::State>) {
buf.get_mut(x, y).reset(); for x in ctx.area.left()..ctx.area.right() {
for y in ctx.area.top()..ctx.area.bottom() {
ctx.buffer.get_mut(x, y).reset();
} }
} }
} }

@ -1,10 +1,8 @@
use crate::{ use crate::{
buffer::Buffer,
layout::Rect,
style::{Color, Style}, style::{Color, Style},
symbols, symbols,
text::{Span, Spans}, text::{Span, Spans},
widgets::{Block, Widget}, widgets::{Block, RenderContext, Widget},
}; };
/// A widget to display a task progress. /// A widget to display a task progress.
@ -92,17 +90,23 @@ impl<'a> Gauge<'a> {
} }
impl<'a> Widget for Gauge<'a> { impl<'a> Widget for Gauge<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) { type State = ();
buf.set_style(area, self.style);
fn render(mut self, ctx: &mut RenderContext<Self::State>) {
ctx.buffer.set_style(ctx.area, self.style);
let gauge_area = match self.block.take() { let gauge_area = match self.block.take() {
Some(b) => { Some(b) => {
let inner_area = b.inner(area); let inner_area = b.inner(ctx.area);
b.render(area, buf); b.render(&mut RenderContext {
area: ctx.area,
buffer: ctx.buffer,
state: &mut (),
});
inner_area inner_area
} }
None => area, None => ctx.area,
}; };
buf.set_style(gauge_area, self.gauge_style); ctx.buffer.set_style(gauge_area, self.gauge_style);
if gauge_area.height < 1 { if gauge_area.height < 1 {
return; return;
} }
@ -124,12 +128,13 @@ impl<'a> Widget for Gauge<'a> {
for y in gauge_area.top()..gauge_area.bottom() { for y in gauge_area.top()..gauge_area.bottom() {
// Gauge // Gauge
for x in gauge_area.left()..end { for x in gauge_area.left()..end {
buf.get_mut(x, y).set_symbol(" "); ctx.buffer.get_mut(x, y).set_symbol(" ");
} }
//set unicode block //set unicode block
if self.use_unicode && self.ratio < 1.0 { if self.use_unicode && self.ratio < 1.0 {
buf.get_mut(end, y) ctx.buffer
.get_mut(end, y)
.set_symbol(get_unicode_block(width % 1.0)); .set_symbol(get_unicode_block(width % 1.0));
} }
@ -138,7 +143,8 @@ impl<'a> Widget for Gauge<'a> {
if y == center { if y == center {
let label_width = label.width() as u16; let label_width = label.width() as u16;
let middle = (gauge_area.width - label_width) / 2 + gauge_area.left(); let middle = (gauge_area.width - label_width) / 2 + gauge_area.left();
buf.set_span(middle, y, &label, gauge_area.right() - middle); ctx.buffer
.set_span(middle, y, &label, gauge_area.right() - middle);
if self.use_unicode && end >= middle && end < middle + label_width { if self.use_unicode && end >= middle && end < middle + label_width {
color_end = gauge_area.left() + (width.round() as u16); //set color on the label to the rounded gauge level color_end = gauge_area.left() + (width.round() as u16); //set color on the label to the rounded gauge level
} }
@ -146,7 +152,8 @@ impl<'a> Widget for Gauge<'a> {
// Fix colors // Fix colors
for x in gauge_area.left()..color_end { for x in gauge_area.left()..color_end {
buf.get_mut(x, y) ctx.buffer
.get_mut(x, y)
.set_fg(self.gauge_style.bg.unwrap_or(Color::Reset)) .set_fg(self.gauge_style.bg.unwrap_or(Color::Reset))
.set_bg(self.gauge_style.fg.unwrap_or(Color::Reset)); .set_bg(self.gauge_style.fg.unwrap_or(Color::Reset));
} }
@ -245,15 +252,21 @@ impl<'a> LineGauge<'a> {
} }
impl<'a> Widget for LineGauge<'a> { impl<'a> Widget for LineGauge<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) { type State = ();
buf.set_style(area, self.style);
fn render(mut self, ctx: &mut RenderContext<Self::State>) {
ctx.buffer.set_style(ctx.area, self.style);
let gauge_area = match self.block.take() { let gauge_area = match self.block.take() {
Some(b) => { Some(b) => {
let inner_area = b.inner(area); let inner_area = b.inner(ctx.area);
b.render(area, buf); b.render(&mut RenderContext {
area: ctx.area,
buffer: ctx.buffer,
state: &mut (),
});
inner_area inner_area
} }
None => area, None => ctx.area,
}; };
if gauge_area.height < 1 { if gauge_area.height < 1 {
@ -264,7 +277,7 @@ impl<'a> Widget for LineGauge<'a> {
let label = self let label = self
.label .label
.unwrap_or_else(move || Spans::from(format!("{:.0}%", ratio * 100.0))); .unwrap_or_else(move || Spans::from(format!("{:.0}%", ratio * 100.0)));
let (col, row) = buf.set_spans( let (col, row) = ctx.buffer.set_spans(
gauge_area.left(), gauge_area.left(),
gauge_area.top(), gauge_area.top(),
&label, &label,
@ -278,7 +291,8 @@ impl<'a> Widget for LineGauge<'a> {
let end = start let end = start
+ (f64::from(gauge_area.right().saturating_sub(start)) * self.ratio).floor() as u16; + (f64::from(gauge_area.right().saturating_sub(start)) * self.ratio).floor() as u16;
for col in start..end { for col in start..end {
buf.get_mut(col, row) ctx.buffer
.get_mut(col, row)
.set_symbol(self.line_set.horizontal) .set_symbol(self.line_set.horizontal)
.set_style(Style { .set_style(Style {
fg: self.gauge_style.fg, fg: self.gauge_style.fg,
@ -288,7 +302,8 @@ impl<'a> Widget for LineGauge<'a> {
}); });
} }
for col in end..gauge_area.right() { for col in end..gauge_area.right() {
buf.get_mut(col, row) ctx.buffer
.get_mut(col, row)
.set_symbol(self.line_set.horizontal) .set_symbol(self.line_set.horizontal)
.set_style(Style { .set_style(Style {
fg: self.gauge_style.bg, fg: self.gauge_style.bg,

@ -1,9 +1,8 @@
use crate::{ use crate::{
buffer::Buffer,
layout::{Corner, Rect}, layout::{Corner, Rect},
style::Style, style::Style,
text::Text, text::Text,
widgets::{Block, StatefulWidget, Widget}, widgets::{Block, RenderContext, Widget},
}; };
use std::iter::{self, Iterator}; use std::iter::{self, Iterator};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
@ -11,28 +10,11 @@ use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ListState { pub struct ListState {
offset: usize, offset: usize,
selected: Option<usize>,
} }
impl Default for ListState { impl Default for ListState {
fn default() -> ListState { fn default() -> ListState {
ListState { ListState { offset: 0 }
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;
}
} }
} }
@ -88,6 +70,7 @@ pub struct List<'a> {
highlight_style: Style, highlight_style: Style,
/// Symbol in front of the selected item (Shift all items to the right) /// Symbol in front of the selected item (Shift all items to the right)
highlight_symbol: Option<&'a str>, highlight_symbol: Option<&'a str>,
selected: Option<usize>,
} }
impl<'a> List<'a> { impl<'a> List<'a> {
@ -102,6 +85,7 @@ impl<'a> List<'a> {
start_corner: Corner::TopLeft, start_corner: Corner::TopLeft,
highlight_style: Style::default(), highlight_style: Style::default(),
highlight_symbol: None, highlight_symbol: None,
selected: None,
} }
} }
@ -129,20 +113,29 @@ impl<'a> List<'a> {
self.start_corner = corner; self.start_corner = corner;
self self
} }
pub fn select(mut self, index: Option<usize>) -> List<'a> {
self.selected = index;
self
}
} }
impl<'a> StatefulWidget for List<'a> { impl<'a> Widget for List<'a> {
type State = ListState; type State = ListState;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(mut self, ctx: &mut RenderContext<Self::State>) {
buf.set_style(area, self.style); ctx.buffer.set_style(ctx.area, self.style);
let list_area = match self.block.take() { let list_area = match self.block.take() {
Some(b) => { Some(b) => {
let inner_area = b.inner(area); let inner_area = b.inner(ctx.area);
b.render(area, buf); b.render(&mut RenderContext {
area: ctx.area,
buffer: ctx.buffer,
state: &mut (),
});
inner_area inner_area
} }
None => area, None => ctx.area,
}; };
if list_area.width < 1 || list_area.height < 1 { if list_area.width < 1 || list_area.height < 1 {
@ -154,10 +147,14 @@ impl<'a> StatefulWidget for List<'a> {
} }
let list_height = list_area.height as usize; let list_height = list_area.height as usize;
let mut start = state.offset; if self.selected.is_none() {
let mut end = state.offset; ctx.state.offset = 0;
}
let mut start = ctx.state.offset;
let mut end = ctx.state.offset;
let mut height = 0; let mut height = 0;
for item in self.items.iter().skip(state.offset) { for item in self.items.iter().skip(ctx.state.offset) {
if height + item.height() > list_height { if height + item.height() > list_height {
break; break;
} }
@ -165,7 +162,7 @@ impl<'a> StatefulWidget for List<'a> {
end += 1; end += 1;
} }
let selected = state.selected.unwrap_or(0).min(self.items.len() - 1); let selected = self.selected.unwrap_or(0).min(self.items.len() - 1);
while selected >= end { while selected >= end {
height = height.saturating_add(self.items[end].height()); height = height.saturating_add(self.items[end].height());
end += 1; end += 1;
@ -182,7 +179,7 @@ impl<'a> StatefulWidget for List<'a> {
height = height.saturating_sub(self.items[end].height()); height = height.saturating_sub(self.items[end].height());
} }
} }
state.offset = start; ctx.state.offset = start;
let highlight_symbol = self.highlight_symbol.unwrap_or(""); let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ") let blank_symbol = iter::repeat(" ")
@ -190,12 +187,12 @@ impl<'a> StatefulWidget for List<'a> {
.collect::<String>(); .collect::<String>();
let mut current_height = 0; let mut current_height = 0;
let has_selection = state.selected.is_some(); let has_selection = self.selected.is_some();
for (i, item) in self for (i, item) in self
.items .items
.iter_mut() .iter_mut()
.enumerate() .enumerate()
.skip(state.offset) .skip(ctx.state.offset)
.take(end - start) .take(end - start)
{ {
let (x, y) = match self.start_corner { let (x, y) = match self.start_corner {
@ -216,34 +213,30 @@ impl<'a> StatefulWidget for List<'a> {
height: item.height() as u16, height: item.height() as u16,
}; };
let item_style = self.style.patch(item.style); let item_style = self.style.patch(item.style);
buf.set_style(area, item_style); ctx.buffer.set_style(area, item_style);
let is_selected = state.selected.map(|s| s == i).unwrap_or(false); let is_selected = self.selected.map(|s| s == i).unwrap_or(false);
let elem_x = if has_selection { let elem_x = if has_selection {
let symbol = if is_selected { let symbol = if is_selected {
highlight_symbol highlight_symbol
} else { } else {
&blank_symbol &blank_symbol
}; };
let (x, _) = buf.set_stringn(x, y, symbol, list_area.width as usize, item_style); let (x, _) =
ctx.buffer
.set_stringn(x, y, symbol, list_area.width as usize, item_style);
x x
} else { } else {
x x
}; };
let max_element_width = (list_area.width - (elem_x - x)) as usize; let max_element_width = (list_area.width - (elem_x - x)) as usize;
for (j, line) in item.content.lines.iter().enumerate() { for (j, line) in item.content.lines.iter().enumerate() {
buf.set_spans(elem_x, y + j as u16, line, max_element_width as u16); ctx.buffer
.set_spans(elem_x, y + j as u16, line, max_element_width as u16);
} }
if is_selected { if is_selected {
buf.set_style(area, self.highlight_style); ctx.buffer.set_style(area, self.highlight_style);
} }
} }
} }
} }
impl<'a> Widget for List<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = ListState::default();
StatefulWidget::render(self, area, buf, &mut state);
}
}

@ -1,4 +1,4 @@
//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both. //! `widgets` is a collection of types that implement [`Widget`].
//! //!
//! All widgets are implemented using the builder pattern and are consumable objects. They are not //! All widgets are implemented using the builder pattern and are consumable objects. They are not
//! meant to be stored but used as *commands* to draw common figures in the UI. //! meant to be stored but used as *commands* to draw common figures in the UI.
@ -62,124 +62,30 @@ bitflags! {
/// Base requirements for a Widget /// Base requirements for a Widget
pub trait Widget { pub trait Widget {
/// Draws the current state of the widget in the given buffer. That the only method required to /// State stores everything that need to be saved between draw calls in order for the widget to
/// implement a custom widget. /// implement certain UI patterns.
fn render(self, area: Rect, buf: &mut Buffer); ///
/// 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. If the widget had
/// only access to the index of the selected item, it could only implement the following
/// behavior: whenever the selected item is out of the viewport scroll to a predefined position
/// (making the selected item the last viewable item or the one in the middle for example).
/// 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.
type State;
/// Render the widget in the internal buffer. That the only method required to implement a
/// custom widget.
fn render(self, ctx: &mut RenderContext<Self::State>);
} }
/// A `StatefulWidget` is a widget that can take advantage of some local state to remember things /// RenderContext is a set of dependencies that may be used when a widget is rendered.
/// between two draw calls. pub struct RenderContext<'a, S> {
/// /// Area where the widget is rendered.
/// Most widgets can be drawn directly based on the input parameters. However, some features may pub area: Rect,
/// require some kind of associated state to be implemented. /// Buffer where the drawing operations will be temporarily registered.
/// pub buffer: &'a mut Buffer,
/// For example, the [`List`] widget can highlight the item currently selected. This can be /// Internal state associated with the widget.
/// translated in an offset, which is the number of elements to skip in order to have the selected pub state: &'a mut S,
/// 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 (making the selected item the last viewable item or the one in the middle
/// for example). 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.
///
/// ## Examples
///
/// ```rust,no_run
/// # use std::io;
/// # use tui::Terminal;
/// # use tui::backend::{Backend, TermionBackend};
/// # use tui::widgets::{Widget, List, ListItem, ListState};
///
/// // Let's say we have some events to display.
/// struct Events {
/// // `items` is the state managed by your application.
/// items: Vec<String>,
/// // `state` is the state that can be modified by the UI. It stores the index of the selected
/// // item as well as the offset computed during the previous draw call (used to implement
/// // natural scrolling).
/// state: ListState
/// }
///
/// impl Events {
/// fn new(items: Vec<String>) -> Events {
/// Events {
/// items,
/// state: ListState::default(),
/// }
/// }
///
/// pub fn set_items(&mut self, items: Vec<String>) {
/// self.items = items;
/// // We reset the state as the associated items have changed. This effectively reset
/// // the selection as well as the stored offset.
/// self.state = ListState::default();
/// }
///
/// // Select the next item. This will not be reflected until the widget is drawn in the
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
/// 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));
/// }
///
/// // Select the previous item. This will not be reflected until the widget is drawn in the
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
/// 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));
/// }
///
/// // Unselect the currently selected item if any. The implementation of `ListState` makes
/// // sure that the stored offset is also reset.
/// pub fn unselect(&mut self) {
/// self.state.select(None);
/// }
/// }
///
/// let stdout = io::stdout();
/// let backend = TermionBackend::new(stdout);
/// let mut terminal = Terminal::new(backend).unwrap();
///
/// let mut events = Events::new(vec![
/// String::from("Item 1"),
/// String::from("Item 2")
/// ]);
///
/// loop {
/// terminal.draw(|f| {
/// // The items managed by the application are transformed to something
/// // that is understood by tui.
/// let items: Vec<ListItem>= events.items.iter().map(|i| ListItem::new(i.as_ref())).collect();
/// // The `List` widget is then built with those items.
/// let list = List::new(items);
/// // Finally the widget is rendered using the associated state. `events.state` is
/// // effectively the only thing that we will "remember" from this draw call.
/// f.render_stateful_widget(list, f.size(), &mut events.state);
/// });
///
/// // In response to some input events or an external http request or whatever:
/// events.next();
/// }
/// ```
pub trait StatefulWidget {
type State;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
} }

@ -1,11 +1,10 @@
use crate::{ use crate::{
buffer::Buffer, layout::Alignment,
layout::{Alignment, Rect},
style::Style, style::Style,
text::{StyledGrapheme, Text}, text::{StyledGrapheme, Text},
widgets::{ widgets::{
reflow::{LineComposer, LineTruncator, WordWrapper}, reflow::{LineComposer, LineTruncator, WordWrapper},
Block, Widget, Block, RenderContext, Widget,
}, },
}; };
use std::iter; use std::iter;
@ -133,15 +132,21 @@ impl<'a> Paragraph<'a> {
} }
impl<'a> Widget for Paragraph<'a> { impl<'a> Widget for Paragraph<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) { type State = ();
buf.set_style(area, self.style);
fn render(mut self, ctx: &mut RenderContext<Self::State>) {
ctx.buffer.set_style(ctx.area, self.style);
let text_area = match self.block.take() { let text_area = match self.block.take() {
Some(b) => { Some(b) => {
let inner_area = b.inner(area); let inner_area = b.inner(ctx.area);
b.render(area, buf); b.render(&mut RenderContext {
area: ctx.area,
buffer: ctx.buffer,
state: &mut (),
});
inner_area inner_area
} }
None => area, None => ctx.area,
}; };
if text_area.height < 1 { if text_area.height < 1 {
@ -176,7 +181,8 @@ impl<'a> Widget for Paragraph<'a> {
if y >= self.scroll.0 { if y >= self.scroll.0 {
let mut x = get_line_offset(current_line_width, text_area.width, self.alignment); let mut x = get_line_offset(current_line_width, text_area.width, self.alignment);
for StyledGrapheme { symbol, style } in current_line { for StyledGrapheme { symbol, style } in current_line {
buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0) ctx.buffer
.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0)
.set_symbol(if symbol.is_empty() { .set_symbol(if symbol.is_empty() {
// If the symbol is empty, the last char which rendered last time will // If the symbol is empty, the last char which rendered last time will
// leave on the line. It's a quick fix. // leave on the line. It's a quick fix.

@ -1,9 +1,7 @@
use crate::{ use crate::{
buffer::Buffer,
layout::Rect,
style::Style, style::Style,
symbols, symbols,
widgets::{Block, Widget}, widgets::{Block, RenderContext, Widget},
}; };
use std::cmp::min; use std::cmp::min;
@ -75,14 +73,20 @@ impl<'a> Sparkline<'a> {
} }
impl<'a> Widget for Sparkline<'a> { impl<'a> Widget for Sparkline<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) { type State = ();
fn render(mut self, ctx: &mut RenderContext<Self::State>) {
let spark_area = match self.block.take() { let spark_area = match self.block.take() {
Some(b) => { Some(b) => {
let inner_area = b.inner(area); let inner_area = b.inner(ctx.area);
b.render(area, buf); b.render(&mut RenderContext {
area: ctx.area,
buffer: ctx.buffer,
state: &mut (),
});
inner_area inner_area
} }
None => area, None => ctx.area,
}; };
if spark_area.height < 1 { if spark_area.height < 1 {
@ -119,7 +123,8 @@ impl<'a> Widget for Sparkline<'a> {
7 => self.bar_set.seven_eighths, 7 => self.bar_set.seven_eighths,
_ => self.bar_set.full, _ => self.bar_set.full,
}; };
buf.get_mut(spark_area.left() + i as u16, spark_area.top() + j) ctx.buffer
.get_mut(spark_area.left() + i as u16, spark_area.top() + j)
.set_symbol(symbol) .set_symbol(symbol)
.set_style(self.style); .set_style(self.style);
@ -135,14 +140,23 @@ impl<'a> Widget for Sparkline<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use crate::{
buffer::Buffer,
layout::Rect,
widgets::{RenderContext, Sparkline, Widget},
};
#[test] #[test]
fn it_does_not_panic_if_max_is_zero() { fn it_does_not_panic_if_max_is_zero() {
let widget = Sparkline::default().data(&[0, 0, 0]); let widget = Sparkline::default().data(&[0, 0, 0]);
let area = Rect::new(0, 0, 3, 1); let area = Rect::new(0, 0, 3, 1);
let mut buffer = Buffer::empty(area); let mut buffer = Buffer::empty(area);
widget.render(area, &mut buffer); let mut ctx = RenderContext {
area,
buffer: &mut buffer,
state: &mut (),
};
widget.render(&mut ctx);
} }
#[test] #[test]
@ -150,6 +164,11 @@ mod tests {
let 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 area = Rect::new(0, 0, 3, 1);
let mut buffer = Buffer::empty(area); let mut buffer = Buffer::empty(area);
widget.render(area, &mut buffer); let mut ctx = RenderContext {
area,
buffer: &mut buffer,
state: &mut (),
};
widget.render(&mut ctx);
} }
} }

@ -3,7 +3,7 @@ use crate::{
layout::{Constraint, Rect}, layout::{Constraint, Rect},
style::Style, style::Style,
text::Text, text::Text,
widgets::{Block, StatefulWidget, Widget}, widgets::{Block, RenderContext, Widget},
}; };
use cassowary::{ use cassowary::{
strength::{MEDIUM, REQUIRED, WEAK}, strength::{MEDIUM, REQUIRED, WEAK},
@ -200,6 +200,7 @@ pub struct Table<'a> {
header: Option<Row<'a>>, header: Option<Row<'a>>,
/// Data to display in each row /// Data to display in each row
rows: Vec<Row<'a>>, rows: Vec<Row<'a>>,
selected: Option<usize>,
} }
impl<'a> Table<'a> { impl<'a> Table<'a> {
@ -216,6 +217,7 @@ impl<'a> Table<'a> {
highlight_symbol: None, highlight_symbol: None,
header: None, header: None,
rows: rows.into_iter().collect(), rows: rows.into_iter().collect(),
selected: None,
} }
} }
@ -262,6 +264,11 @@ impl<'a> Table<'a> {
self self
} }
pub fn select(mut self, index: Option<usize>) -> Self {
self.selected = index;
self
}
fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> { fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> {
let mut solver = Solver::new(); let mut solver = Solver::new();
let mut var_indices = HashMap::new(); let mut var_indices = HashMap::new();
@ -328,12 +335,7 @@ impl<'a> Table<'a> {
widths widths
} }
fn get_row_bounds( fn get_row_bounds(&self, offset: usize, max_height: u16) -> (usize, usize) {
&self,
selected: Option<usize>,
offset: usize,
max_height: u16,
) -> (usize, usize) {
let mut start = offset; let mut start = offset;
let mut end = offset; let mut end = offset;
let mut height = 0; let mut height = 0;
@ -345,7 +347,7 @@ impl<'a> Table<'a> {
end += 1; end += 1;
} }
let selected = selected.unwrap_or(0).min(self.rows.len() - 1); let selected = self.selected.unwrap_or(0).min(self.rows.len() - 1);
while selected >= end { while selected >= end {
height = height.saturating_add(self.rows[end].total_height()); height = height.saturating_add(self.rows[end].total_height());
end += 1; end += 1;
@ -369,49 +371,39 @@ impl<'a> Table<'a> {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TableState { pub struct TableState {
offset: usize, offset: usize,
selected: Option<usize>,
} }
impl Default for TableState { impl Default for TableState {
fn default() -> TableState { fn default() -> TableState {
TableState { TableState { offset: 0 }
offset: 0,
selected: None,
}
} }
} }
impl TableState { impl<'a> Widget for Table<'a> {
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;
}
}
}
impl<'a> StatefulWidget for Table<'a> {
type State = TableState; type State = TableState;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(mut self, ctx: &mut RenderContext<Self::State>) {
if area.area() == 0 { if ctx.area.area() == 0 {
return; return;
} }
buf.set_style(area, self.style); ctx.buffer.set_style(ctx.area, self.style);
let table_area = match self.block.take() { let table_area = match self.block.take() {
Some(b) => { Some(b) => {
let inner_area = b.inner(area); let inner_area = b.inner(ctx.area);
b.render(area, buf); b.render(&mut RenderContext {
area: ctx.area,
buffer: ctx.buffer,
state: &mut (),
});
inner_area inner_area
} }
None => area, None => ctx.area,
}; };
let has_selection = state.selected.is_some(); let has_selection = self.selected.is_some();
if !has_selection {
ctx.state.offset = 0;
}
let columns_widths = self.get_columns_widths(table_area.width, has_selection); let columns_widths = self.get_columns_widths(table_area.width, has_selection);
let highlight_symbol = self.highlight_symbol.unwrap_or(""); let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ") let blank_symbol = iter::repeat(" ")
@ -423,7 +415,7 @@ impl<'a> StatefulWidget for Table<'a> {
// Draw header // Draw header
if let Some(ref header) = self.header { if let Some(ref header) = self.header {
let max_header_height = table_area.height.min(header.total_height()); let max_header_height = table_area.height.min(header.total_height());
buf.set_style( ctx.buffer.set_style(
Rect { Rect {
x: table_area.left(), x: table_area.left(),
y: table_area.top(), y: table_area.top(),
@ -438,7 +430,7 @@ impl<'a> StatefulWidget for Table<'a> {
} }
for (width, cell) in columns_widths.iter().zip(header.cells.iter()) { for (width, cell) in columns_widths.iter().zip(header.cells.iter()) {
render_cell( render_cell(
buf, ctx.buffer,
cell, cell,
Rect { Rect {
x: col, x: col,
@ -457,13 +449,13 @@ impl<'a> StatefulWidget for Table<'a> {
if self.rows.is_empty() { if self.rows.is_empty() {
return; return;
} }
let (start, end) = self.get_row_bounds(state.selected, state.offset, rows_height); let (start, end) = self.get_row_bounds(ctx.state.offset, rows_height);
state.offset = start; ctx.state.offset = start;
for (i, table_row) in self for (i, table_row) in self
.rows .rows
.iter_mut() .iter_mut()
.enumerate() .enumerate()
.skip(state.offset) .skip(ctx.state.offset)
.take(end - start) .take(end - start)
{ {
let (row, col) = (table_area.top() + current_height, table_area.left()); let (row, col) = (table_area.top() + current_height, table_area.left());
@ -474,16 +466,21 @@ impl<'a> StatefulWidget for Table<'a> {
width: table_area.width, width: table_area.width,
height: table_row.height, height: table_row.height,
}; };
buf.set_style(table_row_area, table_row.style); ctx.buffer.set_style(table_row_area, table_row.style);
let is_selected = state.selected.map(|s| s == i).unwrap_or(false); let is_selected = self.selected.map(|s| s == i).unwrap_or(false);
let table_row_start_col = if has_selection { let table_row_start_col = if has_selection {
let symbol = if is_selected { let symbol = if is_selected {
highlight_symbol highlight_symbol
} else { } else {
&blank_symbol &blank_symbol
}; };
let (col, _) = let (col, _) = ctx.buffer.set_stringn(
buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style); col,
row,
symbol,
table_area.width as usize,
table_row.style,
);
col col
} else { } else {
col col
@ -491,7 +488,7 @@ impl<'a> StatefulWidget for Table<'a> {
let mut col = table_row_start_col; let mut col = table_row_start_col;
for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) { for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
render_cell( render_cell(
buf, ctx.buffer,
cell, cell,
Rect { Rect {
x: col, x: col,
@ -503,7 +500,7 @@ impl<'a> StatefulWidget for Table<'a> {
col += *width + self.column_spacing; col += *width + self.column_spacing;
} }
if is_selected { if is_selected {
buf.set_style(table_row_area, self.highlight_style); ctx.buffer.set_style(table_row_area, self.highlight_style);
} }
} }
} }
@ -519,13 +516,6 @@ fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
} }
} }
impl<'a> Widget for Table<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default();
StatefulWidget::render(self, area, buf, &mut state);
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

@ -1,10 +1,9 @@
use crate::{ use crate::{
buffer::Buffer,
layout::Rect, layout::Rect,
style::Style, style::Style,
symbols, symbols,
text::{Span, Spans}, text::{Span, Spans},
widgets::{Block, Widget}, widgets::{Block, RenderContext, Widget},
}; };
/// A widget to display available tabs in a multiple panels context. /// A widget to display available tabs in a multiple panels context.
@ -81,15 +80,21 @@ impl<'a> Tabs<'a> {
} }
impl<'a> Widget for Tabs<'a> { impl<'a> Widget for Tabs<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) { type State = ();
buf.set_style(area, self.style);
fn render(mut self, ctx: &mut RenderContext<Self::State>) {
ctx.buffer.set_style(ctx.area, self.style);
let tabs_area = match self.block.take() { let tabs_area = match self.block.take() {
Some(b) => { Some(b) => {
let inner_area = b.inner(area); let inner_area = b.inner(ctx.area);
b.render(area, buf); b.render(&mut RenderContext {
area: ctx.area,
buffer: ctx.buffer,
state: &mut (),
});
inner_area inner_area
} }
None => area, None => ctx.area,
}; };
if tabs_area.height < 1 { if tabs_area.height < 1 {
@ -105,9 +110,11 @@ impl<'a> Widget for Tabs<'a> {
if remaining_width == 0 { if remaining_width == 0 {
break; break;
} }
let pos = buf.set_spans(x, tabs_area.top(), &title, remaining_width); let pos = ctx
.buffer
.set_spans(x, tabs_area.top(), &title, remaining_width);
if i == self.selected { if i == self.selected {
buf.set_style( ctx.buffer.set_style(
Rect { Rect {
x, x,
y: tabs_area.top(), y: tabs_area.top(),
@ -122,7 +129,9 @@ impl<'a> Widget for Tabs<'a> {
if remaining_width == 0 || last_title { if remaining_width == 0 || last_title {
break; break;
} }
let pos = buf.set_span(x, tabs_area.top(), &self.divider, remaining_width); let pos = ctx
.buffer
.set_span(x, tabs_area.top(), &self.divider, remaining_width);
x = pos.0; x = pos.0;
} }
} }

@ -4,7 +4,7 @@ use tui::{
layout::Rect, layout::Rect,
style::{Color, Style}, style::{Color, Style},
symbols, symbols,
widgets::{Block, Borders, List, ListItem, ListState}, widgets::{Block, Borders, List, ListItem},
Terminal, Terminal,
}; };
@ -12,8 +12,6 @@ use tui::{
fn widgets_list_should_highlight_the_selected_item() { fn widgets_list_should_highlight_the_selected_item() {
let backend = TestBackend::new(10, 3); let backend = TestBackend::new(10, 3);
let mut terminal = Terminal::new(backend).unwrap(); let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default();
state.select(Some(1));
terminal terminal
.draw(|f| { .draw(|f| {
let size = f.size(); let size = f.size();
@ -23,9 +21,10 @@ fn widgets_list_should_highlight_the_selected_item() {
ListItem::new("Item 3"), ListItem::new("Item 3"),
]; ];
let list = List::new(items) let list = List::new(items)
.select(Some(1))
.highlight_style(Style::default().bg(Color::Yellow)) .highlight_style(Style::default().bg(Color::Yellow))
.highlight_symbol(">> "); .highlight_symbol(">> ");
f.render_stateful_widget(list, size, &mut state); f.render_widget(list, size);
}) })
.unwrap(); .unwrap();
let mut expected = Buffer::with_lines(vec![" Item 1 ", ">> Item 2 ", " Item 3 "]); let mut expected = Buffer::with_lines(vec![" Item 1 ", ">> Item 2 ", " Item 3 "]);
@ -73,14 +72,13 @@ fn widgets_list_should_truncate_items() {
}, },
]; ];
for case in cases { for case in cases {
let mut state = ListState::default();
state.select(case.selected);
terminal terminal
.draw(|f| { .draw(|f| {
let list = List::new(case.items.clone()) let list = List::new(case.items.clone())
.block(Block::default().borders(Borders::RIGHT)) .block(Block::default().borders(Borders::RIGHT))
.select(case.selected)
.highlight_symbol(">> "); .highlight_symbol(">> ");
f.render_stateful_widget(list, Rect::new(0, 0, 8, 2), &mut state); f.render_widget(list, Rect::new(0, 0, 8, 2));
}) })
.unwrap(); .unwrap();
terminal.backend().assert_buffer(&case.expected); terminal.backend().assert_buffer(&case.expected);

@ -4,7 +4,7 @@ use tui::{
layout::Constraint, layout::Constraint,
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
text::{Span, Spans}, text::{Span, Spans},
widgets::{Block, Borders, Cell, Row, Table, TableState}, widgets::{Block, Borders, Cell, Row, Table},
Terminal, Terminal,
}; };
@ -517,7 +517,7 @@ fn widgets_table_columns_widths_can_use_ratio_constraints() {
#[test] #[test]
fn widgets_table_can_have_rows_with_multi_lines() { fn widgets_table_can_have_rows_with_multi_lines() {
let test_case = |state: &mut TableState, expected: Buffer| { let test_case = |selected: Option<usize>, expected: Buffer| {
let backend = TestBackend::new(30, 8); let backend = TestBackend::new(30, 8);
let mut terminal = Terminal::new(backend).unwrap(); let mut terminal = Terminal::new(backend).unwrap();
terminal terminal
@ -537,17 +537,17 @@ fn widgets_table_can_have_rows_with_multi_lines() {
Constraint::Length(5), Constraint::Length(5),
Constraint::Length(5), Constraint::Length(5),
]) ])
.select(selected)
.column_spacing(1); .column_spacing(1);
f.render_stateful_widget(table, size, state); f.render_widget(table, size);
}) })
.unwrap(); .unwrap();
terminal.backend().assert_buffer(&expected); terminal.backend().assert_buffer(&expected);
}; };
let mut state = TableState::default();
// no selection // no selection
test_case( test_case(
&mut state, None,
Buffer::with_lines(vec![ Buffer::with_lines(vec![
"┌────────────────────────────┐", "┌────────────────────────────┐",
"│Head1 Head2 Head3 │", "│Head1 Head2 Head3 │",
@ -561,9 +561,8 @@ fn widgets_table_can_have_rows_with_multi_lines() {
); );
// select first // select first
state.select(Some(0));
test_case( test_case(
&mut state, Some(0),
Buffer::with_lines(vec![ Buffer::with_lines(vec![
"┌────────────────────────────┐", "┌────────────────────────────┐",
"│ Head1 Head2 Head3 │", "│ Head1 Head2 Head3 │",
@ -577,9 +576,8 @@ fn widgets_table_can_have_rows_with_multi_lines() {
); );
// select second (we don't show partially the 4th row) // select second (we don't show partially the 4th row)
state.select(Some(1));
test_case( test_case(
&mut state, Some(1),
Buffer::with_lines(vec![ Buffer::with_lines(vec![
"┌────────────────────────────┐", "┌────────────────────────────┐",
"│ Head1 Head2 Head3 │", "│ Head1 Head2 Head3 │",
@ -593,9 +591,8 @@ fn widgets_table_can_have_rows_with_multi_lines() {
); );
// select 4th (we don't show partially the 1st row) // select 4th (we don't show partially the 1st row)
state.select(Some(3));
test_case( test_case(
&mut state, Some(3),
Buffer::with_lines(vec![ Buffer::with_lines(vec![
"┌────────────────────────────┐", "┌────────────────────────────┐",
"│ Head1 Head2 Head3 │", "│ Head1 Head2 Head3 │",
@ -613,8 +610,6 @@ fn widgets_table_can_have_rows_with_multi_lines() {
fn widgets_table_can_have_elements_styled_individually() { fn widgets_table_can_have_elements_styled_individually() {
let backend = TestBackend::new(30, 4); let backend = TestBackend::new(30, 4);
let mut terminal = Terminal::new(backend).unwrap(); let mut terminal = Terminal::new(backend).unwrap();
let mut state = TableState::default();
state.select(Some(0));
terminal terminal
.draw(|f| { .draw(|f| {
let size = f.size(); let size = f.size();
@ -640,8 +635,9 @@ fn widgets_table_can_have_elements_styled_individually() {
Constraint::Length(6), Constraint::Length(6),
Constraint::Length(6), Constraint::Length(6),
]) ])
.select(Some(0))
.column_spacing(1); .column_spacing(1);
f.render_stateful_widget(table, size, &mut state); f.render_widget(table, size);
}) })
.unwrap(); .unwrap();

Loading…
Cancel
Save