mirror of
https://github.com/ratatui-org/ratatui
synced 2025-02-16 14:08:44 +00:00
refactor(canvas): update shape drawing strategy
* Update the `Shape` trait. Instead of returning an iterator of point, all shapes are now aware of the surface they will be drawn to through a `Painter`. In order to draw themselves, they paint points of the "braille grid". * Rewrite how lines are drawn using a common line drawing algorithm (Bresenham).
This commit is contained in:
parent
a6b35031ae
commit
140db9b2e2
10 changed files with 438 additions and 280 deletions
|
@ -20,10 +20,10 @@ use crate::util::event::{Config, Event, Events};
|
|||
struct App {
|
||||
x: f64,
|
||||
y: f64,
|
||||
ball: Rect,
|
||||
ball: Rectangle,
|
||||
playground: Rect,
|
||||
vx: u16,
|
||||
vy: u16,
|
||||
vx: f64,
|
||||
vy: f64,
|
||||
dir_x: bool,
|
||||
dir_y: bool,
|
||||
}
|
||||
|
@ -33,21 +33,29 @@ impl App {
|
|||
App {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
ball: Rect::new(10, 30, 10, 10),
|
||||
ball: Rectangle {
|
||||
x: 10.0,
|
||||
y: 30.0,
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
color: Color::Yellow,
|
||||
},
|
||||
playground: Rect::new(10, 10, 100, 100),
|
||||
vx: 1,
|
||||
vy: 1,
|
||||
vx: 1.0,
|
||||
vy: 1.0,
|
||||
dir_x: true,
|
||||
dir_y: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
if self.ball.left() < self.playground.left() || self.ball.right() > self.playground.right()
|
||||
if self.ball.x < self.playground.left() as f64
|
||||
|| self.ball.x + self.ball.width > self.playground.right() as f64
|
||||
{
|
||||
self.dir_x = !self.dir_x;
|
||||
}
|
||||
if self.ball.top() < self.playground.top() || self.ball.bottom() > self.playground.bottom()
|
||||
if self.ball.y < self.playground.top() as f64
|
||||
|| self.ball.y + self.ball.height > self.playground.bottom() as f64
|
||||
{
|
||||
self.dir_y = !self.dir_y;
|
||||
}
|
||||
|
@ -77,7 +85,7 @@ fn main() -> Result<(), failure::Error> {
|
|||
|
||||
// Setup event handlers
|
||||
let config = Config {
|
||||
tick_rate: Duration::from_millis(100),
|
||||
tick_rate: Duration::from_millis(250),
|
||||
..Default::default()
|
||||
};
|
||||
let events = Events::with_config(config);
|
||||
|
@ -106,10 +114,7 @@ fn main() -> Result<(), failure::Error> {
|
|||
let canvas = Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Pong"))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Rectangle {
|
||||
rect: app.ball,
|
||||
color: Color::Yellow,
|
||||
});
|
||||
ctx.draw(&app.ball);
|
||||
})
|
||||
.x_bounds([10.0, 110.0])
|
||||
.y_bounds([10.0, 110.0]);
|
||||
|
|
|
@ -1,19 +1,30 @@
|
|||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::{
|
||||
event::{Event, Events},
|
||||
SinSignal,
|
||||
};
|
||||
use std::io;
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType, Marker},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
use termion::event::Key;
|
||||
use termion::input::MouseTerminal;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use tui::backend::TermionBackend;
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Axis, Block, Borders, Chart, Dataset, Marker};
|
||||
use tui::Terminal;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use crate::util::SinSignal;
|
||||
const DATA: [(f64, f64); 5] = [(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)];
|
||||
const DATA2: [(f64, f64); 7] = [
|
||||
(0.0, 0.0),
|
||||
(10.0, 1.0),
|
||||
(20.0, 0.5),
|
||||
(30.0, 1.5),
|
||||
(40.0, 1.0),
|
||||
(50.0, 2.5),
|
||||
(60.0, 3.0),
|
||||
];
|
||||
|
||||
struct App {
|
||||
signal1: SinSignal,
|
||||
|
@ -69,6 +80,17 @@ fn main() -> Result<(), failure::Error> {
|
|||
loop {
|
||||
terminal.draw(|mut f| {
|
||||
let size = f.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(size);
|
||||
let x_labels = [
|
||||
format!("{}", app.window[0]),
|
||||
format!("{}", (app.window[0] + app.window[1]) / 2.0),
|
||||
|
@ -89,7 +111,7 @@ fn main() -> Result<(), failure::Error> {
|
|||
let chart = Chart::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart")
|
||||
.title("Chart 1")
|
||||
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
|
@ -110,7 +132,71 @@ fn main() -> Result<(), failure::Error> {
|
|||
.labels(&["-20", "0", "20"]),
|
||||
)
|
||||
.datasets(&datasets);
|
||||
f.render_widget(chart, size);
|
||||
f.render_widget(chart, chunks[0]);
|
||||
|
||||
let datasets = [Dataset::default()
|
||||
.name("data")
|
||||
.marker(Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA)];
|
||||
let chart = Chart::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart 2")
|
||||
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::ITALIC))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(&["0", "2.5", "5.0"]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::ITALIC))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(&["0", "2.5", "5.0"]),
|
||||
)
|
||||
.datasets(&datasets);
|
||||
f.render_widget(chart, chunks[1]);
|
||||
|
||||
let datasets = [Dataset::default()
|
||||
.name("data")
|
||||
.marker(Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA2)];
|
||||
let chart = Chart::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart 3")
|
||||
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::ITALIC))
|
||||
.bounds([0.0, 50.0])
|
||||
.labels(&["0", "25", "50"]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::ITALIC))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(&["0", "2.5", "5"]),
|
||||
)
|
||||
.datasets(&datasets);
|
||||
f.render_widget(chart, chunks[2]);
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
|
|
|
@ -259,12 +259,10 @@ where
|
|||
});
|
||||
ctx.layer();
|
||||
ctx.draw(&Rectangle {
|
||||
rect: Rect {
|
||||
x: 0,
|
||||
y: 30,
|
||||
width: 10,
|
||||
height: 10,
|
||||
},
|
||||
x: 0.0,
|
||||
y: 30.0,
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
color: Color::Yellow,
|
||||
});
|
||||
for (i, s1) in app.servers.iter().enumerate() {
|
||||
|
|
|
@ -47,8 +47,8 @@ impl Constraint {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Margin {
|
||||
vertical: u16,
|
||||
horizontal: u16,
|
||||
pub vertical: u16,
|
||||
pub horizontal: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
|
@ -58,7 +58,6 @@ pub enum Alignment {
|
|||
Right,
|
||||
}
|
||||
|
||||
// TODO: enforce constraints size once const generics has landed
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Layout {
|
||||
direction: Direction,
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
use super::Shape;
|
||||
use crate::style::Color;
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{Painter, Shape},
|
||||
};
|
||||
|
||||
/// Shape to draw a line from (x1, y1) to (x2, y2) with the given color
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Line {
|
||||
pub x1: f64,
|
||||
pub y1: f64,
|
||||
|
@ -10,63 +13,77 @@ pub struct Line {
|
|||
pub color: Color,
|
||||
}
|
||||
|
||||
pub struct LineIterator {
|
||||
x: f64,
|
||||
y: f64,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
dir_x: f64,
|
||||
dir_y: f64,
|
||||
current: f64,
|
||||
end: f64,
|
||||
}
|
||||
|
||||
impl Iterator for LineIterator {
|
||||
type Item = (f64, f64);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current < self.end {
|
||||
let pos = (
|
||||
self.x + (self.current * self.dx) / self.end * self.dir_x,
|
||||
self.y + (self.current * self.dy) / self.end * self.dir_y,
|
||||
);
|
||||
self.current += 1.0;
|
||||
Some(pos)
|
||||
impl Shape for Line {
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
let (x1, y1) = match painter.get_point(self.x1, self.y1) {
|
||||
Some(c) => c,
|
||||
None => return,
|
||||
};
|
||||
let (x2, y2) = match painter.get_point(self.x2, self.y2) {
|
||||
Some(c) => c,
|
||||
None => return,
|
||||
};
|
||||
let (dx, x_range) = if x2 >= x1 {
|
||||
(x2 - x1, x1..=x2)
|
||||
} else {
|
||||
None
|
||||
(x1 - x2, x2..=x1)
|
||||
};
|
||||
let (dy, y_range) = if y2 >= y1 {
|
||||
(y2 - y1, y1..=y2)
|
||||
} else {
|
||||
(y1 - y2, y2..=y1)
|
||||
};
|
||||
|
||||
if dx == 0 {
|
||||
for y in y_range {
|
||||
painter.paint(x1, y, self.color);
|
||||
}
|
||||
} else if dy == 0 {
|
||||
for x in x_range {
|
||||
painter.paint(x, y1, self.color);
|
||||
}
|
||||
} else if dy < dx {
|
||||
if x1 > x2 {
|
||||
draw_line_low(painter, x2, y2, x1, y1, self.color);
|
||||
} else {
|
||||
draw_line_low(painter, x1, y1, x2, y2, self.color);
|
||||
}
|
||||
} else {
|
||||
if y1 > y2 {
|
||||
draw_line_high(painter, x2, y2, x1, y1, self.color);
|
||||
} else {
|
||||
draw_line_high(painter, x1, y1, x2, y2, self.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Line {
|
||||
type Item = (f64, f64);
|
||||
type IntoIter = LineIterator;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
let dx = self.x1.max(self.x2) - self.x1.min(self.x2);
|
||||
let dy = self.y1.max(self.y2) - self.y1.min(self.y2);
|
||||
let dir_x = if self.x1 <= self.x2 { 1.0 } else { -1.0 };
|
||||
let dir_y = if self.y1 <= self.y2 { 1.0 } else { -1.0 };
|
||||
let end = dx.max(dy);
|
||||
LineIterator {
|
||||
x: self.x1,
|
||||
y: self.y1,
|
||||
dx,
|
||||
dy,
|
||||
dir_x,
|
||||
dir_y,
|
||||
current: 0.0,
|
||||
end,
|
||||
fn draw_line_low(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
|
||||
let dx = (x2 - x1) as isize;
|
||||
let dy = (y2 as isize - y1 as isize).abs();
|
||||
let mut d = 2 * dy - dx;
|
||||
let mut y = y1;
|
||||
for x in x1..=x2 {
|
||||
painter.paint(x, y, color);
|
||||
if d > 0 {
|
||||
y = if y1 > y2 { y - 1 } else { y + 1 };
|
||||
d -= 2 * dx;
|
||||
}
|
||||
d += 2 * dy;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Shape<'a> for Line {
|
||||
fn color(&self) -> Color {
|
||||
self.color
|
||||
}
|
||||
|
||||
fn points(&'a self) -> Box<dyn Iterator<Item = (f64, f64)> + 'a> {
|
||||
Box::new(self.into_iter())
|
||||
fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
|
||||
let dx = (x2 as isize - x1 as isize).abs();
|
||||
let dy = (y2 - y1) as isize;
|
||||
let mut d = 2 * dx - dy;
|
||||
let mut x = x1;
|
||||
for y in y1..=y2 {
|
||||
painter.paint(x, y, color);
|
||||
if d > 0 {
|
||||
x = if x1 > x2 { x - 1 } else { x + 1 };
|
||||
d -= 2 * dy;
|
||||
}
|
||||
d += 2 * dx;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
use crate::style::Color;
|
||||
use crate::widgets::canvas::points::PointsIterator;
|
||||
use crate::widgets::canvas::world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION};
|
||||
use crate::widgets::canvas::Shape;
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{
|
||||
world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION},
|
||||
Painter, Shape,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum MapResolution {
|
||||
Low,
|
||||
High,
|
||||
|
@ -19,6 +22,7 @@ impl MapResolution {
|
|||
}
|
||||
|
||||
/// Shape to draw a world map with the given resolution and color
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Map {
|
||||
pub resolution: MapResolution,
|
||||
pub color: Color,
|
||||
|
@ -33,19 +37,12 @@ impl Default for Map {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> Shape<'a> for Map {
|
||||
fn color(&self) -> Color {
|
||||
self.color
|
||||
}
|
||||
fn points(&'a self) -> Box<dyn Iterator<Item = (f64, f64)> + 'a> {
|
||||
Box::new(self.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Map {
|
||||
type Item = (f64, f64);
|
||||
type IntoIter = PointsIterator<'a>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
PointsIterator::from(self.resolution.data())
|
||||
impl Shape for Map {
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
for (x, y) in self.resolution.data() {
|
||||
if let Some((x, y)) = painter.get_point(*x, *y) {
|
||||
painter.paint(x, y, self.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,13 @@ pub use self::map::{Map, MapResolution};
|
|||
pub use self::points::Points;
|
||||
pub use self::rectangle::Rectangle;
|
||||
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::Rect;
|
||||
use crate::style::{Color, Style};
|
||||
use crate::widgets::{Block, Widget};
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub const DOTS: [[u16; 2]; 4] = [
|
||||
[0x0001, 0x0008],
|
||||
|
@ -24,14 +27,12 @@ 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<'a> {
|
||||
/// Returns the color of the shape
|
||||
fn color(&self) -> Color;
|
||||
/// Returns an iterator over all points of the shape
|
||||
fn points(&'a self) -> Box<dyn Iterator<Item = (f64, f64)> + 'a>;
|
||||
pub trait Shape {
|
||||
fn draw(&self, painter: &mut Painter);
|
||||
}
|
||||
|
||||
/// Label to draw some text on the canvas
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Label<'a> {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
|
@ -39,11 +40,13 @@ pub struct Label<'a> {
|
|||
pub color: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Layer {
|
||||
string: String,
|
||||
colors: Vec<Color>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Grid {
|
||||
cells: Vec<u16>,
|
||||
colors: Vec<Color>,
|
||||
|
@ -74,7 +77,82 @@ impl Grid {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Painter<'a, 'b> {
|
||||
context: &'a mut Context<'b>,
|
||||
resolution: [f64; 2],
|
||||
}
|
||||
|
||||
impl<'a, 'b> Painter<'a, 'b> {
|
||||
/// Convert the (x, y) coordinates to location of a braille dot on the grid
|
||||
///
|
||||
/// # Examples:
|
||||
/// ```
|
||||
/// use tui::widgets::canvas::{Painter, Context};
|
||||
///
|
||||
/// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0]);
|
||||
/// let mut painter = Painter::from(&mut ctx);
|
||||
/// let point = painter.get_point(1.0, 0.0);
|
||||
/// assert_eq!(point, Some((0, 7)));
|
||||
/// let point = painter.get_point(1.5, 1.0);
|
||||
/// assert_eq!(point, Some((1, 3)));
|
||||
/// let point = painter.get_point(0.0, 0.0);
|
||||
/// assert_eq!(point, None);
|
||||
/// let point = painter.get_point(2.0, 2.0);
|
||||
/// assert_eq!(point, Some((3, 0)));
|
||||
/// let point = painter.get_point(1.0, 2.0);
|
||||
/// assert_eq!(point, Some((0, 0)));
|
||||
/// ```
|
||||
pub fn get_point(&self, x: f64, y: f64) -> Option<(usize, usize)> {
|
||||
let left = self.context.x_bounds[0];
|
||||
let right = self.context.x_bounds[1];
|
||||
let top = self.context.y_bounds[1];
|
||||
let bottom = self.context.y_bounds[0];
|
||||
if x < left || x > right || y < bottom || y > top {
|
||||
return None;
|
||||
}
|
||||
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;
|
||||
Some((x, y))
|
||||
}
|
||||
|
||||
/// Paint a braille dot
|
||||
///
|
||||
/// # Examples:
|
||||
/// ```
|
||||
/// use tui::{style::Color, widgets::canvas::{Painter, Context}};
|
||||
///
|
||||
/// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0]);
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
|
||||
fn from(context: &'a mut Context<'b>) -> Painter<'a, 'b> {
|
||||
Painter {
|
||||
resolution: [
|
||||
f64::from(context.width) * 2.0 - 1.0,
|
||||
f64::from(context.height) * 4.0 - 1.0,
|
||||
],
|
||||
context,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds the state of the Canvas when painting to it.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Context<'a> {
|
||||
width: u16,
|
||||
height: u16,
|
||||
|
@ -87,26 +165,27 @@ pub struct Context<'a> {
|
|||
}
|
||||
|
||||
impl<'a> Context<'a> {
|
||||
pub fn new(width: u16, height: u16, x_bounds: [f64; 2], y_bounds: [f64; 2]) -> Context<'a> {
|
||||
Context {
|
||||
width,
|
||||
height,
|
||||
x_bounds,
|
||||
y_bounds,
|
||||
grid: Grid::new(width as usize, height as usize),
|
||||
dirty: false,
|
||||
layers: Vec::new(),
|
||||
labels: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw any object that may implement the Shape trait
|
||||
pub fn draw<'b, S>(&mut self, shape: &'b S)
|
||||
pub fn draw<S>(&mut self, shape: &S)
|
||||
where
|
||||
S: Shape<'b>,
|
||||
S: Shape,
|
||||
{
|
||||
self.dirty = true;
|
||||
let left = self.x_bounds[0];
|
||||
let right = self.x_bounds[1];
|
||||
let bottom = self.y_bounds[0];
|
||||
let top = self.y_bounds[1];
|
||||
for (x, y) in shape
|
||||
.points()
|
||||
.filter(|&(x, y)| !(x <= left || x >= right || y <= bottom || y >= top))
|
||||
{
|
||||
let dy = ((top - y) * f64::from(self.height - 1) * 4.0 / (top - bottom)) as usize;
|
||||
let dx = ((x - left) * f64::from(self.width - 1) * 2.0 / (right - left)) as usize;
|
||||
let index = dy / 4 * self.width as usize + dx / 2;
|
||||
self.grid.cells[index] |= DOTS[dy % 4][dx % 2];
|
||||
self.grid.colors[index] = shape.color();
|
||||
}
|
||||
let mut painter = Painter::from(self);
|
||||
shape.draw(&mut painter);
|
||||
}
|
||||
|
||||
/// Go one layer above in the canvas.
|
||||
|
@ -156,12 +235,10 @@ impl<'a> Context<'a> {
|
|||
/// color: Color::White,
|
||||
/// });
|
||||
/// ctx.draw(&Rectangle {
|
||||
/// rect: Rect {
|
||||
/// x: 10,
|
||||
/// y: 20,
|
||||
/// width: 10,
|
||||
/// height: 10,
|
||||
/// },
|
||||
/// x: 10.0,
|
||||
/// y: 20.0,
|
||||
/// width: 10.0,
|
||||
/// height: 10.0,
|
||||
/// color: Color::Red
|
||||
/// });
|
||||
/// });
|
||||
|
@ -235,61 +312,68 @@ where
|
|||
};
|
||||
|
||||
let width = canvas_area.width as usize;
|
||||
let height = canvas_area.height as usize;
|
||||
|
||||
if let Some(ref painter) = self.painter {
|
||||
// Create a blank context that match the size of the terminal
|
||||
let mut ctx = Context {
|
||||
x_bounds: self.x_bounds,
|
||||
y_bounds: self.y_bounds,
|
||||
width: canvas_area.width,
|
||||
height: canvas_area.height,
|
||||
grid: Grid::new(width, height),
|
||||
dirty: false,
|
||||
layers: Vec::new(),
|
||||
labels: Vec::new(),
|
||||
};
|
||||
// Paint to this context
|
||||
painter(&mut ctx);
|
||||
ctx.finish();
|
||||
let painter = match self.painter {
|
||||
Some(ref p) => p,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Retreive painted points for each layer
|
||||
for layer in ctx.layers {
|
||||
for (i, (ch, color)) in layer
|
||||
.string
|
||||
.chars()
|
||||
.zip(layer.colors.into_iter())
|
||||
.enumerate()
|
||||
{
|
||||
if ch != BRAILLE_BLANK {
|
||||
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)
|
||||
.set_fg(color)
|
||||
.set_bg(self.background_color);
|
||||
}
|
||||
// Create a blank context that match the size of the canvas
|
||||
let mut ctx = Context::new(
|
||||
canvas_area.width,
|
||||
canvas_area.height,
|
||||
self.x_bounds,
|
||||
self.y_bounds,
|
||||
);
|
||||
// Paint to this context
|
||||
painter(&mut ctx);
|
||||
ctx.finish();
|
||||
|
||||
// Retreive painted points for each layer
|
||||
for layer in ctx.layers {
|
||||
for (i, (ch, color)) in layer
|
||||
.string
|
||||
.chars()
|
||||
.zip(layer.colors.into_iter())
|
||||
.enumerate()
|
||||
{
|
||||
if ch != BRAILLE_BLANK {
|
||||
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)
|
||||
.set_fg(color)
|
||||
.set_bg(self.background_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally draw the labels
|
||||
let style = Style::default().bg(self.background_color);
|
||||
for label in ctx.labels.iter().filter(|l| {
|
||||
!(l.x < self.x_bounds[0]
|
||||
|| l.x > self.x_bounds[1]
|
||||
|| l.y < self.y_bounds[0]
|
||||
|| l.y > self.y_bounds[1])
|
||||
}) {
|
||||
let dy = ((self.y_bounds[1] - label.y) * f64::from(canvas_area.height - 1)
|
||||
/ (self.y_bounds[1] - self.y_bounds[0])) as u16;
|
||||
let dx = ((label.x - self.x_bounds[0]) * f64::from(canvas_area.width - 1)
|
||||
/ (self.x_bounds[1] - self.x_bounds[0])) as u16;
|
||||
buf.set_string(
|
||||
dx + canvas_area.left(),
|
||||
dy + canvas_area.top(),
|
||||
label.text,
|
||||
style.fg(label.color),
|
||||
);
|
||||
}
|
||||
// Finally draw the labels
|
||||
let style = Style::default().bg(self.background_color);
|
||||
let left = self.x_bounds[0];
|
||||
let right = self.x_bounds[1];
|
||||
let top = self.y_bounds[1];
|
||||
let bottom = self.y_bounds[0];
|
||||
let width = (self.x_bounds[1] - self.x_bounds[0]).abs();
|
||||
let height = (self.y_bounds[1] - self.y_bounds[0]).abs();
|
||||
let resolution = {
|
||||
let width = f64::from(canvas_area.width - 1);
|
||||
let height = f64::from(canvas_area.height - 1);
|
||||
(width, height)
|
||||
};
|
||||
for label in ctx
|
||||
.labels
|
||||
.iter()
|
||||
.filter(|l| l.x >= left && l.x <= right && l.y <= top && l.y >= bottom)
|
||||
{
|
||||
let x = ((label.x - left) * resolution.0 / width) as u16 + canvas_area.left();
|
||||
let y = ((top - label.y) * resolution.1 / height) as u16 + canvas_area.top();
|
||||
buf.set_stringn(
|
||||
x,
|
||||
y,
|
||||
label.text,
|
||||
(canvas_area.right() - x) as usize,
|
||||
style.fg(label.color),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
use std::slice;
|
||||
|
||||
use super::Shape;
|
||||
use crate::style::Color;
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{Painter, Shape},
|
||||
};
|
||||
|
||||
/// A shape to draw a group of points with the given color
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Points<'a> {
|
||||
pub coords: &'a [(f64, f64)],
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl<'a> Shape<'a> for Points<'a> {
|
||||
fn color(&self) -> Color {
|
||||
self.color
|
||||
}
|
||||
fn points(&'a self) -> Box<dyn Iterator<Item = (f64, f64)> + 'a> {
|
||||
Box::new(self.into_iter())
|
||||
impl<'a> Shape for Points<'a> {
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
for (x, y) in self.coords {
|
||||
if let Some((x, y)) = painter.get_point(*x, *y) {
|
||||
painter.paint(x, y, self.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,33 +28,3 @@ impl<'a> Default for Points<'a> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Points<'a> {
|
||||
type Item = (f64, f64);
|
||||
type IntoIter = PointsIterator<'a>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
PointsIterator {
|
||||
iter: self.coords.iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PointsIterator<'a> {
|
||||
iter: slice::Iter<'a, (f64, f64)>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [(f64, f64)]> for PointsIterator<'a> {
|
||||
fn from(data: &'a [(f64, f64)]) -> PointsIterator<'a> {
|
||||
PointsIterator { iter: data.iter() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for PointsIterator<'a> {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.iter.next() {
|
||||
Some(p) => Some(*p),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,54 +1,52 @@
|
|||
use crate::layout::Rect;
|
||||
use crate::style::Color;
|
||||
use crate::widgets::canvas::{Line, Shape};
|
||||
use itertools::Itertools;
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{Line, Painter, Shape},
|
||||
};
|
||||
|
||||
/// Shape to draw a rectangle from a `Rect` with the given color
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Rectangle {
|
||||
pub rect: Rect,
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub width: f64,
|
||||
pub height: f64,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl<'a> Shape<'a> for Rectangle {
|
||||
fn color(&self) -> Color {
|
||||
self.color
|
||||
}
|
||||
|
||||
fn points(&'a self) -> Box<dyn Iterator<Item = (f64, f64)> + 'a> {
|
||||
let left_line = Line {
|
||||
x1: f64::from(self.rect.x),
|
||||
y1: f64::from(self.rect.y),
|
||||
x2: f64::from(self.rect.x),
|
||||
y2: f64::from(self.rect.y + self.rect.height),
|
||||
color: self.color,
|
||||
};
|
||||
let top_line = Line {
|
||||
x1: f64::from(self.rect.x),
|
||||
y1: f64::from(self.rect.y + self.rect.height),
|
||||
x2: f64::from(self.rect.x + self.rect.width),
|
||||
y2: f64::from(self.rect.y + self.rect.height),
|
||||
color: self.color,
|
||||
};
|
||||
let right_line = Line {
|
||||
x1: f64::from(self.rect.x + self.rect.width),
|
||||
y1: f64::from(self.rect.y),
|
||||
x2: f64::from(self.rect.x + self.rect.width),
|
||||
y2: f64::from(self.rect.y + self.rect.height),
|
||||
color: self.color,
|
||||
};
|
||||
let bottom_line = Line {
|
||||
x1: f64::from(self.rect.x),
|
||||
y1: f64::from(self.rect.y),
|
||||
x2: f64::from(self.rect.x + self.rect.width),
|
||||
y2: f64::from(self.rect.y),
|
||||
color: self.color,
|
||||
};
|
||||
Box::new(
|
||||
left_line.into_iter().merge(
|
||||
top_line
|
||||
.into_iter()
|
||||
.merge(right_line.into_iter().merge(bottom_line.into_iter())),
|
||||
),
|
||||
)
|
||||
impl Shape for Rectangle {
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
let lines: [Line; 4] = [
|
||||
Line {
|
||||
x1: self.x,
|
||||
y1: self.y,
|
||||
x2: self.x,
|
||||
y2: self.y + self.height,
|
||||
color: self.color,
|
||||
},
|
||||
Line {
|
||||
x1: self.x,
|
||||
y1: self.y + self.height,
|
||||
x2: self.x + self.width,
|
||||
y2: self.y + self.height,
|
||||
color: self.color,
|
||||
},
|
||||
Line {
|
||||
x1: self.x + self.width,
|
||||
y1: self.y,
|
||||
x2: self.x + self.width,
|
||||
y2: self.y + self.height,
|
||||
color: self.color,
|
||||
},
|
||||
Line {
|
||||
x1: self.x,
|
||||
y1: self.y,
|
||||
x2: self.x + self.width,
|
||||
y2: self.y,
|
||||
color: self.color,
|
||||
},
|
||||
];
|
||||
for line in &lines {
|
||||
line.draw(painter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Rect},
|
||||
style::Style,
|
||||
symbols,
|
||||
widgets::{
|
||||
canvas::{Canvas, Line, Points},
|
||||
Block, Borders, Widget,
|
||||
},
|
||||
};
|
||||
use std::{borrow::Cow, cmp::max};
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::{Constraint, Rect};
|
||||
use crate::style::Style;
|
||||
use crate::symbols;
|
||||
use crate::widgets::canvas::{Canvas, Line, Points};
|
||||
use crate::widgets::{Block, Borders, Widget};
|
||||
|
||||
/// An X or Y axis for the chart widget
|
||||
pub struct Axis<'a, L>
|
||||
where
|
||||
|
|
Loading…
Add table
Reference in a new issue