From a515b06bcb556c1be2d0fc3095cd778d413fe40d Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Fri, 31 Mar 2023 22:57:37 +0100 Subject: Vendor ratatui temporarily (#835) * Vendor ratatui temporarily Once https://github.com/tui-rs-revival/ratatui/pull/114 has been merged, we can undo this! But otherwise we can't publish to crates.io with a git dependency. * make tests pass * Shush. * these literally just fail in nix, nowhere else idk how to work with nix properly, and they're also not our tests --- src/ratatui/widgets/canvas/mod.rs | 510 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 510 insertions(+) create mode 100644 src/ratatui/widgets/canvas/mod.rs (limited to 'src/ratatui/widgets/canvas/mod.rs') diff --git a/src/ratatui/widgets/canvas/mod.rs b/src/ratatui/widgets/canvas/mod.rs new file mode 100644 index 00000000..7b174596 --- /dev/null +++ b/src/ratatui/widgets/canvas/mod.rs @@ -0,0 +1,510 @@ +mod line; +mod map; +mod points; +mod rectangle; +mod world; + +pub use self::line::Line; +pub use self::map::{Map, MapResolution}; +pub use self::points::Points; +pub use self::rectangle::Rectangle; + +use crate::ratatui::{ + buffer::Buffer, + layout::Rect, + style::{Color, Style}, + symbols, + text::Spans, + widgets::{Block, Widget}, +}; +use std::fmt::Debug; + +/// Interface for all shapes that may be drawn on a Canvas widget. +pub trait Shape { + fn draw(&self, painter: &mut Painter); +} + +/// Label to draw some text on the canvas +#[derive(Debug, Clone)] +pub struct Label<'a> { + x: f64, + y: f64, + spans: Spans<'a>, +} + +#[derive(Debug, Clone)] +struct Layer { + string: String, + 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 BrailleGrid { + width: u16, + height: u16, + cells: Vec, + colors: Vec, +} + +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 { + string: String::from_utf16(&self.cells).unwrap(), + colors: self.colors.clone(), + } + } + + fn reset(&mut self) { + for c in &mut self.cells { + *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 CharGrid { + width: u16, + height: u16, + cells: Vec, + colors: Vec, + cell_char: char, +} + +impl CharGrid { + fn new(width: u16, height: u16, cell_char: char) -> CharGrid { + let length = usize::from(width * height); + CharGrid { + width, + height, + cells: vec![' '; length], + colors: vec![Color::Reset; length], + cell_char, + } + } +} + +impl Grid for CharGrid { + 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 = self.cell_char; + } + 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, f64), +} + +impl<'a, 'b> Painter<'a, 'b> { + /// Convert the (x, y) coordinates to location of a point on the grid + /// + /// # Examples: + /// ``` + /// use ratatui::{symbols, widgets::canvas::{Painter, Context}}; + /// + /// 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))); + /// 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(); + if width == 0.0 || height == 0.0 { + return None; + } + 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 point of the grid + /// + /// # Examples: + /// ``` + /// use ratatui::{style::Color, symbols, widgets::canvas::{Painter, Context}}; + /// + /// 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) { + 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 { + context, + resolution, + } + } +} + +/// Holds the state of the Canvas when painting to it. +#[derive(Debug)] +pub struct Context<'a> { + x_bounds: [f64; 2], + y_bounds: [f64; 2], + 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], + marker: symbols::Marker, + ) -> Context<'a> { + let grid: Box = match marker { + symbols::Marker::Dot => Box::new(CharGrid::new(width, height, '•')), + symbols::Marker::Block => Box::new(CharGrid::new(width, height, '▄')), + symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)), + }; + Context { + x_bounds, + y_bounds, + grid, + dirty: false, + layers: Vec::new(), + labels: Vec::new(), + } + } + + /// Draw any object that may implement the Shape trait + pub fn draw(&mut self, shape: &S) + where + S: Shape, + { + self.dirty = true; + let mut painter = Painter::from(self); + shape.draw(&mut painter); + } + + /// Go one layer above in the canvas. + pub fn layer(&mut self) { + self.layers.push(self.grid.save()); + self.grid.reset(); + self.dirty = false; + } + + /// Print a string on the canvas at the given position + pub fn print(&mut self, x: f64, y: f64, spans: T) + where + T: Into>, + { + self.labels.push(Label { + x, + y, + spans: spans.into(), + }); + } + + /// Push the last layer if necessary + fn finish(&mut self) { + if self.dirty { + self.layer() + } + } +} + +/// The Canvas widget may be used to draw more detailed figures using braille patterns (each +/// cell can have a braille character in 8 different positions). +/// # Examples +/// +/// ``` +/// # use ratatui::widgets::{Block, Borders}; +/// # use ratatui::layout::Rect; +/// # use ratatui::widgets::canvas::{Canvas, Shape, Line, Rectangle, Map, MapResolution}; +/// # use ratatui::style::Color; +/// Canvas::default() +/// .block(Block::default().title("Canvas").borders(Borders::ALL)) +/// .x_bounds([-180.0, 180.0]) +/// .y_bounds([-90.0, 90.0]) +/// .paint(|ctx| { +/// ctx.draw(&Map { +/// resolution: MapResolution::High, +/// color: Color::White +/// }); +/// ctx.layer(); +/// ctx.draw(&Line { +/// x1: 0.0, +/// y1: 10.0, +/// x2: 10.0, +/// y2: 10.0, +/// color: Color::White, +/// }); +/// ctx.draw(&Rectangle { +/// x: 10.0, +/// y: 20.0, +/// width: 10.0, +/// height: 10.0, +/// color: Color::Red +/// }); +/// }); +/// ``` +pub struct Canvas<'a, F> +where + F: Fn(&mut Context), +{ + block: Option>, + x_bounds: [f64; 2], + y_bounds: [f64; 2], + painter: Option, + background_color: Color, + marker: symbols::Marker, +} + +impl<'a, F> Default for Canvas<'a, F> +where + F: Fn(&mut Context), +{ + fn default() -> Canvas<'a, F> { + Canvas { + block: None, + x_bounds: [0.0, 0.0], + y_bounds: [0.0, 0.0], + painter: None, + background_color: Color::Reset, + marker: symbols::Marker::Braille, + } + } +} + +impl<'a, F> Canvas<'a, F> +where + F: Fn(&mut Context), +{ + pub fn block(mut self, block: Block<'a>) -> Canvas<'a, F> { + self.block = Some(block); + self + } + + /// Define the viewport of the canvas. + /// If you were to "zoom" to a certain part of the world you may want to choose different + /// bounds. + pub fn x_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> { + self.x_bounds = bounds; + self + } + + /// Define the viewport of the canvas. + /// + /// If you were to "zoom" to a certain part of the world you may want to choose different + /// bounds. + pub fn y_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> { + self.y_bounds = bounds; + self + } + + /// Store the closure that will be used to draw to the Canvas + pub fn paint(mut self, f: F) -> Canvas<'a, F> { + self.painter = Some(f); + self + } + + pub fn background_color(mut self, color: Color) -> Canvas<'a, F> { + 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 or + /// block instead if the targeted terminal does not support those symbols. + /// + /// # Examples + /// + /// ``` + /// # use ratatui::widgets::canvas::Canvas; + /// # use ratatui::symbols; + /// Canvas::default().marker(symbols::Marker::Braille).paint(|ctx| {}); + /// + /// Canvas::default().marker(symbols::Marker::Dot).paint(|ctx| {}); + /// + /// Canvas::default().marker(symbols::Marker::Block).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> +where + F: Fn(&mut Context), +{ + fn render(mut self, area: Rect, buf: &mut Buffer) { + let canvas_area = match self.block.take() { + Some(b) => { + let inner_area = b.inner(area); + b.render(area, buf); + inner_area + } + None => area, + }; + + buf.set_style(canvas_area, Style::default().bg(self.background_color)); + + let width = canvas_area.width as usize; + + let painter = match self.painter { + Some(ref p) => p, + None => return, + }; + + // 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, + self.marker, + ); + // Paint to this context + painter(&mut ctx); + ctx.finish(); + + // Retrieve 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 != ' ' && 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) + .set_fg(color); + } + } + } + + // Finally draw the labels + 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_spans(x, y, &label.spans, canvas_area.right() - x); + } + } +} -- cgit v1.3.1