More work on the widgets API

* Change slice to vecs in table and canvas to ease the interaction with data
constructed on the fly
* Add a legend to charts based on datasets' names and colors
* Update demo to reflect previous updates
This commit is contained in:
Florian Dehau 2016-11-03 12:20:39 +01:00
parent 20465f2159
commit 9e5195096a
4 changed files with 154 additions and 32 deletions

View file

@ -24,7 +24,7 @@ use log4rs::config::{Appender, Config, Root};
use tui::Terminal;
use tui::widgets::{Widget, Block, List, Gauge, Sparkline, Text, border, Chart, Axis, Dataset,
BarChart, Marker, Tabs, Table};
use tui::widgets::canvas::{Canvas, Line, Map, MapResolution};
use tui::widgets::canvas::{Canvas, Line, Shape, Map, MapResolution, Label};
use tui::layout::{Group, Direction, Size, Rect};
use tui::style::Color;
@ -78,6 +78,13 @@ impl Iterator for SinSignal {
}
}
struct Server<'a> {
name: &'a str,
location: &'a str,
coords: (f64, f64),
status: &'a str,
}
struct MyTabs {
titles: [&'static str; 2],
selection: usize,
@ -110,6 +117,7 @@ struct App<'a> {
window: [f64; 2],
colors: [Color; 2],
color_index: usize,
servers: Vec<Server<'a>>,
}
enum Event {
@ -151,7 +159,7 @@ fn main() {
"Event15", "Event16", "Event17", "Event18", "Event19"],
selected: 0,
tabs: MyTabs {
titles: ["Main", "Map"],
titles: ["Tab0", "Tab1"],
selection: 0,
},
show_chart: true,
@ -177,6 +185,18 @@ fn main() {
window: [0.0, 20.0],
colors: [Color::Magenta, Color::Red],
color_index: 0,
servers: vec![Server {
name: "US-1",
location: "New York City",
coords: (40.71, -74.00),
status: "Up",
},
Server {
name: "Europe-1",
location: "Paris",
coords: (48.85, 2.35),
status: "Failure",
}],
};
let (tx, rx) = mpsc::channel();
let input_tx = tx.clone();
@ -307,16 +327,35 @@ fn draw(t: &mut Terminal, app: &App) {
.header(&["Server", "Location", "Status"])
.header_color(Color::Red)
.widths(&[20, 20, 20])
.rows(&[&["Europe#1", "Paris", "Up"],
&["Europe#2", "Berlin", "Up"]])
.color(Color::Green)
.rows(app.servers
.iter()
.map(|s| vec![s.name, s.location, s.status])
.collect::<Vec<Vec<&str>>>())
.render(&chunks[0], t);
Canvas::default()
.block(Block::default().title("World").borders(border::ALL))
.layers(&[&[&Map {
color: Color::Green,
resolution: MapResolution::High,
}]])
.layer([&Map {
color: Color::White,
resolution: MapResolution::High,
} as &Shape]
.as_ref())
.labels(app.servers
.iter()
.map(|s| {
let color = if s.status == "Up" {
Color::Green
} else {
Color::Red
};
Label {
x: s.coords.1,
y: s.coords.0,
text: "X",
color: color,
}
})
.collect::<Vec<Label>>())
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0])
.render(&chunks[1], t);
@ -414,10 +453,12 @@ fn draw_main(t: &mut Terminal, app: &App, area: &Rect) {
.bounds([-25.0, 25.0])
.labels(&["-25", "0", "25"]))
.datasets(&[Dataset::default()
.name("data2")
.marker(Marker::Dot)
.color(Color::Cyan)
.data(&app.data2),
Dataset::default()
.name("data3")
.marker(Marker::Braille)
.color(Color::Yellow)
.data(&app.data3)])

View file

@ -7,6 +7,8 @@ pub use self::points::Points;
pub use self::line::Line;
pub use self::map::{Map, MapResolution};
use std::borrow::Cow;
use style::Color;
use buffer::Buffer;
use widgets::{Block, Widget};
@ -22,11 +24,19 @@ pub trait Shape<'a> {
fn points(&'a self) -> Box<Iterator<Item = (f64, f64)> + 'a>;
}
pub struct Label<'a> {
pub x: f64,
pub y: f64,
pub text: &'a str,
pub color: Color,
}
pub struct Canvas<'a> {
block: Option<Block<'a>>,
x_bounds: [f64; 2],
y_bounds: [f64; 2],
layers: &'a [&'a [&'a Shape<'a>]],
layers: Vec<Cow<'a, [&'a Shape<'a>]>>,
labels: Vec<Label<'a>>,
background_color: Color,
}
@ -36,7 +46,8 @@ impl<'a> Default for Canvas<'a> {
block: None,
x_bounds: [0.0, 0.0],
y_bounds: [0.0, 0.0],
layers: &[],
layers: Vec::new(),
labels: Vec::new(),
background_color: Color::Reset,
}
}
@ -55,8 +66,25 @@ impl<'a> Canvas<'a> {
self.y_bounds = bounds;
self
}
pub fn layers(&mut self, layers: &'a [&'a [&'a Shape<'a>]]) -> &mut Canvas<'a> {
self.layers = layers;
pub fn layers<L>(&mut self, layers: Vec<L>) -> &mut Canvas<'a>
where L: Into<Cow<'a, [&'a Shape<'a>]>>
{
self.layers =
layers.into_iter().map(|l| l.into()).collect::<Vec<Cow<'a, [&'a Shape<'a>]>>>();
self
}
pub fn layer<L>(&mut self, layer: L) -> &mut Canvas<'a>
where L: Into<Cow<'a, [&'a Shape<'a>]>>
{
self.layers.push(layer.into());
self
}
pub fn labels<L>(&mut self, labels: L) -> &mut Canvas<'a>
where L: Into<Vec<Label<'a>>>
{
self.labels = labels.into();
self
}
@ -79,7 +107,7 @@ impl<'a> Widget for Canvas<'a> {
let width = canvas_area.width as usize;
let height = canvas_area.height as usize;
for layer in self.layers {
for layer in &self.layers {
let mut grid: Vec<u16> = vec![BRAILLE_OFFSET; width * height];
let mut colors: Vec<Color> = vec![Color::Reset; width * height];
@ -116,5 +144,20 @@ impl<'a> Widget for Canvas<'a> {
}
}
}
for label in self.labels.iter().filter(|l| {
!(l.x < self.x_bounds[0] || l.x > self.x_bounds[1] || l.y < self.y_bounds[0] ||
l.y > self.y_bounds[1])
}) {
let dy = ((self.y_bounds[1] - label.y) * (canvas_area.height - 1) as f64 /
(self.y_bounds[1] - self.y_bounds[0])) as u16;
let dx = ((label.x - self.x_bounds[0]) * (canvas_area.width - 1) as f64 /
(self.x_bounds[1] - self.x_bounds[0])) as u16;
buf.set_string(dx + canvas_area.left(),
dy + canvas_area.top(),
label.text,
label.color,
self.background_color);
}
}
}

