Improve Chart Widget, safer buffer and unicode width

This commit is contained in:
Florian Dehau 2016-10-20 12:01:09 +02:00
parent fde0ba95dd
commit 07ff2b08eb
9 changed files with 170 additions and 79 deletions

View file

@ -9,6 +9,7 @@ bitflags = "0.7"
cassowary = "0.2.0"
log = "0.3"
unicode-segmentation = "0.1.2"
unicode-width = "0.1.3"
[dev-dependencies]
log4rs = "*"

View file

@ -22,7 +22,7 @@ use log4rs::encode::pattern::PatternEncoder;
use log4rs::config::{Appender, Config, Logger, Root};
use tui::Terminal;
use tui::widgets::{Widget, Block, List, Gauge, Sparkline, Text, border, Chart};
use tui::widgets::{Widget, Block, List, Gauge, Sparkline, Text, border, Chart, Axis, Dataset};
use tui::layout::{Group, Direction, Alignment, Size};
use tui::style::Color;
@ -66,10 +66,10 @@ impl SinSignal {
}
impl Iterator for SinSignal {
type Item = f64;
fn next(&mut self) -> Option<f64> {
type Item = (f64, f64);
fn next(&mut self) -> Option<(f64, f64)> {
self.x += 1.0;
Some(((self.x * 1.0 / self.period).sin() + 1.0) * self.scale)
Some((self.x, ((self.x * 1.0 / self.period).sin() + 1.0) * self.scale))
}
}
@ -81,7 +81,8 @@ struct App {
show_chart: bool,
progress: u16,
data: Vec<u64>,
data2: Vec<u64>,
data2: Vec<(f64, f64)>,
window: [f64; 2],
colors: [Color; 2],
color_index: usize,
}
@ -117,7 +118,8 @@ fn main() {
show_chart: true,
progress: 0,
data: rand_signal.clone().take(100).collect(),
data2: sin_signal.clone().take(100).map(|i| i as u64).collect(),
data2: sin_signal.clone().take(100).collect(),
window: [0.0, 100.0],
colors: [Color::Magenta, Color::Red],
color_index: 0,
};
@ -181,7 +183,9 @@ fn main() {
app.data.insert(0, rand_signal.next().unwrap());
app.data.pop();
app.data2.remove(0);
app.data2.push(sin_signal.next().unwrap() as u64);
app.data2.push(sin_signal.next().unwrap());
app.window[0] += 1.0;
app.window[1] += 1.0;
app.selected += 1;
if app.selected >= app.items.len() {
app.selected = 0;
@ -211,12 +215,12 @@ fn draw(t: &mut Terminal, app: &App) {
.chunks(&[Size::Fixed(2), Size::Fixed(3)])
.render(&chunks[0], |chunks| {
Gauge::default()
.block(*Block::default().title("Gauge:"))
.block(Block::default().title("Gauge:"))
.bg(Color::Yellow)
.percent(app.progress)
.render(&chunks[0], t);
Sparkline::default()
.block(*Block::default().title("Sparkline:"))
.block(Block::default().title("Sparkline:"))
.fg(Color::Green)
.data(&app.data)
.render(&chunks[1], t);
@ -232,21 +236,21 @@ fn draw(t: &mut Terminal, app: &App) {
.chunks(&sizes)
.render(&chunks[1], |chunks| {
List::default()
.block(*Block::default().borders(border::ALL).title("List"))
.block(Block::default().borders(border::ALL).title("List"))
.render(&chunks[0], t);
if app.show_chart {
Chart::default()
.block(*Block::default()
.block(Block::default()
.borders(border::ALL)
.title("Chart"))
.fg(Color::Cyan)
.axis([0, 40])
.data(&app.data2)
.x_axis(Axis::default().title("X").bounds(app.window))
.y_axis(Axis::default().title("Y").bounds([0.0, 40.0]))
.datasets(&[Dataset::default().color(Color::Cyan).data(&app.data2)])
.render(&chunks[1], t);
}
});
Text::default()
.block(*Block::default().borders(border::ALL).title("Footer"))
.block(Block::default().borders(border::ALL).title("Footer"))
.fg(app.colors[app.color_index])
.text("This żółw is a footer")
.render(&chunks[2], t);

View file

@ -61,15 +61,21 @@ impl<'a> Buffer<'a> {
&self.area
}
pub fn index_of(&self, x: u16, y: u16) -> usize {
pub fn index_of(&self, x: u16, y: u16) -> Option<usize> {
let index = (y * self.area.width + x) as usize;
debug_assert!(index < self.content.len());
index
if index < self.content.len() {
Some(index)
} else {
None
}
}
pub fn pos_of(&self, i: usize) -> (u16, u16) {
debug_assert!(self.area.width != 0);
(i as u16 % self.area.width, i as u16 / self.area.width)
pub fn pos_of(&self, i: usize) -> Option<(u16, u16)> {
if self.area.width > 0 {
Some((i as u16 % self.area.width, i as u16 / self.area.width))
} else {
None
}
}
pub fn next_pos(&self, x: u16, y: u16) -> Option<(u16, u16)> {
@ -86,48 +92,50 @@ impl<'a> Buffer<'a> {
}
pub fn set(&mut self, x: u16, y: u16, cell: Cell<'a>) {
let i = self.index_of(x, y);
self.content[i] = cell;
if let Some(i) = self.index_of(x, y) {
self.content[i] = cell;
}
}
pub fn set_symbol(&mut self, x: u16, y: u16, symbol: &'a str) {
let i = self.index_of(x, y);
self.content[i].symbol = symbol;
if let Some(i) = self.index_of(x, y) {
self.content[i].symbol = symbol;
}
}
pub fn set_fg(&mut self, x: u16, y: u16, color: Color) {
let i = self.index_of(x, y);
self.content[i].fg = color;
if let Some(i) = self.index_of(x, y) {
self.content[i].fg = color;
}
}
pub fn set_bg(&mut self, x: u16, y: u16, color: Color) {
let i = self.index_of(x, y);
self.content[i].bg = color;
if let Some(i) = self.index_of(x, y) {
self.content[i].bg = color;
}
}
pub fn set_string(&mut self, x: u16, y: u16, string: &'a str, fg: Color, bg: Color) {
let mut cursor = (x, y);
for s in UnicodeSegmentation::graphemes(string, true).collect::<Vec<&str>>() {
info!("{}", s);
let index = self.index_of(cursor.0, cursor.1);
let index = self.index_of(x, y);
if index.is_none() {
return;
}
let mut index = index.unwrap();
let graphemes = UnicodeSegmentation::graphemes(string, true).collect::<Vec<&str>>();
let max_index = (self.area.width - x) as usize;
for s in graphemes.iter().take(max_index) {
self.content[index].symbol = s;
self.content[index].fg = fg;
self.content[index].bg = bg;
match self.next_pos(cursor.0, cursor.1) {
Some(c) => {
cursor = c;
}
None => {
warn!("Failed to set all string");
}
}
index += 1;
}
}
pub fn update_cell<F>(&mut self, x: u16, y: u16, f: F)
where F: Fn(&mut Cell)
{
let i = self.index_of(x, y);
f(&mut self.content[i]);
if let Some(i) = self.index_of(x, y) {
f(&mut self.content[i]);
}
}
pub fn merge(&'a mut self, other: Buffer<'a>) {
@ -140,7 +148,7 @@ impl<'a> Buffer<'a> {
let offset_y = self.area.y - area.y;
let size = self.area.area() as usize;
for i in (0..size).rev() {
let (x, y) = self.pos_of(i);
let (x, y) = self.pos_of(i).unwrap();
// New index in content
let k = ((y + offset_y) * area.width + (x + offset_x)) as usize;
self.content[k] = self.content[i].clone();
@ -155,7 +163,7 @@ impl<'a> Buffer<'a> {
let offset_y = other.area.y - area.y;
let size = other.area.area() as usize;
for i in 0..size {
let (x, y) = other.pos_of(i);
let (x, y) = other.pos_of(i).unwrap();
// New index in content
let k = ((y + offset_y) * area.width + (x + offset_x)) as usize;
self.content[k] = other.content[i].clone();

View file

@ -5,6 +5,7 @@ extern crate bitflags;
extern crate log;
extern crate cassowary;
extern crate unicode_segmentation;
extern crate unicode_width;
mod buffer;
mod util;

View file

@ -32,7 +32,7 @@ impl Terminal {
pub fn render_buffer(&mut self, buffer: Buffer) {
for (i, cell) in buffer.content().iter().enumerate() {
let (lx, ly) = buffer.pos_of(i);
let (lx, ly) = buffer.pos_of(i).unwrap();
let (x, y) = (lx + buffer.area().x, ly + buffer.area().y);
if cell.symbol != "" {
write!(self.stdout,

View file

@ -29,33 +29,33 @@ impl<'a> Default for Block<'a> {
}
impl<'a> Block<'a> {
pub fn title(&mut self, title: &'a str) -> &mut Block<'a> {
pub fn title(mut self, title: &'a str) -> Block<'a> {
self.title = Some(title);
self
}
pub fn title_fg(&mut self, color: Color) -> &mut Block<'a> {
pub fn title_fg(mut self, color: Color) -> Block<'a> {
self.title_fg = color;
self
}
pub fn title_bg(&mut self, color: Color) -> &mut Block<'a> {
pub fn title_bg(mut self, color: Color) -> Block<'a> {
self.title_bg = color;
self
}
pub fn border_fg(&mut self, color: Color) -> &mut Block<'a> {
pub fn border_fg(mut self, color: Color) -> Block<'a> {
self.border_fg = color;
self
}
pub fn border_bg(&mut self, color: Color) -> &mut Block<'a> {
pub fn border_bg(mut self, color: Color) -> Block<'a> {
self.border_bg = color;
self
}
pub fn borders(&mut self, flag: border::Flags) -> &mut Block<'a> {
pub fn borders(mut self, flag: border::Flags) -> Block<'a> {
self.borders = flag;
self
}

View file

@ -1,4 +1,6 @@
use std::cmp::min;
use std::cmp::{min, max};
use unicode_width::UnicodeWidthStr;
use widgets::{Widget, Block};
use buffer::Buffer;
@ -7,22 +9,95 @@ use style::Color;
use util::hash;
use symbols;
pub struct Axis<'a> {
title: Option<&'a str>,
bounds: [f64; 2],
labels: Option<&'a [&'a str]>,
}
impl<'a> Default for Axis<'a> {
fn default() -> Axis<'a> {
Axis {
title: None,
bounds: [0.0, 0.0],
labels: None,
}
}
}
impl<'a> Axis<'a> {
pub fn title(mut self, title: &'a str) -> Axis<'a> {
self.title = Some(title);
self
}
pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a> {
self.bounds = bounds;
self
}
pub fn labels(mut self, labels: &'a [&'a str]) -> Axis<'a> {
self.labels = Some(labels);
self
}
fn title_width(&self) -> u16 {
match self.title {
Some(title) => title.width() as u16,
None => 0,
}
}
fn max_label_width(&self) -> u16 {
match self.labels {
Some(labels) => labels.iter().fold(0, |acc, l| max(l.width(), acc)) as u16,
None => 0,
}
}
}
pub struct Dataset<'a> {
data: &'a [(f64, f64)],
color: Color,
}
impl<'a> Default for Dataset<'a> {
fn default() -> Dataset<'a> {
Dataset {
data: &[],
color: Color::White,
}
}
}
impl<'a> Dataset<'a> {
pub fn data(mut self, data: &'a [(f64, f64)]) -> Dataset<'a> {
self.data = data;
self
}
pub fn color(mut self, color: Color) -> Dataset<'a> {
self.color = color;
self
}
}
pub struct Chart<'a> {
block: Option<Block<'a>>,
fg: Color,
x_axis: Axis<'a>,
y_axis: Axis<'a>,
datasets: &'a [Dataset<'a>],
bg: Color,
axis: [u64; 2],
data: &'a [u64],
}
impl<'a> Default for Chart<'a> {
fn default() -> Chart<'a> {
Chart {
block: None,
fg: Color::White,
x_axis: Axis::default(),
y_axis: Axis::default(),
bg: Color::Black,
axis: [0, 1],
data: &[],
datasets: &[],
}
}
}
@ -38,19 +113,18 @@ impl<'a> Chart<'a> {
self
}
pub fn fg(&mut self, fg: Color) -> &mut Chart<'a> {
self.fg = fg;
pub fn x_axis(&mut self, axis: Axis<'a>) -> &mut Chart<'a> {
self.x_axis = axis;
self
}
pub fn axis(&mut self, axis: [u64; 2]) -> &mut Chart<'a> {
debug_assert!(self.axis[0] <= self.axis[1]);
self.axis = axis;
pub fn y_axis(&mut self, axis: Axis<'a>) -> &mut Chart<'a> {
self.y_axis = axis;
self
}
pub fn data(&mut self, data: &'a [u64]) -> &mut Chart<'a> {
self.data = data;
pub fn datasets(&mut self, datasets: &'a [Dataset<'a>]) -> &mut Chart<'a> {
self.datasets = datasets;
self
}
}
@ -62,20 +136,24 @@ impl<'a> Widget<'a> for Chart<'a> {
None => (Buffer::empty(*area), *area),
};
if self.axis[1] == 0 {
return buf;
}
let margin_x = chart_area.x - area.x;
let margin_y = chart_area.y - area.y;
let max_index = min(chart_area.width as usize, self.data.len());
for (i, &y) in self.data.iter().take(max_index).enumerate() {
if y < self.axis[1] {
let dy = (self.axis[1] - y) * (chart_area.height - 1) as u64 /
(self.axis[1] - self.axis[0]);
buf.update_cell(i as u16 + margin_x, dy as u16 + margin_y, |c| {
// info!("{:?}", self.datasets[0].data[0]);
for dataset in self.datasets {
for &(x, y) in dataset.data.iter() {
if x <= self.x_axis.bounds[0] || x > self.x_axis.bounds[1] ||
y <= self.y_axis.bounds[0] || y > self.y_axis.bounds[1] {
continue;
}
let dy = (self.y_axis.bounds[1] - y) * (chart_area.height - 1) as f64 /
(self.y_axis.bounds[1] - self.y_axis.bounds[0]);
let dx = (self.x_axis.bounds[1] - x) * (chart_area.width - 1) as f64 /
(self.x_axis.bounds[1] - self.x_axis.bounds[0]);
info!("{} {}", dx, dy);
buf.update_cell(dx as u16 + margin_x, dy as u16 + margin_y, |c| {
c.symbol = symbols::DOT;
c.fg = self.fg;
c.fg = dataset.color;
c.bg = self.bg;
})
}

View file

@ -74,7 +74,6 @@ impl<'a> Widget<'a> for Gauge<'a> {
let margin_y = gauge_area.y - area.y;
// Gauge
let width = (gauge_area.width * self.percent) / 100;
info!("{}", width);
// Label
let len = self.percent_string.len() as u16;
let middle = gauge_area.width / 2 - len / 2;

View file

@ -10,7 +10,7 @@ pub use self::text::Text;
pub use self::list::List;
pub use self::gauge::Gauge;
pub use self::sparkline::Sparkline;
pub use self::chart::Chart;
pub use self::chart::{Chart, Axis, Dataset};
use std::hash::Hash;