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 termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend, buffer::Buffer, layout::Rect, style::Style, widgets::Widget, Terminal,
backend::TermionBackend,
style::Style,
widgets::{RenderContext, Widget},
Terminal,
};
struct Label<'a> {
@ -19,8 +22,11 @@ impl<'a> Default for Label<'a> {
}
impl<'a> Widget for Label<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_string(area.left(), area.top(), self.text, Style::default());
type State = ();
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] = [
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
@ -110,8 +110,8 @@ pub struct App<'a> {
pub show_chart: bool,
pub progress: f64,
pub sparkline: Signal<RandomSignal>,
pub tasks: StatefulList<&'a str>,
pub logs: StatefulList<(&'a str, &'a str)>,
pub tasks: SelectableList<&'a str>,
pub logs: Vec<(&'a str, &'a str)>,
pub signals: Signals,
pub barchart: Vec<(&'a str, u64)>,
pub servers: Vec<Server<'a>>,
@ -137,8 +137,8 @@ impl<'a> App<'a> {
points: sparkline_points,
tick_rate: 1,
},
tasks: StatefulList::with_items(TASKS.to_vec()),
logs: StatefulList::with_items(LOGS.to_vec()),
tasks: SelectableList::with_items(TASKS.to_vec()),
logs: Vec::from(LOGS),
signals: Signals {
sin1: Signal {
source: sin_signal,
@ -221,8 +221,8 @@ impl<'a> App<'a> {
self.sparkline.on_tick();
self.signals.on_tick();
let log = self.logs.items.pop().unwrap();
self.logs.items.insert(0, log);
let log = self.logs.pop().unwrap();
self.logs.insert(0, log);
let event = self.barchart.pop().unwrap();
self.barchart.insert(0, event);

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

@ -3,7 +3,7 @@ mod util;
use crate::util::{
event::{Event, Events},
StatefulList,
SelectableList,
};
use std::{error::Error, io};
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 drawing logic for items on how to specify the highlighting style for selected items.
struct App<'a> {
items: StatefulList<(&'a str, usize)>,
items: SelectableList<(&'a str, usize)>,
events: Vec<(&'a str, &'a str)>,
}
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
items: StatefulList::with_items(vec![
items: SelectableList::with_items(vec![
("Item0", 1),
("Item1", 2),
("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
let items = List::new(items)
.block(Block::default().borders(Borders::ALL).title("List"))
.select(app.items.selected)
.highlight_style(
Style::default()
.bg(Color::LightGreen)
@ -145,7 +146,7 @@ fn main() -> Result<(), Box<dyn Error>> {
.highlight_symbol(">> ");
// 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.
// 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,
layout::{Constraint, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Cell, Row, Table, TableState},
widgets::{Block, Borders, Cell, Row, Table},
Terminal,
};
pub struct StatefulTable<'a> {
state: TableState,
pub struct SelectableTable<'a> {
selected: Option<usize>,
items: Vec<Vec<&'a str>>,
}
impl<'a> StatefulTable<'a> {
fn new() -> StatefulTable<'a> {
StatefulTable {
state: TableState::default(),
impl<'a> SelectableTable<'a> {
fn new() -> SelectableTable<'a> {
SelectableTable {
selected: None,
items: vec![
vec!["Row11", "Row12", "Row13"],
vec!["Row21", "Row22", "Row23"],
@ -44,32 +44,19 @@ impl<'a> StatefulTable<'a> {
],
}
}
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));
self.selected = self
.selected
.map(|i| if i >= self.items.len() - 1 { 0 } else { i + 1 })
.or(Some(0));
}
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));
self.selected = self
.selected
.map(|i| if i == 0 { self.items.len() - 1 } else { i - 1 })
.or(Some(0));
}
}
@ -83,7 +70,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let events = Events::new();
let mut table = StatefulTable::new();
let mut table = SelectableTable::new();
// Input
loop {
@ -117,12 +104,13 @@ fn main() -> Result<(), Box<dyn Error>> {
.block(Block::default().borders(Borders::ALL).title("Table"))
.highlight_style(selected_style)
.highlight_symbol(">> ")
.select(table.selected)
.widths(&[
Constraint::Percentage(50),
Constraint::Length(30),
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()? {

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

@ -156,4 +156,4 @@ pub mod terminal;
pub mod text;
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,
buffer::Buffer,
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)]
/// 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)]
/// Options to pass to [`Terminal::with_options`]
pub struct TerminalOptions {
@ -38,7 +78,6 @@ pub struct TerminalOptions {
}
/// Interface to the terminal backed by Termion
#[derive(Debug)]
pub struct Terminal<B>
where
B: Backend,
@ -53,6 +92,11 @@ where
hidden_cursor: bool,
/// 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.
@ -69,6 +113,31 @@ where
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>
where
B: Backend,
@ -96,45 +165,79 @@ where
/// let mut frame = terminal.get_frame();
/// 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
/// given [`StatefulWidget`].
///
/// # Examples
/// If you happen to render two or more widgets using the same render call, you may want to
/// associate them with a unique id so they do not share any internal state.
///
/// For example, let say your app shows a list of songs of a given album:
/// ```rust,no_run
/// # use std::io;
/// # use tui::Terminal;
/// # use std::{collections::HashMap, io};
/// # use tui::{Terminal, RenderArgs};
/// # use tui::backend::TermionBackend;
/// # use tui::layout::Rect;
/// # use tui::widgets::{List, ListItem, ListState};
/// # use tui::widgets::{Block, List, ListItem};
/// # let stdout = io::stdout();
/// # let backend = TermionBackend::new(stdout);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// let mut state = ListState::default();
/// state.select(Some(1));
/// let items = vec![
/// ListItem::new("Item 1"),
/// ListItem::new("Item 2"),
/// ];
/// let list = List::new(items);
/// let area = Rect::new(0, 0, 5, 5);
/// let mut frame = terminal.get_frame();
/// frame.render_stateful_widget(list, area, &mut state);
/// struct App {
/// albums: HashMap<String, Vec<String>>,
/// selected_album: String
/// }
/// # let app = App {
/// # albums: HashMap::new(),
/// # selected_album: String::new(),
/// # };
/// terminal.draw(|f| {
/// 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
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)
@ -200,6 +303,8 @@ where
current: 0,
hidden_cursor: false,
viewport: options.viewport,
widget_states: HashMap::new(),
frame_index: 0,
})
}
@ -285,6 +390,12 @@ where
self.buffers[1 - self.current].reset();
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
self.backend.flush()?;
Ok(CompletedFrame {

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

@ -1,10 +1,9 @@
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
symbols::line,
text::{Span, Spans},
widgets::{Borders, Widget},
widgets::{Borders, RenderContext, Widget},
};
#[derive(Debug, Clone, Copy, PartialEq)]
@ -131,40 +130,46 @@ impl<'a> Block<'a> {
}
impl<'a> Widget for Block<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.area() == 0 {
type State = ();
fn render(self, ctx: &mut RenderContext<Self::State>) {
if ctx.area.area() == 0 {
return;
}
buf.set_style(area, self.style);
ctx.buffer.set_style(ctx.area, self.style);
let symbols = BorderType::line_symbols(self.border_type);
// Sides
if self.borders.intersects(Borders::LEFT) {
for y in area.top()..area.bottom() {
buf.get_mut(area.left(), y)
for y in ctx.area.top()..ctx.area.bottom() {
ctx.buffer
.get_mut(ctx.area.left(), y)
.set_symbol(symbols.vertical)
.set_style(self.border_style);
}
}
if self.borders.intersects(Borders::TOP) {
for x in area.left()..area.right() {
buf.get_mut(x, area.top())
for x in ctx.area.left()..ctx.area.right() {
ctx.buffer
.get_mut(x, ctx.area.top())
.set_symbol(symbols.horizontal)
.set_style(self.border_style);
}
}
if self.borders.intersects(Borders::RIGHT) {
let x = area.right() - 1;
for y in area.top()..area.bottom() {
buf.get_mut(x, y)
let x = ctx.area.right() - 1;
for y in ctx.area.top()..ctx.area.bottom() {
ctx.buffer
.get_mut(x, y)
.set_symbol(symbols.vertical)
.set_style(self.border_style);
}
}
if self.borders.intersects(Borders::BOTTOM) {
let y = area.bottom() - 1;
for x in area.left()..area.right() {
buf.get_mut(x, y)
let y = ctx.area.bottom() - 1;
for x in ctx.area.left()..ctx.area.right() {
ctx.buffer
.get_mut(x, y)
.set_symbol(symbols.horizontal)
.set_style(self.border_style);
}
@ -172,22 +177,26 @@ impl<'a> Widget for Block<'a> {
// Corners
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_style(self.border_style);
}
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_style(self.border_style);
}
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_style(self.border_style);
}
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_style(self.border_style);
}
@ -203,8 +212,9 @@ impl<'a> Widget for Block<'a> {
} else {
0
};
let width = area.width.saturating_sub(lx).saturating_sub(rx);
buf.set_spans(area.left() + lx, area.top(), &title, width);
let width = ctx.area.width.saturating_sub(lx).saturating_sub(rx);
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;
use crate::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
symbols,
widgets::{Block, Widget},
widgets::{Block, RenderContext, Widget},
};
use std::fmt::Debug;
@ -423,14 +421,20 @@ impl<'a, F> Widget for Canvas<'a, F>
where
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() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
let inner_area = b.inner(ctx.area);
b.render(&mut RenderContext {
area: ctx.area,
buffer: ctx.buffer,
state: &mut (),
});
inner_area
}
None => area,
None => ctx.area,
};
let width = canvas_area.width as usize;
@ -441,7 +445,7 @@ where
};
// 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.height,
self.x_bounds,
@ -449,11 +453,11 @@ where
self.marker,
);
// Paint to this context
painter(&mut ctx);
ctx.finish();
painter(&mut canvas_ctx);
canvas_ctx.finish();
// Retreive painted points for each layer
for layer in ctx.layers {
for layer in canvas_ctx.layers {
for (i, (ch, color)) in layer
.string
.chars()
@ -462,7 +466,8 @@ where
{
if ch != ' ' && ch != '\u{2800}' {
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_fg(color)
.set_bg(self.background_color);
@ -483,14 +488,14 @@ where
let height = f64::from(canvas_area.height - 1);
(width, height)
};
for label in ctx
for label in canvas_ctx
.labels
.iter()
.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 y = ((top - label.y) * resolution.1 / height) as u16 + canvas_area.top();
buf.set_stringn(
ctx.buffer.set_stringn(
x,
y,
label.text,

@ -1,12 +1,11 @@
use crate::{
buffer::Buffer,
layout::{Constraint, Rect},
style::{Color, Style},
symbols,
text::{Span, Spans},
widgets::{
canvas::{Canvas, Line, Points},
Block, Borders, Widget,
Block, Borders, RenderContext, Widget,
},
};
use std::{borrow::Cow, cmp::max};
@ -365,23 +364,29 @@ impl<'a> Chart<'a> {
}
impl<'a> Widget for Chart<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
if area.area() == 0 {
type State = ();
fn render(mut self, ctx: &mut RenderContext<Self::State>) {
if ctx.area.area() == 0 {
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
// the cells that are part of the components put on top of the grah area (i.e legend and
// 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() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
let inner_area = b.inner(ctx.area);
b.render(&mut RenderContext {
area: ctx.area,
buffer: ctx.buffer,
state: &mut (),
});
inner_area
}
None => area,
None => ctx.area,
};
let layout = self.layout(chart_area);
@ -396,7 +401,7 @@ impl<'a> Widget for Chart<'a> {
let labels_len = labels.len() as u16;
if total_width < graph_area.width && labels_len > 1 {
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)
- label.content.width() as u16,
y,
@ -413,14 +418,20 @@ impl<'a> Widget for Chart<'a> {
for (i, label) in labels.iter().enumerate() {
let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
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 {
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_style(self.x_axis.style);
}
@ -428,7 +439,8 @@ impl<'a> Widget for Chart<'a> {
if let Some(x) = layout.axis_y {
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_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(x) = layout.axis_y {
buf.get_mut(x, y)
ctx.buffer
.get_mut(x, y)
.set_symbol(symbols::line::BOTTOM_LEFT)
.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 {
buf.set_style(legend_area, original_style);
ctx.buffer.set_style(legend_area, original_style);
Block::default()
.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() {
buf.set_string(
ctx.buffer.set_string(
legend_area.x + 1,
legend_area.y + 1 + i as u16,
&dataset.name,
@ -486,7 +507,7 @@ impl<'a> Widget for Chart<'a> {
if let Some((x, y)) = layout.title_x {
let title = self.x_axis.title.unwrap();
let width = graph_area.right().saturating_sub(x);
buf.set_style(
ctx.buffer.set_style(
Rect {
x,
y,
@ -495,13 +516,13 @@ impl<'a> Widget for Chart<'a> {
},
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 {
let title = self.y_axis.title.unwrap();
let width = graph_area.right().saturating_sub(x);
buf.set_style(
ctx.buffer.set_style(
Rect {
x,
y,
@ -510,7 +531,7 @@ impl<'a> Widget for Chart<'a> {
},
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::layout::Rect;
use crate::widgets::Widget;
use crate::widgets::{RenderContext, Widget};
/// 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;
impl Widget for Clear {
fn render(self, area: Rect, buf: &mut Buffer) {
for x in area.left()..area.right() {
for y in area.top()..area.bottom() {
buf.get_mut(x, y).reset();
type State = ();
fn render(self, ctx: &mut RenderContext<Self::State>) {
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::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
symbols,
text::{Span, Spans},
widgets::{Block, Widget},
widgets::{Block, RenderContext, Widget},
};
/// A widget to display a task progress.
@ -92,17 +90,23 @@ impl<'a> Gauge<'a> {
}
impl<'a> Widget for Gauge<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
type State = ();
fn render(mut self, ctx: &mut RenderContext<Self::State>) {
ctx.buffer.set_style(ctx.area, self.style);
let gauge_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
let inner_area = b.inner(ctx.area);
b.render(&mut RenderContext {
area: ctx.area,
buffer: ctx.buffer,
state: &mut (),
});
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 {
return;
}
@ -124,12 +128,13 @@ impl<'a> Widget for Gauge<'a> {
for y in gauge_area.top()..gauge_area.bottom() {
// Gauge
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
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));
}
@ -138,7 +143,8 @@ impl<'a> Widget for Gauge<'a> {
if y == center {
let label_width = label.width() as u16;
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 {
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
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_bg(self.gauge_style.fg.unwrap_or(Color::Reset));
}
@ -245,15 +252,21 @@ impl<'a> LineGauge<'a> {
}
impl<'a> Widget for LineGauge<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
type State = ();
fn render(mut self, ctx: &mut RenderContext<Self::State>) {
ctx.buffer.set_style(ctx.area, self.style);
let gauge_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
let inner_area = b.inner(ctx.area);
b.render(&mut RenderContext {
area: ctx.area,
buffer: ctx.buffer,
state: &mut (),
});
inner_area
}
None => area,
None => ctx.area,
};
if gauge_area.height < 1 {
@ -264,7 +277,7 @@ impl<'a> Widget for LineGauge<'a> {
let label = self
.label
.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.top(),
&label,
@ -278,7 +291,8 @@ impl<'a> Widget for LineGauge<'a> {
let end = start
+ (f64::from(gauge_area.right().saturating_sub(start)) * self.ratio).floor() as u16;
for col in start..end {
buf.get_mut(col, row)
ctx.buffer
.get_mut(col, row)
.set_symbol(self.line_set.horizontal)
.set_style(Style {
fg: self.gauge_style.fg,
@ -288,7 +302,8 @@ impl<'a> Widget for LineGauge<'a> {
});
}
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_style(Style {
fg: self.gauge_style.bg,

@ -1,9 +1,8 @@
use crate::{
buffer::Buffer,
layout::{Corner, Rect},
style::Style,
text::Text,
widgets::{Block, StatefulWidget, Widget},
widgets::{Block, RenderContext, Widget},
};
use std::iter::{self, Iterator};
use unicode_width::UnicodeWidthStr;
@ -11,28 +10,11 @@ use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone)]
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;
}
ListState { offset: 0 }
}
}
@ -88,6 +70,7 @@ pub struct List<'a> {
highlight_style: Style,
/// Symbol in front of the selected item (Shift all items to the right)
highlight_symbol: Option<&'a str>,
selected: Option<usize>,
}
impl<'a> List<'a> {
@ -102,6 +85,7 @@ impl<'a> List<'a> {
start_corner: Corner::TopLeft,
highlight_style: Style::default(),
highlight_symbol: None,
selected: None,
}
}
@ -129,20 +113,29 @@ impl<'a> List<'a> {
self.start_corner = corner;
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;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::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 list_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
let inner_area = b.inner(ctx.area);
b.render(&mut RenderContext {
area: ctx.area,
buffer: ctx.buffer,
state: &mut (),
});
inner_area
}
None => area,
None => ctx.area,
};
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 mut start = state.offset;
let mut end = state.offset;
if self.selected.is_none() {
ctx.state.offset = 0;
}
let mut start = ctx.state.offset;
let mut end = ctx.state.offset;
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 {
break;
}
@ -165,7 +162,7 @@ impl<'a> StatefulWidget for List<'a> {
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 {
height = height.saturating_add(self.items[end].height());
end += 1;
@ -182,7 +179,7 @@ impl<'a> StatefulWidget for List<'a> {
height = height.saturating_sub(self.items[end].height());
}
}
state.offset = start;
ctx.state.offset = start;
let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ")
@ -190,12 +187,12 @@ impl<'a> StatefulWidget for List<'a> {
.collect::<String>();
let mut current_height = 0;
let has_selection = state.selected.is_some();
let has_selection = self.selected.is_some();
for (i, item) in self
.items
.iter_mut()
.enumerate()
.skip(state.offset)
.skip(ctx.state.offset)
.take(end - start)
{
let (x, y) = match self.start_corner {
@ -216,34 +213,30 @@ impl<'a> StatefulWidget for List<'a> {
height: item.height() as u16,
};
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 symbol = if is_selected {
highlight_symbol
} else {
&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
} else {
x
};
let max_element_width = (list_area.width - (elem_x - x)) as usize;
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 {
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
//! 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
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 render(self, area: Rect, buf: &mut Buffer);
/// State stores everything that need to be saved between draw calls in order for the widget to
/// implement certain UI patterns.
///
/// 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
/// between two draw calls.
///
/// 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 (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);
/// RenderContext is a set of dependencies that may be used when a widget is rendered.
pub struct RenderContext<'a, S> {
/// Area where the widget is rendered.
pub area: Rect,
/// Buffer where the drawing operations will be temporarily registered.
pub buffer: &'a mut Buffer,
/// Internal state associated with the widget.
pub state: &'a mut S,
}

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

@ -1,9 +1,7 @@
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
symbols,
widgets::{Block, Widget},
widgets::{Block, RenderContext, Widget},
};
use std::cmp::min;
@ -75,14 +73,20 @@ impl<'a> 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() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
let inner_area = b.inner(ctx.area);
b.render(&mut RenderContext {
area: ctx.area,
buffer: ctx.buffer,
state: &mut (),
});
inner_area
}
None => area,
None => ctx.area,
};
if spark_area.height < 1 {
@ -119,7 +123,8 @@ impl<'a> Widget for Sparkline<'a> {
7 => self.bar_set.seven_eighths,
_ => 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_style(self.style);
@ -135,14 +140,23 @@ impl<'a> Widget for Sparkline<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::{
buffer::Buffer,
layout::Rect,
widgets::{RenderContext, Sparkline, Widget},
};
#[test]
fn it_does_not_panic_if_max_is_zero() {
let widget = Sparkline::default().data(&[0, 0, 0]);
let area = Rect::new(0, 0, 3, 1);
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]
@ -150,6 +164,11 @@ mod tests {
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.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},
style::Style,
text::Text,
widgets::{Block, StatefulWidget, Widget},
widgets::{Block, RenderContext, Widget},
};
use cassowary::{
strength::{MEDIUM, REQUIRED, WEAK},
@ -200,6 +200,7 @@ pub struct Table<'a> {
header: Option<Row<'a>>,
/// Data to display in each row
rows: Vec<Row<'a>>,
selected: Option<usize>,
}
impl<'a> Table<'a> {
@ -216,6 +217,7 @@ impl<'a> Table<'a> {
highlight_symbol: None,
header: None,
rows: rows.into_iter().collect(),
selected: None,
}
}
@ -262,6 +264,11 @@ impl<'a> Table<'a> {
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> {
let mut solver = Solver::new();
let mut var_indices = HashMap::new();
@ -328,12 +335,7 @@ impl<'a> Table<'a> {
widths
}
fn get_row_bounds(
&self,
selected: Option<usize>,
offset: usize,
max_height: u16,
) -> (usize, usize) {
fn get_row_bounds(&self, offset: usize, max_height: u16) -> (usize, usize) {
let mut start = offset;
let mut end = offset;
let mut height = 0;
@ -345,7 +347,7 @@ impl<'a> Table<'a> {
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 {
height = height.saturating_add(self.rows[end].total_height());
end += 1;
@ -369,49 +371,39 @@ impl<'a> Table<'a> {
#[derive(Debug, Clone)]
pub struct TableState {
offset: usize,
selected: Option<usize>,
}
impl Default for TableState {
fn default() -> TableState {
TableState {
offset: 0,
selected: None,
}
TableState { offset: 0 }
}
}
impl TableState {
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> {
impl<'a> Widget for Table<'a> {
type State = TableState;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
if area.area() == 0 {
fn render(mut self, ctx: &mut RenderContext<Self::State>) {
if ctx.area.area() == 0 {
return;
}
buf.set_style(area, self.style);
ctx.buffer.set_style(ctx.area, self.style);
let table_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
let inner_area = b.inner(ctx.area);
b.render(&mut RenderContext {
area: ctx.area,
buffer: ctx.buffer,
state: &mut (),
});
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 highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ")
@ -423,7 +415,7 @@ impl<'a> StatefulWidget for Table<'a> {
// Draw header
if let Some(ref header) = self.header {
let max_header_height = table_area.height.min(header.total_height());
buf.set_style(
ctx.buffer.set_style(
Rect {
x: table_area.left(),
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()) {
render_cell(
buf,
ctx.buffer,
cell,
Rect {
x: col,
@ -457,13 +449,13 @@ impl<'a> StatefulWidget for Table<'a> {
if self.rows.is_empty() {
return;
}
let (start, end) = self.get_row_bounds(state.selected, state.offset, rows_height);
state.offset = start;
let (start, end) = self.get_row_bounds(ctx.state.offset, rows_height);
ctx.state.offset = start;
for (i, table_row) in self
.rows
.iter_mut()
.enumerate()
.skip(state.offset)
.skip(ctx.state.offset)
.take(end - start)
{
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,
height: table_row.height,
};
buf.set_style(table_row_area, table_row.style);
let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
ctx.buffer.set_style(table_row_area, table_row.style);
let is_selected = self.selected.map(|s| s == i).unwrap_or(false);
let table_row_start_col = if has_selection {
let symbol = if is_selected {
highlight_symbol
} else {
&blank_symbol
};
let (col, _) =
buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style);
let (col, _) = ctx.buffer.set_stringn(
col,
row,
symbol,
table_area.width as usize,
table_row.style,
);
col
} else {
col
@ -491,7 +488,7 @@ impl<'a> StatefulWidget for Table<'a> {
let mut col = table_row_start_col;
for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
render_cell(
buf,
ctx.buffer,
cell,
Rect {
x: col,
@ -503,7 +500,7 @@ impl<'a> StatefulWidget for Table<'a> {
col += *width + self.column_spacing;
}
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)]
mod tests {
use super::*;

@ -1,10 +1,9 @@
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
symbols,
text::{Span, Spans},
widgets::{Block, Widget},
widgets::{Block, RenderContext, Widget},
};
/// 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> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
type State = ();
fn render(mut self, ctx: &mut RenderContext<Self::State>) {
ctx.buffer.set_style(ctx.area, self.style);
let tabs_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
let inner_area = b.inner(ctx.area);
b.render(&mut RenderContext {
area: ctx.area,
buffer: ctx.buffer,
state: &mut (),
});
inner_area
}
None => area,
None => ctx.area,
};
if tabs_area.height < 1 {
@ -105,9 +110,11 @@ impl<'a> Widget for Tabs<'a> {
if remaining_width == 0 {
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 {
buf.set_style(
ctx.buffer.set_style(
Rect {
x,
y: tabs_area.top(),
@ -122,7 +129,9 @@ impl<'a> Widget for Tabs<'a> {
if remaining_width == 0 || last_title {
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;
}
}

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

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

Loading…
Cancel
Save