From 07ff2b08eb7410cebf15fb8455f674c48373845b Mon Sep 17 00:00:00 2001 From: Florian Dehau Date: Thu, 20 Oct 2016 12:01:09 +0200 Subject: [PATCH] Improve Chart Widget, safer buffer and unicode width --- Cargo.toml | 1 + examples/prototype.rs | 34 ++++++----- src/buffer.rs | 68 ++++++++++++---------- src/lib.rs | 1 + src/terminal.rs | 2 +- src/widgets/block.rs | 12 ++-- src/widgets/chart.rs | 128 +++++++++++++++++++++++++++++++++--------- src/widgets/gauge.rs | 1 - src/widgets/mod.rs | 2 +- 9 files changed, 170 insertions(+), 79 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5d6405cd..9dab6d9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = "*" diff --git a/examples/prototype.rs b/examples/prototype.rs index dafc8b36..81c0af2d 100644 --- a/examples/prototype.rs +++ b/examples/prototype.rs @@ -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 { + 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, - data2: Vec, + 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); diff --git a/src/buffer.rs b/src/buffer.rs index 408ca890..65f78454 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -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 { 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::>() { - 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::>(); + 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(&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(); diff --git a/src/lib.rs b/src/lib.rs index f0569796..18deaa01 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/terminal.rs b/src/terminal.rs index 24476f35..6367b27a 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -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, diff --git a/src/widgets/block.rs b/src/widgets/block.rs index 4116975a..4426b7d4 100644 --- a/src/widgets/block.rs +++ b/src/widgets/block.rs @@ -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 } diff --git a/src/widgets/chart.rs b/src/widgets/chart.rs index 844a393c..97a5fa15 100644 --- a/src/widgets/chart.rs +++ b/src/widgets/chart.rs @@ -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>, - 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; }) } diff --git a/src/widgets/gauge.rs b/src/widgets/gauge.rs index 3a42d090..df169c8b 100644 --- a/src/widgets/gauge.rs +++ b/src/widgets/gauge.rs @@ -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; diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 1c621e0a..7dacdfe7 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -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;