View file

@ -2,8 +2,8 @@ use std::cmp::max;
use unicode_width::UnicodeWidthStr;
use widgets::{Widget, Block};
use widgets::canvas::{Canvas, Points};
use widgets::{Widget, Block, border};
use widgets::canvas::{Canvas, Shape, Points};
use buffer::Buffer;
use layout::Rect;
use style::Color;
@ -69,6 +69,7 @@ pub enum Marker {
}
pub struct Dataset<'a> {
name: &'a str,
data: &'a [(f64, f64)],
marker: Marker,
color: Color,
@ -77,6 +78,7 @@ pub struct Dataset<'a> {
impl<'a> Default for Dataset<'a> {
fn default() -> Dataset<'a> {
Dataset {
name: "",
data: &[],
marker: Marker::Dot,
color: Color::Reset,
@ -85,6 +87,11 @@ impl<'a> Default for Dataset<'a> {
}
impl<'a> Dataset<'a> {
pub fn name(mut self, name: &'a str) -> Dataset<'a> {
self.name = name;
self
}
pub fn data(mut self, data: &'a [(f64, f64)]) -> Dataset<'a> {
self.data = data;
self
@ -103,24 +110,26 @@ impl<'a> Dataset<'a> {
#[derive(Debug)]
struct ChartLayout {
legend_x: Option<(u16, u16)>,
legend_y: Option<(u16, u16)>,
title_x: Option<(u16, u16)>,
title_y: Option<(u16, u16)>,
label_x: Option<u16>,
label_y: Option<u16>,
axis_x: Option<u16>,
axis_y: Option<u16>,
legend_area: Option<Rect>,
graph_area: Rect,
}
impl Default for ChartLayout {
fn default() -> ChartLayout {
ChartLayout {
legend_x: None,
legend_y: None,
title_x: None,
title_y: None,
label_x: None,
label_y: None,
axis_x: None,
axis_y: None,
legend_area: None,
graph_area: Rect::default(),
}
}
@ -211,14 +220,26 @@ impl<'a> Chart<'a> {
if let Some(title) = self.x_axis.title {
let w = title.width() as u16;
if w < layout.graph_area.width && layout.graph_area.height > 2 {
layout.legend_x = Some((x + layout.graph_area.width - w, y));
layout.title_x = Some((x + layout.graph_area.width - w, y));
}
}
if let Some(title) = self.y_axis.title {
let w = title.width() as u16;
if w + 1 < layout.graph_area.width && layout.graph_area.height > 2 {
layout.legend_y = Some((x + 1, area.top()));
layout.title_y = Some((x + 1, area.top()));
}
}
if let Some(inner_width) = self.datasets.iter().map(|d| d.name.width() as u16).max() {
let legend_width = inner_width + 2;
let legend_height = self.datasets.len() as u16 + 2;
if legend_width < layout.graph_area.width / 3 &&
legend_height < layout.graph_area.height / 3 {
layout.legend_area = Some(Rect::new(layout.graph_area.right() - legend_width,
layout.graph_area.top(),
legend_width,
legend_height));
}
}
layout
@ -245,12 +266,12 @@ impl<'a> Widget for Chart<'a> {
self.background(&chart_area, buf, self.background_color);
}
if let Some((x, y)) = layout.legend_x {
if let Some((x, y)) = layout.title_x {
let title = self.x_axis.title.unwrap();
buf.set_string(x, y, title, self.x_axis.title_color, self.background_color);
}
if let Some((x, y)) = layout.legend_y {
if let Some((x, y)) = layout.title_y {
let title = self.y_axis.title.unwrap();
buf.set_string(x, y, title, self.y_axis.title_color, self.background_color);
}
@ -344,13 +365,27 @@ impl<'a> Widget for Chart<'a> {
.background_color(self.background_color)
.x_bounds(self.x_axis.bounds)
.y_bounds(self.y_axis.bounds)
.layers(&[&[&Points {
coords: dataset.data,
color: dataset.color,
}]])
.layer([&Points {
coords: dataset.data,
color: dataset.color,
} as &Shape]
.as_ref())
.draw(&graph_area, buf);
}
}
}
if let Some(legend_area) = layout.legend_area {
Block::default()
.borders(border::ALL)
.draw(&legend_area, buf);
for (i, dataset) in self.datasets.iter().enumerate() {
buf.set_string(legend_area.x + 1,
legend_area.y + 1 + i as u16,
dataset.name,
dataset.color,
self.background_color);
}
}
}
}

View file

@ -1,4 +1,5 @@
use std::cmp::max;
use std::borrow::Cow;
use unicode_width::UnicodeWidthStr;
@ -12,7 +13,7 @@ pub struct Table<'a> {
header: &'a [&'a str],
header_color: Color,
widths: &'a [u16],
rows: &'a [&'a [&'a str]],
rows: Vec<Cow<'a, [&'a str]>>,
color: Color,
column_spacing: u16,
background_color: Color,
@ -25,7 +26,7 @@ impl<'a> Default for Table<'a> {
header: &[],
header_color: Color::Reset,
widths: &[],
rows: &[],
rows: Vec::new(),
color: Color::Reset,
column_spacing: 1,
background_color: Color::Reset,
@ -54,8 +55,10 @@ impl<'a> Table<'a> {
self
}
pub fn rows(&mut self, rows: &'a [&'a [&'a str]]) -> &mut Table<'a> {
self.rows = rows;
pub fn rows<R>(&mut self, rows: Vec<R>) -> &mut Table<'a>
where R: Into<Cow<'a, [&'a str]>>
{
self.rows = rows.into_iter().map(|r| r.into()).collect::<Vec<Cow<'a, [&'a str]>>>();
self
}