mirror of
https://github.com/ratatui/ratatui.git
synced 2025-09-29 14:04:50 +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
@ -20,10 +20,10 @@ use crate::util::event::{Config, Event, Events};
|
|||||||
struct App {
|
struct App {
|
||||||
x: f64,
|
x: f64,
|
||||||
y: f64,
|
y: f64,
|
||||||
ball: Rect,
|
ball: Rectangle,
|
||||||
playground: Rect,
|
playground: Rect,
|
||||||
vx: u16,
|
vx: f64,
|
||||||
vy: u16,
|
vy: f64,
|
||||||
dir_x: bool,
|
dir_x: bool,
|
||||||
dir_y: bool,
|
dir_y: bool,
|
||||||
}
|
}
|
||||||
@ -33,21 +33,29 @@ impl App {
|
|||||||
App {
|
App {
|
||||||
x: 0.0,
|
x: 0.0,
|
||||||
y: 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),
|
playground: Rect::new(10, 10, 100, 100),
|
||||||
vx: 1,
|
vx: 1.0,
|
||||||
vy: 1,
|
vy: 1.0,
|
||||||
dir_x: true,
|
dir_x: true,
|
||||||
dir_y: true,
|
dir_y: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self) {
|
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;
|
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;
|
self.dir_y = !self.dir_y;
|
||||||
}
|
}
|
||||||
@ -77,7 +85,7 @@ fn main() -> Result<(), failure::Error> {
|
|||||||
|
|
||||||
// Setup event handlers
|
// Setup event handlers
|
||||||
let config = Config {
|
let config = Config {
|
||||||
tick_rate: Duration::from_millis(100),
|
tick_rate: Duration::from_millis(250),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let events = Events::with_config(config);
|
let events = Events::with_config(config);
|
||||||
@ -106,10 +114,7 @@ fn main() -> Result<(), failure::Error> {
|
|||||||
let canvas = Canvas::default()
|
let canvas = Canvas::default()
|
||||||
.block(Block::default().borders(Borders::ALL).title("Pong"))
|
.block(Block::default().borders(Borders::ALL).title("Pong"))
|
||||||
.paint(|ctx| {
|
.paint(|ctx| {
|
||||||
ctx.draw(&Rectangle {
|
ctx.draw(&app.ball);
|
||||||
rect: app.ball,
|
|
||||||
color: Color::Yellow,
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.x_bounds([10.0, 110.0])
|
.x_bounds([10.0, 110.0])
|
||||||
.y_bounds([10.0, 110.0]);
|
.y_bounds([10.0, 110.0]);
|
||||||
|
@ -1,19 +1,30 @@
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
use crate::util::{
|
||||||
|
event::{Event, Events},
|
||||||
|
SinSignal,
|
||||||
|
};
|
||||||
use std::io;
|
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;
|
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)];
|
||||||
use termion::input::MouseTerminal;
|
const DATA2: [(f64, f64); 7] = [
|
||||||
use termion::raw::IntoRawMode;
|
(0.0, 0.0),
|
||||||
use termion::screen::AlternateScreen;
|
(10.0, 1.0),
|
||||||
use tui::backend::TermionBackend;
|
(20.0, 0.5),
|
||||||
use tui::style::{Color, Modifier, Style};
|
(30.0, 1.5),
|
||||||
use tui::widgets::{Axis, Block, Borders, Chart, Dataset, Marker};
|
(40.0, 1.0),
|
||||||
use tui::Terminal;
|
(50.0, 2.5),
|
||||||
|
(60.0, 3.0),
|
||||||
use crate::util::event::{Event, Events};
|
];
|
||||||
use crate::util::SinSignal;
|
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
signal1: SinSignal,
|
signal1: SinSignal,
|
||||||
@ -69,6 +80,17 @@ fn main() -> Result<(), failure::Error> {
|
|||||||
loop {
|
loop {
|
||||||
terminal.draw(|mut f| {
|
terminal.draw(|mut f| {
|
||||||
let size = f.size();
|
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 = [
|
let x_labels = [
|
||||||
format!("{}", app.window[0]),
|
format!("{}", app.window[0]),
|
||||||
format!("{}", (app.window[0] + app.window[1]) / 2.0),
|
format!("{}", (app.window[0] + app.window[1]) / 2.0),
|
||||||
@ -89,7 +111,7 @@ fn main() -> Result<(), failure::Error> {
|
|||||||
let chart = Chart::default()
|
let chart = Chart::default()
|
||||||
.block(
|
.block(
|
||||||
Block::default()
|
Block::default()
|
||||||
.title("Chart")
|
.title("Chart 1")
|
||||||
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
|
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
|
||||||
.borders(Borders::ALL),
|
.borders(Borders::ALL),
|
||||||
)
|
)
|
||||||
@ -110,7 +132,71 @@ fn main() -> Result<(), failure::Error> {
|
|||||||
.labels(&["-20", "0", "20"]),
|
.labels(&["-20", "0", "20"]),
|
||||||
)
|
)
|
||||||
.datasets(&datasets);
|
.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()? {
|
match events.next()? {
|
||||||
|
@ -259,12 +259,10 @@ where
|
|||||||
});
|
});
|
||||||
ctx.layer();
|
ctx.layer();
|
||||||
ctx.draw(&Rectangle {
|
ctx.draw(&Rectangle {
|
||||||
rect: Rect {
|
x: 0.0,
|
||||||
x: 0,
|
y: 30.0,
|
||||||
y: 30,
|
width: 10.0,
|
||||||
width: 10,
|
height: 10.0,
|
||||||
height: 10,
|
|
||||||
},
|
|
||||||
color: Color::Yellow,
|
color: Color::Yellow,
|
||||||
});
|
});
|
||||||
for (i, s1) in app.servers.iter().enumerate() {
|
for (i, s1) in app.servers.iter().enumerate() {
|
||||||
|
@ -47,8 +47,8 @@ impl Constraint {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct Margin {
|
pub struct Margin {
|
||||||
vertical: u16,
|
pub vertical: u16,
|
||||||
horizontal: u16,
|
pub horizontal: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
@ -58,7 +58,6 @@ pub enum Alignment {
|
|||||||
Right,
|
Right,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: enforce constraints size once const generics has landed
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct Layout {
|
pub struct Layout {
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
use super::Shape;
|
use crate::{
|
||||||
use crate::style::Color;
|
style::Color,
|
||||||
|
widgets::canvas::{Painter, Shape},
|
||||||
|
};
|
||||||
|
|
||||||
/// Shape to draw a line from (x1, y1) to (x2, y2) with the given color
|
/// Shape to draw a line from (x1, y1) to (x2, y2) with the given color
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct Line {
|
pub struct Line {
|
||||||
pub x1: f64,
|
pub x1: f64,
|
||||||
pub y1: f64,
|
pub y1: f64,
|
||||||
@ -10,63 +13,77 @@ pub struct Line {
|
|||||||
pub color: Color,
|
pub color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LineIterator {
|
impl Shape for Line {
|
||||||
x: f64,
|
fn draw(&self, painter: &mut Painter) {
|
||||||
y: f64,
|
let (x1, y1) = match painter.get_point(self.x1, self.y1) {
|
||||||
dx: f64,
|
Some(c) => c,
|
||||||
dy: f64,
|
None => return,
|
||||||
dir_x: f64,
|
};
|
||||||
dir_y: f64,
|
let (x2, y2) = match painter.get_point(self.x2, self.y2) {
|
||||||
current: f64,
|
Some(c) => c,
|
||||||
end: f64,
|
None => return,
|
||||||
}
|
};
|
||||||
|
let (dx, x_range) = if x2 >= x1 {
|
||||||
impl Iterator for LineIterator {
|
(x2 - x1, x1..=x2)
|
||||||
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)
|
|
||||||
} else {
|
} 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 {
|
fn draw_line_low(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
|
||||||
type Item = (f64, f64);
|
let dx = (x2 - x1) as isize;
|
||||||
type IntoIter = LineIterator;
|
let dy = (y2 as isize - y1 as isize).abs();
|
||||||
|
let mut d = 2 * dy - dx;
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
let mut y = y1;
|
||||||
let dx = self.x1.max(self.x2) - self.x1.min(self.x2);
|
for x in x1..=x2 {
|
||||||
let dy = self.y1.max(self.y2) - self.y1.min(self.y2);
|
painter.paint(x, y, color);
|
||||||
let dir_x = if self.x1 <= self.x2 { 1.0 } else { -1.0 };
|
if d > 0 {
|
||||||
let dir_y = if self.y1 <= self.y2 { 1.0 } else { -1.0 };
|
y = if y1 > y2 { y - 1 } else { y + 1 };
|
||||||
let end = dx.max(dy);
|
d -= 2 * dx;
|
||||||
LineIterator {
|
|
||||||
x: self.x1,
|
|
||||||
y: self.y1,
|
|
||||||
dx,
|
|
||||||
dy,
|
|
||||||
dir_x,
|
|
||||||
dir_y,
|
|
||||||
current: 0.0,
|
|
||||||
end,
|
|
||||||
}
|
}
|
||||||
|
d += 2 * dy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Shape<'a> for Line {
|
fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
|
||||||
fn color(&self) -> Color {
|
let dx = (x2 as isize - x1 as isize).abs();
|
||||||
self.color
|
let dy = (y2 - y1) as isize;
|
||||||
}
|
let mut d = 2 * dx - dy;
|
||||||
|
let mut x = x1;
|
||||||
fn points(&'a self) -> Box<dyn Iterator<Item = (f64, f64)> + 'a> {
|
for y in y1..=y2 {
|
||||||
Box::new(self.into_iter())
|
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::{
|
||||||
use crate::widgets::canvas::points::PointsIterator;
|
style::Color,
|
||||||
use crate::widgets::canvas::world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION};
|
widgets::canvas::{
|
||||||
use crate::widgets::canvas::Shape;
|
world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION},
|
||||||
|
Painter, Shape,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum MapResolution {
|
pub enum MapResolution {
|
||||||
Low,
|
Low,
|
||||||
High,
|
High,
|
||||||
@ -19,6 +22,7 @@ impl MapResolution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Shape to draw a world map with the given resolution and color
|
/// Shape to draw a world map with the given resolution and color
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct Map {
|
pub struct Map {
|
||||||
pub resolution: MapResolution,
|
pub resolution: MapResolution,
|
||||||
pub color: Color,
|
pub color: Color,
|
||||||
@ -33,19 +37,12 @@ impl Default for Map {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Shape<'a> for Map {
|
impl Shape for Map {
|
||||||
fn color(&self) -> Color {
|
fn draw(&self, painter: &mut Painter) {
|
||||||
self.color
|
for (x, y) in self.resolution.data() {
|
||||||
}
|
if let Some((x, y)) = painter.get_point(*x, *y) {
|
||||||
fn points(&'a self) -> Box<dyn Iterator<Item = (f64, f64)> + 'a> {
|
painter.paint(x, y, self.color);
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,13 @@ pub use self::map::{Map, MapResolution};
|
|||||||
pub use self::points::Points;
|
pub use self::points::Points;
|
||||||
pub use self::rectangle::Rectangle;
|
pub use self::rectangle::Rectangle;
|
||||||
|
|
||||||
use crate::buffer::Buffer;
|
use crate::{
|
||||||
use crate::layout::Rect;
|
buffer::Buffer,
|
||||||
use crate::style::{Color, Style};
|
layout::Rect,
|
||||||
use crate::widgets::{Block, Widget};
|
style::{Color, Style},
|
||||||
|
widgets::{Block, Widget},
|
||||||
|
};
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
pub const DOTS: [[u16; 2]; 4] = [
|
pub const DOTS: [[u16; 2]; 4] = [
|
||||||
[0x0001, 0x0008],
|
[0x0001, 0x0008],
|
||||||
@ -24,14 +27,12 @@ pub const BRAILLE_OFFSET: u16 = 0x2800;
|
|||||||
pub const BRAILLE_BLANK: char = '⠀';
|
pub const BRAILLE_BLANK: char = '⠀';
|
||||||
|
|
||||||
/// Interface for all shapes that may be drawn on a Canvas widget.
|
/// Interface for all shapes that may be drawn on a Canvas widget.
|
||||||
pub trait Shape<'a> {
|
pub trait Shape {
|
||||||
/// Returns the color of the shape
|
fn draw(&self, painter: &mut Painter);
|
||||||
fn color(&self) -> Color;
|
|
||||||
/// Returns an iterator over all points of the shape
|
|
||||||
fn points(&'a self) -> Box<dyn Iterator<Item = (f64, f64)> + 'a>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Label to draw some text on the canvas
|
/// Label to draw some text on the canvas
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct Label<'a> {
|
pub struct Label<'a> {
|
||||||
pub x: f64,
|
pub x: f64,
|
||||||
pub y: f64,
|
pub y: f64,
|
||||||
@ -39,11 +40,13 @@ pub struct Label<'a> {
|
|||||||
pub color: Color,
|
pub color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
struct Layer {
|
struct Layer {
|
||||||
string: String,
|
string: String,
|
||||||
colors: Vec<Color>,
|
colors: Vec<Color>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
struct Grid {
|
struct Grid {
|
||||||
cells: Vec<u16>,
|
cells: Vec<u16>,
|
||||||
colors: Vec<Color>,
|
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.
|
/// Holds the state of the Canvas when painting to it.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct Context<'a> {
|
pub struct Context<'a> {
|
||||||
width: u16,
|
width: u16,
|
||||||
height: u16,
|
height: u16,
|
||||||
@ -87,26 +165,27 @@ pub struct Context<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> 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
|
/// 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
|
where
|
||||||
S: Shape<'b>,
|
S: Shape,
|
||||||
{
|
{
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
let left = self.x_bounds[0];
|
let mut painter = Painter::from(self);
|
||||||
let right = self.x_bounds[1];
|
shape.draw(&mut painter);
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Go one layer above in the canvas.
|
/// Go one layer above in the canvas.
|
||||||
@ -156,12 +235,10 @@ impl<'a> Context<'a> {
|
|||||||
/// color: Color::White,
|
/// color: Color::White,
|
||||||
/// });
|
/// });
|
||||||
/// ctx.draw(&Rectangle {
|
/// ctx.draw(&Rectangle {
|
||||||
/// rect: Rect {
|
/// x: 10.0,
|
||||||
/// x: 10,
|
/// y: 20.0,
|
||||||
/// y: 20,
|
/// width: 10.0,
|
||||||
/// width: 10,
|
/// height: 10.0,
|
||||||
/// height: 10,
|
|
||||||
/// },
|
|
||||||
/// color: Color::Red
|
/// color: Color::Red
|
||||||
/// });
|
/// });
|
||||||
/// });
|
/// });
|
||||||
@ -235,61 +312,68 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
let width = canvas_area.width as usize;
|
let width = canvas_area.width as usize;
|
||||||
let height = canvas_area.height as usize;
|
|
||||||
|
|
||||||
if let Some(ref painter) = self.painter {
|
let painter = match self.painter {
|
||||||
// Create a blank context that match the size of the terminal
|
Some(ref p) => p,
|
||||||
let mut ctx = Context {
|
None => return,
|
||||||
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();
|
|
||||||
|
|
||||||
// Retreive painted points for each layer
|
// Create a blank context that match the size of the canvas
|
||||||
for layer in ctx.layers {
|
let mut ctx = Context::new(
|
||||||
for (i, (ch, color)) in layer
|
canvas_area.width,
|
||||||
.string
|
canvas_area.height,
|
||||||
.chars()
|
self.x_bounds,
|
||||||
.zip(layer.colors.into_iter())
|
self.y_bounds,
|
||||||
.enumerate()
|
);
|
||||||
{
|
// Paint to this context
|
||||||
if ch != BRAILLE_BLANK {
|
painter(&mut ctx);
|
||||||
let (x, y) = (i % width, i / width);
|
ctx.finish();
|
||||||
buf.get_mut(x as u16 + canvas_area.left(), y as u16 + canvas_area.top())
|
|
||||||
.set_char(ch)
|
// Retreive painted points for each layer
|
||||||
.set_fg(color)
|
for layer in ctx.layers {
|
||||||
.set_bg(self.background_color);
|
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
|
// Finally draw the labels
|
||||||
let style = Style::default().bg(self.background_color);
|
let style = Style::default().bg(self.background_color);
|
||||||
for label in ctx.labels.iter().filter(|l| {
|
let left = self.x_bounds[0];
|
||||||
!(l.x < self.x_bounds[0]
|
let right = self.x_bounds[1];
|
||||||
|| l.x > self.x_bounds[1]
|
let top = self.y_bounds[1];
|
||||||
|| l.y < self.y_bounds[0]
|
let bottom = self.y_bounds[0];
|
||||||
|| l.y > self.y_bounds[1])
|
let width = (self.x_bounds[1] - self.x_bounds[0]).abs();
|
||||||
}) {
|
let height = (self.y_bounds[1] - self.y_bounds[0]).abs();
|
||||||
let dy = ((self.y_bounds[1] - label.y) * f64::from(canvas_area.height - 1)
|
let resolution = {
|
||||||
/ (self.y_bounds[1] - self.y_bounds[0])) as u16;
|
let width = f64::from(canvas_area.width - 1);
|
||||||
let dx = ((label.x - self.x_bounds[0]) * f64::from(canvas_area.width - 1)
|
let height = f64::from(canvas_area.height - 1);
|
||||||
/ (self.x_bounds[1] - self.x_bounds[0])) as u16;
|
(width, height)
|
||||||
buf.set_string(
|
};
|
||||||
dx + canvas_area.left(),
|
for label in ctx
|
||||||
dy + canvas_area.top(),
|
.labels
|
||||||
label.text,
|
.iter()
|
||||||
style.fg(label.color),
|
.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 crate::{
|
||||||
|
style::Color,
|
||||||
use super::Shape;
|
widgets::canvas::{Painter, Shape},
|
||||||
use crate::style::Color;
|
};
|
||||||
|
|
||||||
/// A shape to draw a group of points with the given color
|
/// A shape to draw a group of points with the given color
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct Points<'a> {
|
pub struct Points<'a> {
|
||||||
pub coords: &'a [(f64, f64)],
|
pub coords: &'a [(f64, f64)],
|
||||||
pub color: Color,
|
pub color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Shape<'a> for Points<'a> {
|
impl<'a> Shape for Points<'a> {
|
||||||
fn color(&self) -> Color {
|
fn draw(&self, painter: &mut Painter) {
|
||||||
self.color
|
for (x, y) in self.coords {
|
||||||
}
|
if let Some((x, y)) = painter.get_point(*x, *y) {
|
||||||
fn points(&'a self) -> Box<dyn Iterator<Item = (f64, f64)> + 'a> {
|
painter.paint(x, y, self.color);
|
||||||
Box::new(self.into_iter())
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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::{
|
||||||
use crate::style::Color;
|
style::Color,
|
||||||
use crate::widgets::canvas::{Line, Shape};
|
widgets::canvas::{Line, Painter, Shape},
|
||||||
use itertools::Itertools;
|
};
|
||||||
|
|
||||||
/// Shape to draw a rectangle from a `Rect` with the given color
|
/// Shape to draw a rectangle from a `Rect` with the given color
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct Rectangle {
|
pub struct Rectangle {
|
||||||
pub rect: Rect,
|
pub x: f64,
|
||||||
|
pub y: f64,
|
||||||
|
pub width: f64,
|
||||||
|
pub height: f64,
|
||||||
pub color: Color,
|
pub color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Shape<'a> for Rectangle {
|
impl Shape for Rectangle {
|
||||||
fn color(&self) -> Color {
|
fn draw(&self, painter: &mut Painter) {
|
||||||
self.color
|
let lines: [Line; 4] = [
|
||||||
}
|
Line {
|
||||||
|
x1: self.x,
|
||||||
fn points(&'a self) -> Box<dyn Iterator<Item = (f64, f64)> + 'a> {
|
y1: self.y,
|
||||||
let left_line = Line {
|
x2: self.x,
|
||||||
x1: f64::from(self.rect.x),
|
y2: self.y + self.height,
|
||||||
y1: f64::from(self.rect.y),
|
color: self.color,
|
||||||
x2: f64::from(self.rect.x),
|
},
|
||||||
y2: f64::from(self.rect.y + self.rect.height),
|
Line {
|
||||||
color: self.color,
|
x1: self.x,
|
||||||
};
|
y1: self.y + self.height,
|
||||||
let top_line = Line {
|
x2: self.x + self.width,
|
||||||
x1: f64::from(self.rect.x),
|
y2: self.y + self.height,
|
||||||
y1: f64::from(self.rect.y + self.rect.height),
|
color: self.color,
|
||||||
x2: f64::from(self.rect.x + self.rect.width),
|
},
|
||||||
y2: f64::from(self.rect.y + self.rect.height),
|
Line {
|
||||||
color: self.color,
|
x1: self.x + self.width,
|
||||||
};
|
y1: self.y,
|
||||||
let right_line = Line {
|
x2: self.x + self.width,
|
||||||
x1: f64::from(self.rect.x + self.rect.width),
|
y2: self.y + self.height,
|
||||||
y1: f64::from(self.rect.y),
|
color: self.color,
|
||||||
x2: f64::from(self.rect.x + self.rect.width),
|
},
|
||||||
y2: f64::from(self.rect.y + self.rect.height),
|
Line {
|
||||||
color: self.color,
|
x1: self.x,
|
||||||
};
|
y1: self.y,
|
||||||
let bottom_line = Line {
|
x2: self.x + self.width,
|
||||||
x1: f64::from(self.rect.x),
|
y2: self.y,
|
||||||
y1: f64::from(self.rect.y),
|
color: self.color,
|
||||||
x2: f64::from(self.rect.x + self.rect.width),
|
},
|
||||||
y2: f64::from(self.rect.y),
|
];
|
||||||
color: self.color,
|
for line in &lines {
|
||||||
};
|
line.draw(painter);
|
||||||
Box::new(
|
}
|
||||||
left_line.into_iter().merge(
|
|
||||||
top_line
|
|
||||||
.into_iter()
|
|
||||||
.merge(right_line.into_iter().merge(bottom_line.into_iter())),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 std::{borrow::Cow, cmp::max};
|
||||||
|
|
||||||
use unicode_width::UnicodeWidthStr;
|
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
|
/// An X or Y axis for the chart widget
|
||||||
pub struct Axis<'a, L>
|
pub struct Axis<'a, L>
|
||||||
where
|
where
|
||||||
|
Loading…
x
Reference in New Issue
Block a user