From a8c75508e311abb3d51f4db7ff2f1e4b76435867 Mon Sep 17 00:00:00 2001 From: Florian Dehau Date: Tue, 5 Jan 2021 02:25:02 +0100 Subject: [PATCH] feat: automatic state management --- examples/custom_widget.rs | 12 ++- examples/demo/app.rs | 14 +-- examples/demo/ui.rs | 10 +-- examples/list.rs | 9 +- examples/table.rs | 50 +++++------ examples/util/mod.rs | 55 ++++++------ src/lib.rs | 2 +- src/terminal.rs | 173 +++++++++++++++++++++++++++++++------- src/widgets/barchart.rs | 37 ++++---- src/widgets/block.rs | 52 +++++++----- src/widgets/canvas/mod.rs | 33 +++++--- src/widgets/chart.rs | 65 +++++++++----- src/widgets/clear.rs | 14 +-- src/widgets/gauge.rs | 57 ++++++++----- src/widgets/list.rs | 83 +++++++++--------- src/widgets/mod.rs | 144 ++++++------------------------- src/widgets/paragraph.rs | 24 ++++-- src/widgets/sparkline.rs | 41 ++++++--- src/widgets/table.rs | 94 +++++++++------------ src/widgets/tabs.rs | 29 ++++--- tests/widgets_list.rs | 12 ++- tests/widgets_table.rs | 24 +++--- 22 files changed, 555 insertions(+), 479 deletions(-) diff --git a/examples/custom_widget.rs b/examples/custom_widget.rs index d01ef3b..f59b080 100644 --- a/examples/custom_widget.rs +++ b/examples/custom_widget.rs @@ -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) { + ctx.buffer + .set_string(ctx.area.left(), ctx.area.top(), self.text, Style::default()); } } diff --git a/examples/demo/app.rs b/examples/demo/app.rs index 0291cfd..759e02b 100644 --- a/examples/demo/app.rs +++ b/examples/demo/app.rs @@ -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, - 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>, @@ -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); diff --git a/examples/demo/ui.rs b/examples/demo/ui.rs index e5664c0..652dd69 100644 --- a/examples/demo/ui.rs +++ b/examples/demo/ui.rs @@ -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 = 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() diff --git a/examples/list.rs b/examples/list.rs index ea36a40..a9d490e 100644 --- a/examples/list.rs +++ b/examples/list.rs @@ -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> { // 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> { .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. diff --git a/examples/table.rs b/examples/table.rs index 846a55c..c801d98 100644 --- a/examples/table.rs +++ b/examples/table.rs @@ -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, items: Vec>, } -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> { let events = Events::new(); - let mut table = StatefulTable::new(); + let mut table = SelectableTable::new(); // Input loop { @@ -117,12 +104,13 @@ fn main() -> Result<(), Box> { .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()? { diff --git a/examples/util/mod.rs b/examples/util/mod.rs index c926b3a..7cdb25d 100644 --- a/examples/util/mod.rs +++ b/examples/util/mod.rs @@ -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 { - pub state: ListState, +pub struct SelectableList { + pub selected: Option, pub items: Vec, } -impl StatefulList { - pub fn new() -> StatefulList { - StatefulList { - state: ListState::default(), +impl SelectableList { + pub fn new() -> SelectableList { + Self { + selected: None, items: Vec::new(), } } - pub fn with_items(items: Vec) -> StatefulList { - StatefulList { - state: ListState::default(), + pub fn with_items(items: Vec) -> SelectableList { + 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; } } diff --git a/src/lib.rs b/src/lib.rs index 12b1d7b..7f9c730 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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}; diff --git a/src/terminal.rs b/src/terminal.rs index 48d43be..b8ef690 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -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(&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, +} + +/// StateEntry holds the state of a [`Widget`]. +struct StateEntry { + /// State of a [`Widget`]. + state: Box, + /// 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 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, + /// 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, +} + +impl From for RenderArgs { + fn from(area: Rect) -> RenderArgs { + RenderArgs { area, id: None } + } +} + +impl RenderArgs { + /// Set the [`Widget`] id. + pub fn id(mut self, id: S) -> Self + where + S: Into, + { + 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(&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>, + /// selected_album: String + /// } + /// # let app = App { + /// # albums: HashMap::new(), + /// # selected_album: String::new(), + /// # }; + /// terminal.draw(|f| { + /// let songs: Vec = 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(&mut self, widget: W, area: Rect, state: &mut W::State) + #[track_caller] + pub fn render_widget(&mut self, widget: W, args: R) where - W: StatefulWidget, + W: Widget, + W::State: 'static + Default, + R: Into, { - 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(::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 { diff --git a/src/widgets/barchart.rs b/src/widgets/barchart.rs index dc57d1a..ebdc6e1 100644 --- a/src/widgets/barchart.rs +++ b/src/widgets/barchart.rs @@ -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) { + 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, diff --git a/src/widgets/block.rs b/src/widgets/block.rs index 7061013..d0d9d33 100644 --- a/src/widgets/block.rs +++ b/src/widgets/block.rs @@ -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) { + 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); } } } diff --git a/src/widgets/canvas/mod.rs b/src/widgets/canvas/mod.rs index 48e2240..fcfa04f 100644 --- a/src/widgets/canvas/mod.rs +++ b/src/widgets/canvas/mod.rs @@ -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) { 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, diff --git a/src/widgets/chart.rs b/src/widgets/chart.rs index df6905f..7aaec05 100644 --- a/src/widgets/chart.rs +++ b/src/widgets/chart.rs @@ -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) { + 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); } } } diff --git a/src/widgets/clear.rs b/src/widgets/clear.rs index 71357f5..3c607e1 100644 --- a/src/widgets/clear.rs +++ b/src/widgets/clear.rs @@ -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) { + 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(); } } } diff --git a/src/widgets/gauge.rs b/src/widgets/gauge.rs index 521e44f..0689279 100644 --- a/src/widgets/gauge.rs +++ b/src/widgets/gauge.rs @@ -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) { + 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) { + 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, diff --git a/src/widgets/list.rs b/src/widgets/list.rs index 064a7b2..f8b98b5 100644 --- a/src/widgets/list.rs +++ b/src/widgets/list.rs @@ -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, } impl Default for ListState { fn default() -> ListState { - ListState { - offset: 0, - selected: None, - } - } -} - -impl ListState { - pub fn selected(&self) -> Option { - self.selected - } - - pub fn select(&mut self, index: Option) { - self.selected = index; - if index.is_none() { - self.offset = 0; - } + 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, } 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) -> 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) { + 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::(); 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); - } -} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index d617893..508179f 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -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); } -/// 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, -/// // `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) -> Events { -/// Events { -/// items, -/// state: ListState::default(), -/// } -/// } -/// -/// pub fn set_items(&mut self, items: Vec) { -/// 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= 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, } diff --git a/src/widgets/paragraph.rs b/src/widgets/paragraph.rs index f4ebd8d..efd9d8b 100644 --- a/src/widgets/paragraph.rs +++ b/src/widgets/paragraph.rs @@ -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) { + 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. diff --git a/src/widgets/sparkline.rs b/src/widgets/sparkline.rs index e0b9eb2..af9a87d 100644 --- a/src/widgets/sparkline.rs +++ b/src/widgets/sparkline.rs @@ -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) { 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); } } diff --git a/src/widgets/table.rs b/src/widgets/table.rs index 261bc18..04e1a58 100644 --- a/src/widgets/table.rs +++ b/src/widgets/table.rs @@ -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>, /// Data to display in each row rows: Vec>, + selected: Option, } 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) -> Self { + self.selected = index; + self + } + fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec { 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, - 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, } impl Default for TableState { fn default() -> TableState { - TableState { - offset: 0, - selected: None, - } + TableState { offset: 0 } } } -impl TableState { - pub fn selected(&self) -> Option { - self.selected - } - - pub fn select(&mut self, index: Option) { - 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) { + 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::*; diff --git a/src/widgets/tabs.rs b/src/widgets/tabs.rs index 0dfa35c..306d070 100644 --- a/src/widgets/tabs.rs +++ b/src/widgets/tabs.rs @@ -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) { + 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; } } diff --git a/tests/widgets_list.rs b/tests/widgets_list.rs index 7028031..9e980f5 100644 --- a/tests/widgets_list.rs +++ b/tests/widgets_list.rs @@ -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); diff --git a/tests/widgets_table.rs b/tests/widgets_table.rs index f71169c..e61ce59 100644 --- a/tests/widgets_table.rs +++ b/tests/widgets_table.rs @@ -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, 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();