Compare commits
2 Commits
docs/updat
...
feat-autom
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3efcc992e | ||
|
|
a8c75508e3 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust: ["1.44.0", "stable"]
|
||||
rust: ["1.46.0", "stable"]
|
||||
steps:
|
||||
- name: "Install dependencies"
|
||||
run: sudo apt-get install libncurses5-dev
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust: ["1.44.0", "stable"]
|
||||
rust: ["1.46.0", "stable"]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
|
||||
|
||||
@@ -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};
|
||||
|
||||
173
src/terminal.rs
173
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<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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user