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.
pull/266/head
Florian Dehau 4 years ago
parent cee65ed283
commit 584e1b0500

@ -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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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)];

@ -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,
}

@ -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<Color>,
}
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<u16>,
colors: Vec<Color>,
}
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<char>,
colors: Vec<Color>,
}
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<dyn Grid>,
dirty: bool,
layers: Vec<Layer>,
labels: Vec<Label<'a>>,
}
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<dyn Grid> = 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<F>,
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)

@ -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 {

@ -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};

@ -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()

Loading…
Cancel
Save