mirror of
https://github.com/ratatui-org/ratatui
synced 2025-02-16 22:18:51 +00:00
Redefine canvas widget and add shapes
* Give the canvas widget a proper modules and define a standard way to draw to it (Shape trait) * Add Points, Line, Rectangle and Map shapes
This commit is contained in:
parent
107d7297af
commit
654be037be
11 changed files with 6641 additions and 1272 deletions
|
@ -22,7 +22,8 @@ use log4rs::config::{Appender, Config, Root};
|
|||
|
||||
use tui::Terminal;
|
||||
use tui::widgets::{Widget, Block, List, Gauge, Sparkline, Text, border, Chart, Axis, Dataset,
|
||||
BarChart, Marker, Tabs, Map};
|
||||
BarChart, Marker, Tabs};
|
||||
use tui::widgets::canvas::{Canvas, Shape, Line, Map, MapResolution};
|
||||
use tui::layout::{Group, Direction, Size, Rect};
|
||||
use tui::style::Color;
|
||||
|
||||
|
@ -286,8 +287,18 @@ fn draw(t: &mut Terminal, app: &App) {
|
|||
draw_main(t, app, &chunks[1]);
|
||||
}
|
||||
1 => {
|
||||
Map::default()
|
||||
Canvas::default()
|
||||
.block(Block::default().title("World").borders(border::ALL))
|
||||
.layers(&[&[Map::default().resolution(MapResolution::High)],
|
||||
&[&Line {
|
||||
x1: 0.0,
|
||||
y1: 0.0,
|
||||
x2: 10.0,
|
||||
y2: 10.0,
|
||||
color: Color::Red,
|
||||
}]])
|
||||
.x_bounds([180.0, 0.0])
|
||||
.y_bounds([0.0, 90.0])
|
||||
.render(&chunks[1], t);
|
||||
}
|
||||
_ => {}
|
||||
|
|
|
@ -23,10 +23,11 @@ pub struct LineIterator {
|
|||
impl Iterator for LineIterator {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.current += 1.0;
|
||||
if self.current < self.end + 1.0 {
|
||||
Some((self.x + (self.current * self.dx) / self.end * self.dir_x,
|
||||
self.y + (self.current * self.dy) / self.end * self.dir_y))
|
||||
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 {
|
||||
None
|
||||
}
|
||||
|
|
57
src/widgets/canvas/map.rs
Normal file
57
src/widgets/canvas/map.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use widgets::canvas::Shape;
|
||||
use widgets::canvas::points::PointsIterator;
|
||||
use widgets::canvas::world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION};
|
||||
use style::Color;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum MapResolution {
|
||||
Low,
|
||||
High,
|
||||
}
|
||||
|
||||
impl MapResolution {
|
||||
fn data(&self) -> &'static [(f64, f64)] {
|
||||
match *self {
|
||||
MapResolution::Low => &WORLD_LOW_RESOLUTION,
|
||||
MapResolution::High => &WORLD_HIGH_RESOLUTION,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Map {
|
||||
resolution: MapResolution,
|
||||
color: Color,
|
||||
}
|
||||
|
||||
impl Default for Map {
|
||||
fn default() -> Map {
|
||||
Map {
|
||||
resolution: MapResolution::Low,
|
||||
color: Color::Reset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Shape<'a> for Map {
|
||||
fn color(&self) -> Color {
|
||||
self.color
|
||||
}
|
||||
fn points(&'a self) -> Box<Iterator<Item = (f64, f64)> + 'a> {
|
||||
Box::new(self.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
impl Map {
|
||||
pub fn resolution(&mut self, resolution: MapResolution) -> &mut Map {
|
||||
self.resolution = resolution;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -1,8 +1,13 @@
|
|||
mod points;
|
||||
mod line;
|
||||
mod rectangle;
|
||||
mod map;
|
||||
mod world;
|
||||
|
||||
pub use self::points::Points;
|
||||
pub use self::line::Line;
|
||||
pub use self::rectangle::Rectangle;
|
||||
pub use self::map::{Map, MapResolution};
|
||||
|
||||
use style::Color;
|
||||
use buffer::Buffer;
|
||||
|
@ -23,7 +28,7 @@ pub struct Canvas<'a> {
|
|||
block: Option<Block<'a>>,
|
||||
x_bounds: [f64; 2],
|
||||
y_bounds: [f64; 2],
|
||||
shapes: &'a [&'a Shape<'a>],
|
||||
layers: &'a [&'a [&'a Shape<'a>]],
|
||||
}
|
||||
|
||||
impl<'a> Default for Canvas<'a> {
|
||||
|
@ -32,7 +37,7 @@ impl<'a> Default for Canvas<'a> {
|
|||
block: None,
|
||||
x_bounds: [0.0, 0.0],
|
||||
y_bounds: [0.0, 0.0],
|
||||
shapes: &[],
|
||||
layers: &[],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,8 +55,8 @@ impl<'a> Canvas<'a> {
|
|||
self.y_bounds = bounds;
|
||||
self
|
||||
}
|
||||
pub fn shapes(&mut self, shapes: &'a [&'a Shape<'a>]) -> &mut Canvas<'a> {
|
||||
self.shapes = shapes;
|
||||
pub fn layers(&mut self, layers: &'a [&'a [&'a Shape<'a>]]) -> &mut Canvas<'a> {
|
||||
self.layers = layers;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -69,38 +74,43 @@ impl<'a> Widget for Canvas<'a> {
|
|||
let width = canvas_area.width as usize;
|
||||
let height = canvas_area.height as usize;
|
||||
|
||||
let mut grid: Vec<u16> = vec![BRAILLE_OFFSET; width * height + 1];
|
||||
let mut colors: Vec<Color> = vec![Color::Reset; width * height + 1];
|
||||
|
||||
let mut x_bounds = self.x_bounds;
|
||||
x_bounds.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let mut y_bounds = self.y_bounds;
|
||||
y_bounds.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
|
||||
for shape in self.shapes {
|
||||
for (x, y) in shape.points().filter(|&(x, y)| {
|
||||
!(x < x_bounds[0] || x > x_bounds[1] || y < y_bounds[0] || y > y_bounds[1])
|
||||
}) {
|
||||
let dy = ((self.y_bounds[1] - y) * canvas_area.height as f64 * 4.0 /
|
||||
(self.y_bounds[1] - self.y_bounds[0])) as usize;
|
||||
let dx = ((self.x_bounds[1] - x) * canvas_area.width as f64 * 2.0 /
|
||||
(self.x_bounds[1] - self.x_bounds[0])) as usize;
|
||||
let index = dy / 4 * width + dx / 2;
|
||||
grid[index] |= DOTS[dy % 4][dx % 2];
|
||||
colors[index] = shape.color();
|
||||
}
|
||||
}
|
||||
for layer in self.layers {
|
||||
let mut grid: Vec<u16> = vec![BRAILLE_OFFSET; width * height + 1];
|
||||
let mut colors: Vec<Color> = vec![Color::Reset; width * height + 1];
|
||||
|
||||
let string = String::from_utf16(&grid).unwrap();
|
||||
for (i, (ch, color)) in string.chars().zip(colors.into_iter()).enumerate() {
|
||||
if ch != BRAILLE_BLANK {
|
||||
let (x, y) = (i % width, i / width);
|
||||
buf.update_cell(x as u16 + canvas_area.left(), y as u16 + area.top(), |c| {
|
||||
c.symbol.clear();
|
||||
c.symbol.push(ch);
|
||||
c.fg = color;
|
||||
c.bg = Color::Reset;
|
||||
});
|
||||
for shape in layer.iter() {
|
||||
for (x, y) in shape.points().filter(|&(x, y)| {
|
||||
!(x < x_bounds[0] || x > x_bounds[1] || y < y_bounds[0] || y > y_bounds[1])
|
||||
}) {
|
||||
let dy = ((self.y_bounds[1] - y) * canvas_area.height as f64 * 4.0 /
|
||||
(self.y_bounds[1] -
|
||||
self.y_bounds[0])) as usize;
|
||||
let dx = ((self.x_bounds[1] - x) * canvas_area.width as f64 * 2.0 /
|
||||
(self.x_bounds[1] -
|
||||
self.x_bounds[0])) as usize;
|
||||
let index = dy / 4 * width + dx / 2;
|
||||
grid[index] |= DOTS[dy % 4][dx % 2];
|
||||
colors[index] = shape.color();
|
||||
}
|
||||
}
|
||||
|
||||
let string = String::from_utf16(&grid).unwrap();
|
||||
for (i, (ch, color)) in string.chars().zip(colors.into_iter()).enumerate() {
|
||||
if ch != BRAILLE_BLANK {
|
||||
let (x, y) = (i % width, i / width);
|
||||
buf.update_cell(x as u16 + canvas_area.left(), y as u16 + area.top(), |c| {
|
||||
c.symbol.clear();
|
||||
c.symbol.push(ch);
|
||||
c.fg = color;
|
||||
c.bg = Color::Reset;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::slice;
|
||||
|
||||
use super::Shape;
|
||||
use style::Color;
|
||||
|
||||
|
@ -28,27 +30,26 @@ impl<'a> IntoIterator for &'a Points<'a> {
|
|||
type Item = (f64, f64);
|
||||
type IntoIter = PointsIterator<'a>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
PointsIterator {
|
||||
coords: self.coords,
|
||||
index: 0,
|
||||
}
|
||||
PointsIterator { iter: self.coords.iter() }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PointsIterator<'a> {
|
||||
coords: &'a [(f64, f64)],
|
||||
index: usize,
|
||||
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> {
|
||||
let point = if self.index < self.coords.len() {
|
||||
Some(self.coords[self.index])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.index += 1;
|
||||
point
|
||||
match self.iter.next() {
|
||||
Some(p) => Some(*p),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
76
src/widgets/canvas/rectangle.rs
Normal file
76
src/widgets/canvas/rectangle.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
use super::Shape;
|
||||
use style::Color;
|
||||
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use tui::style::Color;
|
||||
/// use tui::widgets::canvas::{Shape, Rectangle};
|
||||
///
|
||||
/// let rectangle = Rectangle { x: 4.0, y: 4.0, width: 4.0, height: 4.0, color: Color::Red };
|
||||
/// let points = rectangle.points().collect::<Vec<(f64, f64)>>();
|
||||
/// assert_eq!(&points, &[(4.0, 4.0), (5.0, 4.0), (6.0, 4.0), (7.0, 4.0), (4.0, 5.0), (7.0, 5.0),
|
||||
/// (4.0, 6.0), (7.0, 6.0), (4.0, 7.0), (5.0, 7.0), (6.0, 7.0), (7.0, 7.0)]);
|
||||
/// ```
|
||||
pub struct Rectangle {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub width: f64,
|
||||
pub height: f64,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
pub struct RectangleIterator<'a> {
|
||||
rect: &'a Rectangle,
|
||||
x: f64,
|
||||
y: f64,
|
||||
right: f64,
|
||||
bottom: f64,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for RectangleIterator<'a> {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.y < self.bottom {
|
||||
let pos = (self.x, self.y);
|
||||
let dx = if self.y == self.rect.y || self.y == self.bottom - 1.0 {
|
||||
1.0
|
||||
} else {
|
||||
self.rect.width - 1.0
|
||||
};
|
||||
self.x += dx;
|
||||
if self.x >= self.right {
|
||||
self.x = self.rect.x;
|
||||
self.y += 1.0;
|
||||
}
|
||||
Some(pos)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Rectangle {
|
||||
type Item = (f64, f64);
|
||||
type IntoIter = RectangleIterator<'a>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
let right = self.x + self.width;
|
||||
let bottom = self.y + self.height;
|
||||
RectangleIterator {
|
||||
rect: self,
|
||||
x: self.x,
|
||||
y: self.y,
|
||||
right: right,
|
||||
bottom: bottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Shape<'a> for Rectangle {
|
||||
fn color(&self) -> Color {
|
||||
self.color
|
||||
}
|
||||
fn points(&'a self) -> Box<Iterator<Item = (f64, f64)> + 'a> {
|
||||
Box::new(self.into_iter())
|
||||
}
|
||||
}
|
6424
src/widgets/canvas/world.rs
Normal file
6424
src/widgets/canvas/world.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -2,7 +2,8 @@ use std::cmp::max;
|
|||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use widgets::{Widget, Block, Canvas, Points};
|
||||
use widgets::{Widget, Block};
|
||||
use widgets::canvas::{Canvas, Points};
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use style::Color;
|
||||
|
@ -270,11 +271,11 @@ impl<'a> Widget for Chart<'a> {
|
|||
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().enumerate() {
|
||||
for (i, label) in labels.iter().enumerate() {
|
||||
let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
|
||||
if dy < graph_area.bottom() {
|
||||
buf.set_string(x,
|
||||
graph_area.bottom() -
|
||||
i as u16 * (graph_area.height - 1) / (labels_len - 1),
|
||||
graph_area.bottom() - 1 - dy,
|
||||
label,
|
||||
self.y_axis.labels_color,
|
||||
self.bg);
|
||||
|
@ -325,10 +326,10 @@ impl<'a> Widget for Chart<'a> {
|
|||
Canvas::default()
|
||||
.x_bounds(self.x_axis.bounds)
|
||||
.y_bounds(self.y_axis.bounds)
|
||||
.shapes(&[&Points {
|
||||
coords: dataset.data,
|
||||
color: dataset.color,
|
||||
}])
|
||||
.layers(&[&[&Points {
|
||||
coords: dataset.data,
|
||||
color: dataset.color,
|
||||
}]])
|
||||
.buffer(&graph_area, buf);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,8 @@ use layout::Rect;
|
|||
/// use tui::widgets::{Widget, Gauge, Block, border};
|
||||
///
|
||||
/// fn main() {
|
||||
/// Gauge::new()
|
||||
/// .block(*Block::default().borders(border::ALL).title("Progress"))
|
||||
/// Gauge::default()
|
||||
/// .block(Block::default().borders(border::ALL).title("Progress"))
|
||||
/// .percent(20);
|
||||
/// }
|
||||
/// ```
|
||||
|
|
1209
src/widgets/map.rs
1209
src/widgets/map.rs
File diff suppressed because it is too large
Load diff
|
@ -6,8 +6,7 @@ mod sparkline;
|
|||
mod chart;
|
||||
mod barchart;
|
||||
mod tabs;
|
||||
mod canvas;
|
||||
mod map;
|
||||
pub mod canvas;
|
||||
|
||||
pub use self::block::Block;
|
||||
pub use self::text::Text;
|
||||
|
@ -17,8 +16,6 @@ pub use self::sparkline::Sparkline;
|
|||
pub use self::chart::{Chart, Axis, Dataset, Marker};
|
||||
pub use self::barchart::BarChart;
|
||||
pub use self::tabs::Tabs;
|
||||
pub use self::canvas::{Canvas, Shape, Line, Points};
|
||||
pub use self::map::Map;
|
||||
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
|
|
Loading…
Add table
Reference in a new issue