From 584e1b05007245fa333ad4bff5924f8eead744d1 Mon Sep 17 00:00:00 2001 From: Florian Dehau Date: Tue, 14 Apr 2020 01:01:01 +0200 Subject: [PATCH] refactor(widgets/canvas): allow canvas to render with a simple dot character instead of braille patterns This change allows developers to gracefully degrade the output if the targeted terminal does not support the full range of unicode symbols. --- examples/chart.rs | 11 +- src/symbols.rs | 19 ++++ src/widgets/canvas/mod.rs | 204 +++++++++++++++++++++++++++++--------- src/widgets/chart.rs | 88 ++++++---------- src/widgets/mod.rs | 2 +- tests/chart.rs | 15 +-- 6 files changed, 223 insertions(+), 116 deletions(-) diff --git a/examples/chart.rs b/examples/chart.rs index 87fe175..b899b33 100644 --- a/examples/chart.rs +++ b/examples/chart.rs @@ -11,7 +11,8 @@ use tui::{ backend::TermionBackend, layout::{Constraint, Direction, Layout}, style::{Color, Modifier, Style}, - widgets::{Axis, Block, Borders, Chart, Dataset, GraphType, Marker}, + symbols, + widgets::{Axis, Block, Borders, Chart, Dataset, GraphType}, Terminal, }; @@ -99,12 +100,12 @@ fn main() -> Result<(), Box> { let datasets = [ Dataset::default() .name("data2") - .marker(Marker::Dot) + .marker(symbols::Marker::Dot) .style(Style::default().fg(Color::Cyan)) .data(&app.data1), Dataset::default() .name("data3") - .marker(Marker::Braille) + .marker(symbols::Marker::Braille) .style(Style::default().fg(Color::Yellow)) .data(&app.data2), ]; @@ -136,7 +137,7 @@ fn main() -> Result<(), Box> { let datasets = [Dataset::default() .name("data") - .marker(Marker::Braille) + .marker(symbols::Marker::Braille) .style(Style::default().fg(Color::Yellow)) .graph_type(GraphType::Line) .data(&DATA)]; @@ -168,7 +169,7 @@ fn main() -> Result<(), Box> { let datasets = [Dataset::default() .name("data") - .marker(Marker::Braille) + .marker(symbols::Marker::Braille) .style(Style::default().fg(Color::Yellow)) .graph_type(GraphType::Line) .data(&DATA2)]; diff --git a/src/symbols.rs b/src/symbols.rs index d6fcdf4..2a24483 100644 --- a/src/symbols.rs +++ b/src/symbols.rs @@ -207,3 +207,22 @@ pub mod line { } pub const DOT: &str = "•"; + +pub mod braille { + pub const BLANK: u16 = 0x2800; + pub const DOTS: [[u16; 2]; 4] = [ + [0x0001, 0x0008], + [0x0002, 0x0010], + [0x0004, 0x0020], + [0x0040, 0x0080], + ]; +} + +/// Marker to use when plotting data points +#[derive(Debug, Clone, Copy)] +pub enum Marker { + /// One point per cell + Dot, + /// Up to 8 points per cell + Braille, +} diff --git a/src/widgets/canvas/mod.rs b/src/widgets/canvas/mod.rs index 4423160..ab6c272 100644 --- a/src/widgets/canvas/mod.rs +++ b/src/widgets/canvas/mod.rs @@ -13,19 +13,11 @@ use crate::{ buffer::Buffer, layout::Rect, style::{Color, Style}, + symbols, widgets::{Block, Widget}, }; use std::fmt::Debug; -pub const DOTS: [[u16; 2]; 4] = [ - [0x0001, 0x0008], - [0x0002, 0x0010], - [0x0004, 0x0020], - [0x0040, 0x0080], -]; -pub const BRAILLE_OFFSET: u16 = 0x2800; -pub const BRAILLE_BLANK: char = '⠀'; - /// Interface for all shapes that may be drawn on a Canvas widget. pub trait Shape { fn draw(&self, painter: &mut Painter); @@ -46,19 +38,50 @@ struct Layer { colors: Vec, } +trait Grid: Debug { + fn width(&self) -> u16; + fn height(&self) -> u16; + fn resolution(&self) -> (f64, f64); + fn paint(&mut self, x: usize, y: usize, color: Color); + fn save(&self) -> Layer; + fn reset(&mut self); +} + #[derive(Debug, Clone)] -struct Grid { +struct BrailleGrid { + width: u16, + height: u16, cells: Vec, colors: Vec, } -impl Grid { - fn new(width: usize, height: usize) -> Grid { - Grid { - cells: vec![BRAILLE_OFFSET; width * height], - colors: vec![Color::Reset; width * height], +impl BrailleGrid { + fn new(width: u16, height: u16) -> BrailleGrid { + let length = usize::from(width * height); + BrailleGrid { + width, + height, + cells: vec![symbols::braille::BLANK; length], + colors: vec![Color::Reset; length], } } +} + +impl Grid for BrailleGrid { + fn width(&self) -> u16 { + self.width + } + + fn height(&self) -> u16 { + self.height + } + + fn resolution(&self) -> (f64, f64) { + ( + f64::from(self.width) * 2.0 - 1.0, + f64::from(self.height) * 4.0 - 1.0, + ) + } fn save(&self) -> Layer { Layer { @@ -69,28 +92,98 @@ impl Grid { fn reset(&mut self) { for c in &mut self.cells { - *c = BRAILLE_OFFSET; + *c = symbols::braille::BLANK; } for c in &mut self.colors { *c = Color::Reset; } } + + fn paint(&mut self, x: usize, y: usize, color: Color) { + let index = y / 4 * self.width as usize + x / 2; + if let Some(c) = self.cells.get_mut(index) { + *c |= symbols::braille::DOTS[y % 4][x % 2]; + } + if let Some(c) = self.colors.get_mut(index) { + *c = color; + } + } +} + +#[derive(Debug, Clone)] +struct DotGrid { + width: u16, + height: u16, + cells: Vec, + colors: Vec, +} + +impl DotGrid { + fn new(width: u16, height: u16) -> DotGrid { + let length = usize::from(width * height); + DotGrid { + width, + height, + cells: vec![' '; length], + colors: vec![Color::Reset; length], + } + } +} + +impl Grid for DotGrid { + fn width(&self) -> u16 { + self.width + } + + fn height(&self) -> u16 { + self.height + } + + fn resolution(&self) -> (f64, f64) { + (f64::from(self.width) - 1.0, f64::from(self.height) - 1.0) + } + + fn save(&self) -> Layer { + Layer { + string: self.cells.iter().collect(), + colors: self.colors.clone(), + } + } + + fn reset(&mut self) { + for c in &mut self.cells { + *c = ' '; + } + for c in &mut self.colors { + *c = Color::Reset; + } + } + + fn paint(&mut self, x: usize, y: usize, color: Color) { + let index = y * self.width as usize + x; + if let Some(c) = self.cells.get_mut(index) { + *c = '•'; + } + if let Some(c) = self.colors.get_mut(index) { + *c = color; + } + } } #[derive(Debug)] pub struct Painter<'a, 'b> { context: &'a mut Context<'b>, - resolution: [f64; 2], + resolution: (f64, f64), } impl<'a, 'b> Painter<'a, 'b> { - /// Convert the (x, y) coordinates to location of a braille dot on the grid + /// Convert the (x, y) coordinates to location of a point on the grid /// /// # Examples: /// ``` - /// use tui::widgets::canvas::{Painter, Context}; + /// use tui::{symbols, widgets::canvas::{Painter, Context}}; /// - /// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0]); + /// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0], symbols::Marker::Braille); /// let mut painter = Painter::from(&mut ctx); /// let point = painter.get_point(1.0, 0.0); /// assert_eq!(point, Some((0, 7))); @@ -113,65 +206,63 @@ impl<'a, 'b> Painter<'a, 'b> { } let width = (self.context.x_bounds[1] - self.context.x_bounds[0]).abs(); let height = (self.context.y_bounds[1] - self.context.y_bounds[0]).abs(); - let x = ((x - left) * self.resolution[0] / width) as usize; - let y = ((top - y) * self.resolution[1] / height) as usize; + let x = ((x - left) * self.resolution.0 / width) as usize; + let y = ((top - y) * self.resolution.1 / height) as usize; Some((x, y)) } - /// Paint a braille dot + /// Paint a point of the grid /// /// # Examples: /// ``` - /// use tui::{style::Color, widgets::canvas::{Painter, Context}}; + /// use tui::{style::Color, symbols, widgets::canvas::{Painter, Context}}; /// - /// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0]); + /// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille); /// let mut painter = Painter::from(&mut ctx); /// let cell = painter.paint(1, 3, Color::Red); /// ``` pub fn paint(&mut self, x: usize, y: usize, color: Color) { - let index = y / 4 * self.context.width as usize + x / 2; - if let Some(c) = self.context.grid.cells.get_mut(index) { - *c |= DOTS[y % 4][x % 2]; - } - if let Some(c) = self.context.grid.colors.get_mut(index) { - *c = color; - } + self.context.grid.paint(x, y, color); } } impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> { fn from(context: &'a mut Context<'b>) -> Painter<'a, 'b> { + let resolution = context.grid.resolution(); Painter { - resolution: [ - f64::from(context.width) * 2.0 - 1.0, - f64::from(context.height) * 4.0 - 1.0, - ], context, + resolution, } } } /// Holds the state of the Canvas when painting to it. -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct Context<'a> { - width: u16, - height: u16, x_bounds: [f64; 2], y_bounds: [f64; 2], - grid: Grid, + grid: Box, dirty: bool, layers: Vec, labels: Vec>, } impl<'a> Context<'a> { - pub fn new(width: u16, height: u16, x_bounds: [f64; 2], y_bounds: [f64; 2]) -> Context<'a> { + pub fn new( + width: u16, + height: u16, + x_bounds: [f64; 2], + y_bounds: [f64; 2], + marker: symbols::Marker, + ) -> Context<'a> { + let grid: Box = match marker { + symbols::Marker::Dot => Box::new(DotGrid::new(width, height)), + symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)), + }; Context { - width, - height, x_bounds, y_bounds, - grid: Grid::new(width as usize, height as usize), + grid, dirty: false, layers: Vec::new(), labels: Vec::new(), @@ -252,6 +343,7 @@ where y_bounds: [f64; 2], painter: Option, background_color: Color, + marker: symbols::Marker, } impl<'a, F> Default for Canvas<'a, F> @@ -265,6 +357,7 @@ where y_bounds: [0.0, 0.0], painter: None, background_color: Color::Reset, + marker: symbols::Marker::Braille, } } } @@ -277,10 +370,12 @@ where self.block = Some(block); self } + pub fn x_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> { self.x_bounds = bounds; self } + pub fn y_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> { self.y_bounds = bounds; self @@ -296,6 +391,24 @@ where self.background_color = color; self } + + /// Change the type of points used to draw the shapes. By default the braille patterns are used + /// as they provide a more fine grained result but you might want to use the simple dot instead + /// if the targeted terminal does not support those symbols. + /// + /// # Examples + /// + /// ``` + /// # use tui::widgets::canvas::Canvas; + /// # use tui::symbols; + /// Canvas::default().marker(symbols::Marker::Braille).paint(|ctx| {}); + /// + /// Canvas::default().marker(symbols::Marker::Dot).paint(|ctx| {}); + /// ``` + pub fn marker(mut self, marker: symbols::Marker) -> Canvas<'a, F> { + self.marker = marker; + self + } } impl<'a, F> Widget for Canvas<'a, F> @@ -324,6 +437,7 @@ where canvas_area.height, self.x_bounds, self.y_bounds, + self.marker, ); // Paint to this context painter(&mut ctx); @@ -337,7 +451,7 @@ where .zip(layer.colors.into_iter()) .enumerate() { - if ch != BRAILLE_BLANK { + 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()) .set_char(ch) diff --git a/src/widgets/chart.rs b/src/widgets/chart.rs index 44738c8..6cccded 100644 --- a/src/widgets/chart.rs +++ b/src/widgets/chart.rs @@ -81,14 +81,6 @@ where } } -/// Marker to use when plotting data points -pub enum Marker { - /// One point per cell - Dot, - /// Up to 8 points per cell - Braille, -} - /// Used to determine which style of graphing to use pub enum GraphType { /// Draw each point @@ -104,7 +96,7 @@ pub struct Dataset<'a> { /// A reference to the actual data data: &'a [(f64, f64)], /// Symbol used for each points of this dataset - marker: Marker, + marker: symbols::Marker, /// Determines graph type used for drawing points graph_type: GraphType, /// Style used to plot this dataset @@ -116,7 +108,7 @@ impl<'a> Default for Dataset<'a> { Dataset { name: Cow::from(""), data: &[], - marker: Marker::Dot, + marker: symbols::Marker::Dot, graph_type: GraphType::Scatter, style: Style::default(), } @@ -137,7 +129,7 @@ impl<'a> Dataset<'a> { self } - pub fn marker(mut self, marker: Marker) -> Dataset<'a> { + pub fn marker(mut self, marker: symbols::Marker) -> Dataset<'a> { self.marker = marker; self } @@ -195,7 +187,8 @@ impl Default for ChartLayout { /// # Examples /// /// ``` -/// # use tui::widgets::{Block, Borders, Chart, Axis, Dataset, Marker, GraphType}; +/// # use tui::symbols; +/// # use tui::widgets::{Block, Borders, Chart, Axis, Dataset, GraphType}; /// # use tui::style::{Style, Color}; /// Chart::default() /// .block(Block::default().title("Chart")) @@ -213,13 +206,13 @@ impl Default for ChartLayout { /// .labels(&["0.0", "5.0", "10.0"])) /// .datasets(&[Dataset::default() /// .name("data1") -/// .marker(Marker::Dot) +/// .marker(symbols::Marker::Dot) /// .graph_type(GraphType::Scatter) /// .style(Style::default().fg(Color::Cyan)) /// .data(&[(0.0, 5.0), (1.0, 6.0), (1.5, 6.434)]), /// Dataset::default() /// .name("data2") -/// .marker(Marker::Braille) +/// .marker(symbols::Marker::Braille) /// .graph_type(GraphType::Line) /// .style(Style::default().fg(Color::Magenta)) /// .data(&[(4.0, 5.0), (5.0, 8.0), (7.66, 13.5)])]); @@ -491,52 +484,29 @@ where } for dataset in self.datasets { - match dataset.marker { - Marker::Dot => { - for &(x, y) in dataset.data.iter().filter(|&&(x, y)| { - !(x < self.x_axis.bounds[0] - || x > self.x_axis.bounds[1] - || y < self.y_axis.bounds[0] - || y > self.y_axis.bounds[1]) - }) { - let dy = ((self.y_axis.bounds[1] - y) * f64::from(graph_area.height - 1) - / (self.y_axis.bounds[1] - self.y_axis.bounds[0])) - as u16; - let dx = ((x - self.x_axis.bounds[0]) * f64::from(graph_area.width - 1) - / (self.x_axis.bounds[1] - self.x_axis.bounds[0])) - as u16; - - buf.get_mut(graph_area.left() + dx, graph_area.top() + dy) - .set_symbol(symbols::DOT) - .set_fg(dataset.style.fg) - .set_bg(dataset.style.bg); - } - } - Marker::Braille => { - Canvas::default() - .background_color(self.style.bg) - .x_bounds(self.x_axis.bounds) - .y_bounds(self.y_axis.bounds) - .paint(|ctx| { - ctx.draw(&Points { - coords: dataset.data, + Canvas::default() + .background_color(self.style.bg) + .x_bounds(self.x_axis.bounds) + .y_bounds(self.y_axis.bounds) + .marker(dataset.marker) + .paint(|ctx| { + ctx.draw(&Points { + coords: dataset.data, + color: dataset.style.fg, + }); + if let GraphType::Line = dataset.graph_type { + for i in 0..dataset.data.len() - 1 { + ctx.draw(&Line { + x1: dataset.data[i].0, + y1: dataset.data[i].1, + x2: dataset.data[i + 1].0, + y2: dataset.data[i + 1].1, color: dataset.style.fg, - }); - if let GraphType::Line = dataset.graph_type { - for i in 0..dataset.data.len() - 1 { - ctx.draw(&Line { - x1: dataset.data[i].0, - y1: dataset.data[i].1, - x2: dataset.data[i + 1].0, - y2: dataset.data[i + 1].1, - color: dataset.style.fg, - }) - } - } - }) - .render(graph_area, buf); - } - } + }) + } + } + }) + .render(graph_area, buf); } if let Some(legend_area) = layout.legend_area { diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 98b6fcc..87255d2 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -16,7 +16,7 @@ mod tabs; pub use self::barchart::BarChart; pub use self::block::{Block, BorderType}; -pub use self::chart::{Axis, Chart, Dataset, GraphType, Marker}; +pub use self::chart::{Axis, Chart, Dataset, GraphType}; pub use self::clear::Clear; pub use self::gauge::Gauge; pub use self::list::{List, ListState}; diff --git a/tests/chart.rs b/tests/chart.rs index 3c30bfb..04e386c 100644 --- a/tests/chart.rs +++ b/tests/chart.rs @@ -1,8 +1,11 @@ -use tui::backend::TestBackend; -use tui::layout::Rect; -use tui::style::{Color, Style}; -use tui::widgets::{Axis, Block, Borders, Chart, Dataset, Marker}; -use tui::Terminal; +use tui::{ + backend::TestBackend, + layout::Rect, + style::{Color, Style}, + symbols, + widgets::{Axis, Block, Borders, Chart, Dataset}, + Terminal, +}; #[test] fn zero_axes_ok() { @@ -12,7 +15,7 @@ fn zero_axes_ok() { terminal .draw(|mut f| { let datasets = [Dataset::default() - .marker(Marker::Braille) + .marker(symbols::Marker::Braille) .style(Style::default().fg(Color::Magenta)) .data(&[(0.0, 0.0)])]; let chart = Chart::default()