From 7a427c06d492c63a941489612b2c3ac80a99368e Mon Sep 17 00:00:00 2001 From: Florian Dehau Date: Fri, 21 Oct 2016 19:02:19 +0200 Subject: [PATCH] Update chart widget and fix colors in all widgets --- examples/prototype.rs | 45 +++++---- src/buffer.rs | 19 ++-- src/style.rs | 3 + src/symbols.rs | 1 + src/widgets/block.rs | 58 ++++-------- src/widgets/chart.rs | 196 ++++++++++++++++++++++++++++++++++----- src/widgets/gauge.rs | 20 ++-- src/widgets/sparkline.rs | 32 +++---- src/widgets/text.rs | 10 +- 9 files changed, 263 insertions(+), 121 deletions(-) diff --git a/examples/prototype.rs b/examples/prototype.rs index b775be7..c9660dc 100644 --- a/examples/prototype.rs +++ b/examples/prototype.rs @@ -81,6 +81,7 @@ struct App { data: Vec, data2: Vec<(f64, f64)>, data3: Vec<(f64, f64)>, + window: [f64; 2], colors: [Color; 2], color_index: usize, } @@ -115,15 +116,21 @@ fn main() { selected: 0, show_chart: true, progress: 0, - data: rand_signal.clone().take(100).collect(), - data2: sin_signal.clone().take(100).collect(), - data3: sin_signal2.clone().take(100).collect(), + data: rand_signal.clone().take(200).collect(), + data2: sin_signal.clone().take(20).collect(), + data3: sin_signal2.clone().take(20).collect(), + window: [0.0, 20.0], colors: [Color::Magenta, Color::Red], color_index: 0, }; let (tx, rx) = mpsc::channel(); let input_tx = tx.clone(); + for i in 0..20 { + sin_signal.next(); + sin_signal2.next(); + } + thread::spawn(move || { let stdin = stdin(); for c in stdin.keys() { @@ -139,7 +146,7 @@ fn main() { let tx = tx.clone(); loop { tx.send(Event::Tick).unwrap(); - thread::sleep(time::Duration::from_millis(1000)); + thread::sleep(time::Duration::from_millis(200)); } }); @@ -184,6 +191,8 @@ fn main() { app.data2.push(sin_signal.next().unwrap()); app.data3.remove(0); app.data3.push(sin_signal2.next().unwrap()); + app.window[0] += 1.0; + app.window[1] += 1.0; app.selected += 1; if app.selected >= app.items.len() { app.selected = 0; @@ -202,15 +211,9 @@ fn draw(t: &mut Terminal, app: &App) { let size = Terminal::size().unwrap(); - Block::default() - .borders(border::ALL) - .title(&app.name) - .render(&size, t); - Group::default() .direction(Direction::Vertical) .alignment(Alignment::Left) - .margin(1) .chunks(&[Size::Fixed(7), Size::Min(5), Size::Fixed(3)]) .render(t, &size, |t, chunks| { Block::default().borders(border::ALL).title("Graphs").render(&chunks[0], t); @@ -222,7 +225,7 @@ fn draw(t: &mut Terminal, app: &App) { .render(t, &chunks[0], |t, chunks| { Gauge::default() .block(Block::default().title("Gauge:")) - .bg(Color::Yellow) + .bg(Color::Magenta) .percent(app.progress) .render(&chunks[0], t); Sparkline::default() @@ -246,11 +249,19 @@ fn draw(t: &mut Terminal, app: &App) { .render(&chunks[0], t); if app.show_chart { Chart::default() - .block(Block::default() - .borders(border::ALL) - .title("Chart")) - .x_axis(Axis::default().title("X").bounds([0.0, 100.0])) - .y_axis(Axis::default().title("Y").bounds([0.0, 40.0])) + .block(Block::default().title("Chart")) + .x_axis(Axis::default() + .title("X Axis") + .color(Color::Gray) + .bounds(app.window) + .labels(&[&format!("{}", app.window[0]), + &format!("{}", (app.window[0] + app.window[1]) / 2.0), + &format!("{}", app.window[1])])) + .y_axis(Axis::default() + .title("Y Axis") + .color(Color::Gray) + .bounds([0.0, 40.0]) + .labels(&["0", "20", "40"])) .datasets(&[Dataset::default().color(Color::Cyan).data(&app.data2), Dataset::default().color(Color::Yellow).data(&app.data3)]) .render(&chunks[1], t); @@ -259,7 +270,7 @@ fn draw(t: &mut Terminal, app: &App) { Text::default() .block(Block::default().borders(border::ALL).title("Footer")) .fg(app.colors[app.color_index]) - .text("This żółw is a footer") + .text("日本国 UTF-8 charaters") .render(&chunks[2], t); }); } diff --git a/src/buffer.rs b/src/buffer.rs index 65f7845..3e18b0f 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -14,8 +14,8 @@ impl<'a> Default for Cell<'a> { fn default() -> Cell<'a> { Cell { symbol: "", - fg: Color::White, - bg: Color::Black, + fg: Color::Reset, + bg: Color::Reset, } } } @@ -130,11 +130,18 @@ impl<'a> Buffer<'a> { } } - pub fn update_cell(&mut self, x: u16, y: u16, f: F) - where F: Fn(&mut Cell) - { + pub fn update_colors(&mut self, x: u16, y: u16, fg: Color, bg: Color) { if let Some(i) = self.index_of(x, y) { - f(&mut self.content[i]); + self.content[i].fg = fg; + self.content[i].bg = bg; + } + } + + pub fn update_cell(&mut self, x: u16, y: u16, symbol: &'a str, fg: Color, bg: Color) { + if let Some(i) = self.index_of(x, y) { + self.content[i].symbol = symbol; + self.content[i].fg = fg; + self.content[i].bg = bg; } } diff --git a/src/style.rs b/src/style.rs index 7b0cade..db70888 100644 --- a/src/style.rs +++ b/src/style.rs @@ -2,6 +2,7 @@ use termion; #[derive(Debug, Clone, Copy, Hash)] pub enum Color { + Reset, Black, Red, Green, @@ -22,6 +23,7 @@ pub enum Color { impl Color { pub fn fg(&self) -> String { match *self { + Color::Reset => format!("{}", termion::color::Fg(termion::color::Reset)), Color::Black => format!("{}", termion::color::Fg(termion::color::Black)), Color::Red => format!("{}", termion::color::Fg(termion::color::Red)), Color::Green => format!("{}", termion::color::Fg(termion::color::Green)), @@ -41,6 +43,7 @@ impl Color { } pub fn bg(&self) -> String { match *self { + Color::Reset => format!("{}", termion::color::Bg(termion::color::Reset)), Color::Black => format!("{}", termion::color::Bg(termion::color::Black)), Color::Red => format!("{}", termion::color::Bg(termion::color::Red)), Color::Green => format!("{}", termion::color::Bg(termion::color::Green)), diff --git a/src/symbols.rs b/src/symbols.rs index efaac87..fc9ed4b 100644 --- a/src/symbols.rs +++ b/src/symbols.rs @@ -34,3 +34,4 @@ pub mod line { } pub const DOT: &'static str = "•"; +pub const BLACK_CIRCLE: &'static str = "●"; diff --git a/src/widgets/block.rs b/src/widgets/block.rs index c88ca46..884257d 100644 --- a/src/widgets/block.rs +++ b/src/widgets/block.rs @@ -8,22 +8,20 @@ use symbols::line; #[derive(Clone, Copy)] pub struct Block<'a> { title: Option<&'a str>, - title_fg: Color, - title_bg: Color, + title_color: Color, borders: border::Flags, - border_fg: Color, - border_bg: Color, + border_color: Color, + bg: Color, } impl<'a> Default for Block<'a> { fn default() -> Block<'a> { Block { title: None, - title_fg: Color::White, - title_bg: Color::Black, + title_color: Color::Reset, borders: border::NONE, - border_fg: Color::White, - border_bg: Color::Black, + border_color: Color::Reset, + bg: Color::Reset, } } } @@ -34,27 +32,21 @@ impl<'a> Block<'a> { self } - pub fn title_fg(mut self, color: Color) -> Block<'a> { - self.title_fg = color; + pub fn title_color(mut self, color: Color) -> Block<'a> { + self.title_color = color; self } - pub fn title_bg(mut self, color: Color) -> Block<'a> { - self.title_bg = color; + pub fn border_color(mut self, color: Color) -> Block<'a> { + self.border_color = color; self } - pub fn border_fg(mut self, color: Color) -> Block<'a> { - self.border_fg = color; + pub fn bg(mut self, color: Color) -> Block<'a> { + self.bg = color; self } - pub fn border_bg(mut self, color: Color) -> Block<'a> { - self.border_bg = color; - self - } - - pub fn borders(mut self, flag: border::Flags) -> Block<'a> { self.borders = flag; self @@ -95,40 +87,24 @@ impl<'a> Widget<'a> for Block<'a> { // Sides if self.borders.intersects(border::LEFT) { for y in 0..area.height { - buf.update_cell(0, y, |c| { - c.symbol = line::VERTICAL; - c.fg = self.border_fg; - c.bg = self.border_bg; - }); + buf.update_cell(0, y, line::VERTICAL, self.border_color, self.bg); } } if self.borders.intersects(border::TOP) { for x in 0..area.width { - buf.update_cell(x, 0, |c| { - c.symbol = line::HORIZONTAL; - c.fg = self.border_fg; - c.bg = self.border_bg; - }); + buf.update_cell(x, 0, line::HORIZONTAL, self.border_color, self.bg); } } if self.borders.intersects(border::RIGHT) { let x = area.width - 1; for y in 0..area.height { - buf.update_cell(x, y, |c| { - c.symbol = line::VERTICAL; - c.fg = self.border_fg; - c.bg = self.border_bg; - }); + buf.update_cell(x, y, line::VERTICAL, self.border_color, self.bg); } } if self.borders.intersects(border::BOTTOM) { let y = area.height - 1; for x in 0..area.width { - buf.update_cell(x, y, |c| { - c.symbol = line::HORIZONTAL; - c.fg = self.border_fg; - c.bg = self.border_bg; - }); + buf.update_cell(x, y, line::HORIZONTAL, self.border_color, self.bg); } } @@ -151,7 +127,7 @@ impl<'a> Widget<'a> for Block<'a> { } else { 0 }; - buf.set_string(margin_x, 0, title, self.title_fg, self.title_bg); + buf.set_string(margin_x, 0, title, self.title_color, self.bg); } buf } diff --git a/src/widgets/chart.rs b/src/widgets/chart.rs index ea91036..790055d 100644 --- a/src/widgets/chart.rs +++ b/src/widgets/chart.rs @@ -1,4 +1,4 @@ -use std::cmp::{min, max}; +use std::cmp::max; use unicode_width::UnicodeWidthStr; @@ -10,16 +10,22 @@ use symbols; pub struct Axis<'a> { title: Option<&'a str>, + title_color: Color, bounds: [f64; 2], labels: Option<&'a [&'a str]>, + labels_color: Color, + color: Color, } impl<'a> Default for Axis<'a> { fn default() -> Axis<'a> { Axis { title: None, + title_color: Color::Reset, bounds: [0.0, 0.0], labels: None, + labels_color: Color::Reset, + color: Color::Reset, } } } @@ -30,6 +36,11 @@ impl<'a> Axis<'a> { self } + pub fn title_color(mut self, color: Color) -> Axis<'a> { + self.title_color = color; + self + } + pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a> { self.bounds = bounds; self @@ -40,18 +51,14 @@ impl<'a> Axis<'a> { self } - fn title_width(&self) -> u16 { - match self.title { - Some(title) => title.width() as u16, - None => 0, - } + pub fn labels_color(mut self, color: Color) -> Axis<'a> { + self.labels_color = color; + self } - 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 fn color(mut self, color: Color) -> Axis<'a> { + self.color = color; + self } } @@ -64,7 +71,7 @@ impl<'a> Default for Dataset<'a> { fn default() -> Dataset<'a> { Dataset { data: &[], - color: Color::White, + color: Color::Reset, } } } @@ -95,12 +102,36 @@ impl<'a> Default for Chart<'a> { block: None, x_axis: Axis::default(), y_axis: Axis::default(), - bg: Color::Black, + bg: Color::Reset, datasets: &[], } } } +struct ChartLayout { + legend_x: Option<(u16, u16)>, + legend_y: Option<(u16, u16)>, + label_x: Option, + label_y: Option, + axis_x: Option, + axis_y: Option, + graph_area: Rect, +} + +impl Default for ChartLayout { + fn default() -> ChartLayout { + ChartLayout { + legend_x: None, + legend_y: None, + label_x: None, + label_y: None, + axis_x: None, + axis_y: None, + graph_area: Rect::default(), + } + } +} + impl<'a> Chart<'a> { pub fn block(&'a mut self, block: Block<'a>) -> &mut Chart<'a> { self.block = Some(block); @@ -126,6 +157,54 @@ impl<'a> Chart<'a> { self.datasets = datasets; self } + + fn layout(&self, inner: &Rect, outer: &Rect) -> ChartLayout { + let mut layout = ChartLayout::default(); + let mut x = inner.x - outer.x; + let mut y = inner.height - 1 + (inner.y - outer.y); + + if self.x_axis.labels.is_some() && y > 1 { + layout.label_x = Some(y); + y -= 1; + } + + if let Some(labels) = self.y_axis.labels { + let max_width = labels.iter().fold(0, |acc, l| max(l.width(), acc)) as u16; + if x + max_width < inner.width { + layout.label_y = Some(x); + x += max_width; + } + } + + if self.x_axis.labels.is_some() && y > 1 { + layout.axis_x = Some(y); + y -= 1; + } + + if self.y_axis.labels.is_some() && x + 1 < inner.width { + layout.axis_y = Some(x); + x += 1; + } + + if x < inner.width && y > 1 { + layout.graph_area = Rect::new(outer.x + x, inner.y, inner.width - x, y); + } + + 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)); + } + } + + 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, inner.y - outer.y)); + } + } + layout + } } impl<'a> Widget<'a> for Chart<'a> { @@ -135,24 +214,93 @@ impl<'a> Widget<'a> for Chart<'a> { None => (Buffer::empty(*area), *area), }; - let margin_x = chart_area.x - area.x; - let margin_y = chart_area.y - area.y; + let layout = self.layout(&chart_area, &area); + let width = layout.graph_area.width; + let height = layout.graph_area.height; + let margin_x = layout.graph_area.x - area.x; + let margin_y = layout.graph_area.y - area.y; + + if let Some((x, y)) = layout.legend_x { + let title = self.x_axis.title.unwrap(); + buf.set_string(x, y, title, self.x_axis.title_color, self.bg); + } + + if let Some((x, y)) = layout.legend_y { + let title = self.y_axis.title.unwrap(); + buf.set_string(x, y, title, self.y_axis.title_color, self.bg); + } + + if let Some(y) = layout.label_x { + let labels = self.x_axis.labels.unwrap(); + let total_width = labels.iter().fold(0, |acc, l| l.width() + acc) as u16; + let labels_len = labels.len() as u16; + if total_width < width && labels_len > 1 { + for (i, label) in labels.iter().enumerate() { + buf.set_string(margin_x + i as u16 * (width - 1) / (labels_len - 1) - + label.width() as u16, + y, + label, + self.x_axis.labels_color, + self.bg); + } + } + } + + if let Some(x) = layout.label_y { + let labels = self.y_axis.labels.unwrap(); + let labels_len = labels.len() as u16; + if labels_len > 1 { + for (i, label) in labels.iter().rev().enumerate() { + buf.set_string(x, + margin_y + i as u16 * (height - 1) / (labels_len - 1), + label, + self.y_axis.labels_color, + self.bg); + } + } + } + + if let Some(y) = layout.axis_x { + for x in 0..width { + buf.update_cell(margin_x + x, + y, + symbols::line::HORIZONTAL, + self.x_axis.color, + self.bg); + } + } + + if let Some(x) = layout.axis_y { + for y in 0..height { + buf.update_cell(x, + margin_y + y, + symbols::line::VERTICAL, + self.y_axis.color, + self.bg); + } + } + + if let Some(y) = layout.axis_x { + if let Some(x) = layout.axis_y { + buf.update_cell(x, y, symbols::line::BOTTOM_LEFT, self.x_axis.color, self.bg); + } + } 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] { + 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 / + let dy = (self.y_axis.bounds[1] - y) * height 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 / + let dx = (self.x_axis.bounds[1] - x) * width as f64 / (self.x_axis.bounds[1] - self.x_axis.bounds[0]); - buf.update_cell(dx as u16 + margin_x, dy as u16 + margin_y, |c| { - c.symbol = symbols::DOT; - c.fg = dataset.color; - c.bg = self.bg; - }) + buf.update_cell(dx as u16 + margin_x, + dy as u16 + margin_y, + symbols::BLACK_CIRCLE, + dataset.color, + self.bg); } } buf diff --git a/src/widgets/gauge.rs b/src/widgets/gauge.rs index 365e01e..e6b67bc 100644 --- a/src/widgets/gauge.rs +++ b/src/widgets/gauge.rs @@ -1,3 +1,5 @@ +use std::cmp::{max, min}; + use widgets::{Widget, Block}; use buffer::Buffer; use style::Color; @@ -31,8 +33,8 @@ impl<'a> Default for Gauge<'a> { block: None, percent: 0, percent_string: String::from("0%"), - bg: Color::White, - fg: Color::Black, + bg: Color::Reset, + fg: Color::Reset, } } } @@ -73,18 +75,16 @@ impl<'a> Widget<'a> for Gauge<'a> { let margin_y = gauge_area.y - area.y; // Gauge let width = (gauge_area.width * self.percent) / 100; + for i in 0..width { + buf.update_cell(margin_x + i, margin_y, " ", self.fg, self.bg); + } // Label let len = self.percent_string.len() as u16; let middle = gauge_area.width / 2 - len / 2; buf.set_string(middle, margin_y, &self.percent_string, self.bg, self.fg); - for i in 0..width { - buf.update_cell(margin_x + i, margin_y, |c| { - if c.symbol == "" { - c.symbol = " " - }; - c.fg = self.fg; - c.bg = self.bg; - }) + let bound = max(middle, min(middle + len, width)); + for i in middle..bound { + buf.update_colors(margin_x + i, margin_y, self.fg, self.bg); } } buf diff --git a/src/widgets/sparkline.rs b/src/widgets/sparkline.rs index da05f21..9bfb1f2 100644 --- a/src/widgets/sparkline.rs +++ b/src/widgets/sparkline.rs @@ -18,8 +18,8 @@ impl<'a> Default for Sparkline<'a> { fn default() -> Sparkline<'a> { Sparkline { block: None, - fg: Color::White, - bg: Color::Black, + fg: Color::Reset, + bg: Color::Reset, data: Vec::new(), max: None, } @@ -76,21 +76,19 @@ impl<'a> Widget<'a> for Sparkline<'a> { .collect::>(); for j in (0..spark_area.height).rev() { for (i, d) in data.iter_mut().take(max_index).enumerate() { - buf.update_cell(margin_x + i as u16, margin_y + j, |c| { - c.symbol = match *d { - 0 => " ", - 1 => bar::ONE_EIGHTH, - 2 => bar::ONE_QUATER, - 3 => bar::THREE_EIGHTHS, - 4 => bar::HALF, - 5 => bar::FIVE_EIGHTHS, - 6 => bar::THREE_QUATERS, - 7 => bar::SEVEN_EIGHTHS, - _ => bar::FULL, - }; - c.fg = self.fg; - c.bg = self.bg; - }); + let symbol = match *d { + 0 => " ", + 1 => bar::ONE_EIGHTH, + 2 => bar::ONE_QUATER, + 3 => bar::THREE_EIGHTHS, + 4 => bar::HALF, + 5 => bar::FIVE_EIGHTHS, + 6 => bar::THREE_QUATERS, + 7 => bar::SEVEN_EIGHTHS, + _ => bar::FULL, + }; + buf.update_cell(margin_x + i as u16, margin_y + j, symbol, self.fg, self.bg); + if *d > 8 { *d -= 8; } else { diff --git a/src/widgets/text.rs b/src/widgets/text.rs index aa90afa..e11e03c 100644 --- a/src/widgets/text.rs +++ b/src/widgets/text.rs @@ -17,8 +17,8 @@ impl<'a> Default for Text<'a> { fn default() -> Text<'a> { Text { block: None, - fg: Color::White, - bg: Color::Black, + fg: Color::Reset, + bg: Color::Reset, text: "", colors: &[], } @@ -67,10 +67,8 @@ impl<'a> Widget<'a> for Text<'a> { } for &(x, y, width, fg, bg) in self.colors { for i in 0..width { - buf.update_cell(x + i, y + margin_y, |c| { - c.fg = fg; - c.bg = bg; - }) + buf.set_fg(x + i, y + margin_y, fg); + buf.set_bg(x + i, y + margin_y, fg); } } buf