Skip to main content

altui_core/widgets/canvas/
mod.rs

1mod line;
2mod map;
3mod points;
4mod rectangle;
5mod world;
6
7pub use self::line::Line;
8pub use self::map::{Map, MapResolution};
9pub use self::points::Points;
10pub use self::rectangle::Rectangle;
11
12use crate::{
13    buffer::Buffer,
14    layout::Rect,
15    style::{Color, Style},
16    symbols,
17    text::Spans,
18    widgets::{Block, Widget},
19};
20use std::fmt::Debug;
21
22/// Interface for all shapes that may be drawn on a Canvas widget.
23pub trait Shape {
24    fn draw(&self, painter: &mut Painter);
25}
26
27/// Label to draw some text on the canvas
28#[derive(Debug, Clone)]
29pub struct Label<'a> {
30    x: f64,
31    y: f64,
32    spans: Spans<'a>,
33}
34
35#[derive(Debug, Clone)]
36struct Layer {
37    string: String,
38    colors: Vec<Color>,
39}
40
41trait Grid: Debug {
42    // fn width(&self) -> u16;
43    // fn height(&self) -> u16;
44    fn resolution(&self) -> (f64, f64);
45    fn paint(&mut self, x: usize, y: usize, color: Color);
46    fn save(&self) -> Layer;
47    fn reset(&mut self);
48}
49
50#[derive(Debug, Clone)]
51struct BrailleGrid {
52    width: u16,
53    height: u16,
54    cells: Vec<u16>,
55    colors: Vec<Color>,
56}
57
58impl BrailleGrid {
59    fn new(width: u16, height: u16) -> BrailleGrid {
60        let length = usize::from(width * height);
61        BrailleGrid {
62            width,
63            height,
64            cells: vec![symbols::braille::BLANK; length],
65            colors: vec![Color::Reset; length],
66        }
67    }
68}
69
70impl Grid for BrailleGrid {
71    // fn width(&self) -> u16 {
72    //     self.width
73    // }
74
75    // fn height(&self) -> u16 {
76    //     self.height
77    // }
78
79    fn resolution(&self) -> (f64, f64) {
80        (
81            f64::from(self.width) * 2.0 - 1.0,
82            f64::from(self.height) * 4.0 - 1.0,
83        )
84    }
85
86    fn save(&self) -> Layer {
87        Layer {
88            string: String::from_utf16(&self.cells).unwrap(),
89            colors: self.colors.clone(),
90        }
91    }
92
93    fn reset(&mut self) {
94        for c in &mut self.cells {
95            *c = symbols::braille::BLANK;
96        }
97        for c in &mut self.colors {
98            *c = Color::Reset;
99        }
100    }
101
102    fn paint(&mut self, x: usize, y: usize, color: Color) {
103        let index = y / 4 * self.width as usize + x / 2;
104        if let Some(c) = self.cells.get_mut(index) {
105            *c |= symbols::braille::DOTS[y % 4][x % 2];
106        }
107        if let Some(c) = self.colors.get_mut(index) {
108            *c = color;
109        }
110    }
111}
112
113#[derive(Debug, Clone)]
114struct CharGrid {
115    width: u16,
116    height: u16,
117    cells: Vec<char>,
118    colors: Vec<Color>,
119    cell_char: char,
120}
121
122impl CharGrid {
123    fn new(width: u16, height: u16, cell_char: char) -> CharGrid {
124        let length = usize::from(width * height);
125        CharGrid {
126            width,
127            height,
128            cells: vec![' '; length],
129            colors: vec![Color::Reset; length],
130            cell_char,
131        }
132    }
133}
134
135impl Grid for CharGrid {
136    // fn width(&self) -> u16 {
137    //     self.width
138    // }
139
140    // fn height(&self) -> u16 {
141    //     self.height
142    // }
143
144    fn resolution(&self) -> (f64, f64) {
145        (f64::from(self.width) - 1.0, f64::from(self.height) - 1.0)
146    }
147
148    fn save(&self) -> Layer {
149        Layer {
150            string: self.cells.iter().collect(),
151            colors: self.colors.clone(),
152        }
153    }
154
155    fn reset(&mut self) {
156        for c in &mut self.cells {
157            *c = ' ';
158        }
159        for c in &mut self.colors {
160            *c = Color::Reset;
161        }
162    }
163
164    fn paint(&mut self, x: usize, y: usize, color: Color) {
165        let index = y * self.width as usize + x;
166        if let Some(c) = self.cells.get_mut(index) {
167            *c = self.cell_char;
168        }
169        if let Some(c) = self.colors.get_mut(index) {
170            *c = color;
171        }
172    }
173}
174
175#[derive(Debug)]
176pub struct Painter<'a, 'b> {
177    context: &'a mut Context<'b>,
178    resolution: (f64, f64),
179}
180
181impl<'a, 'b> Painter<'a, 'b> {
182    /// Convert the (x, y) coordinates to location of a point on the grid
183    ///
184    /// # Examples:
185    /// ```
186    /// use altui_core::{symbols, widgets::canvas::{Painter, Context}};
187    ///
188    /// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
189    /// let mut painter = Painter::from(&mut ctx);
190    /// let point = painter.get_point(1.0, 0.0);
191    /// assert_eq!(point, Some((0, 7)));
192    /// let point = painter.get_point(1.5, 1.0);
193    /// assert_eq!(point, Some((1, 3)));
194    /// let point = painter.get_point(0.0, 0.0);
195    /// assert_eq!(point, None);
196    /// let point = painter.get_point(2.0, 2.0);
197    /// assert_eq!(point, Some((3, 0)));
198    /// let point = painter.get_point(1.0, 2.0);
199    /// assert_eq!(point, Some((0, 0)));
200    /// ```
201    pub fn get_point(&self, x: f64, y: f64) -> Option<(usize, usize)> {
202        let left = self.context.x_bounds[0];
203        let right = self.context.x_bounds[1];
204        let top = self.context.y_bounds[1];
205        let bottom = self.context.y_bounds[0];
206        if x < left || x > right || y < bottom || y > top {
207            return None;
208        }
209        let width = (self.context.x_bounds[1] - self.context.x_bounds[0]).abs();
210        let height = (self.context.y_bounds[1] - self.context.y_bounds[0]).abs();
211        if width == 0.0 || height == 0.0 {
212            return None;
213        }
214        let x = ((x - left) * self.resolution.0 / width) as usize;
215        let y = ((top - y) * self.resolution.1 / height) as usize;
216        Some((x, y))
217    }
218
219    /// Paint a point of the grid
220    ///
221    /// # Examples:
222    /// ```
223    /// use altui_core::{style::Color, symbols, widgets::canvas::{Painter, Context}};
224    ///
225    /// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
226    /// let mut painter = Painter::from(&mut ctx);
227    /// let cell = painter.paint(1, 3, Color::Red);
228    /// ```
229    pub fn paint(&mut self, x: usize, y: usize, color: Color) {
230        self.context.grid.paint(x, y, color);
231    }
232}
233
234impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
235    fn from(context: &'a mut Context<'b>) -> Painter<'a, 'b> {
236        let resolution = context.grid.resolution();
237        Painter {
238            context,
239            resolution,
240        }
241    }
242}
243
244/// Holds the state of the Canvas when painting to it.
245#[derive(Debug)]
246pub struct Context<'a> {
247    x_bounds: [f64; 2],
248    y_bounds: [f64; 2],
249    grid: Box<dyn Grid>,
250    dirty: bool,
251    layers: Vec<Layer>,
252    labels: Vec<Label<'a>>,
253}
254
255impl<'a> Context<'a> {
256    pub fn new(
257        width: u16,
258        height: u16,
259        x_bounds: [f64; 2],
260        y_bounds: [f64; 2],
261        marker: symbols::Marker,
262    ) -> Context<'a> {
263        let grid: Box<dyn Grid> = match marker {
264            symbols::Marker::Dot => Box::new(CharGrid::new(width, height, '•')),
265            symbols::Marker::Block => Box::new(CharGrid::new(width, height, '▄')),
266            symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)),
267        };
268        Context {
269            x_bounds,
270            y_bounds,
271            grid,
272            dirty: false,
273            layers: Vec::new(),
274            labels: Vec::new(),
275        }
276    }
277
278    /// Draw any object that may implement the Shape trait
279    pub fn draw<S>(&mut self, shape: &S)
280    where
281        S: Shape,
282    {
283        self.dirty = true;
284        let mut painter = Painter::from(self);
285        shape.draw(&mut painter);
286    }
287
288    /// Go one layer above in the canvas.
289    pub fn layer(&mut self) {
290        self.layers.push(self.grid.save());
291        self.grid.reset();
292        self.dirty = false;
293    }
294
295    /// Print a string on the canvas at the given position
296    pub fn print<T>(&mut self, x: f64, y: f64, spans: T)
297    where
298        T: Into<Spans<'a>>,
299    {
300        self.labels.push(Label {
301            x,
302            y,
303            spans: spans.into(),
304        });
305    }
306
307    /// Push the last layer if necessary
308    fn finish(&mut self) {
309        if self.dirty {
310            self.layer()
311        }
312    }
313}
314
315/// The Canvas widget may be used to draw more detailed figures using braille patterns (each
316/// cell can have a braille character in 8 different positions).
317/// # Examples
318///
319/// ```
320/// # use altui_core::widgets::{Block, Borders};
321/// # use altui_core::layout::Rect;
322/// # use altui_core::widgets::canvas::{Canvas, Shape, Line, Rectangle, Map, MapResolution};
323/// # use altui_core::style::Color;
324/// Canvas::default()
325///     .block(Block::default().title("Canvas").borders(Borders::ALL))
326///     .x_bounds([-180.0, 180.0])
327///     .y_bounds([-90.0, 90.0])
328///     .paint(|ctx| {
329///         ctx.draw(&Map {
330///             resolution: MapResolution::High,
331///             color: Color::White
332///         });
333///         ctx.layer();
334///         ctx.draw(&Line {
335///             x1: 0.0,
336///             y1: 10.0,
337///             x2: 10.0,
338///             y2: 10.0,
339///             color: Color::White,
340///         });
341///         ctx.draw(&Rectangle {
342///             x: 10.0,
343///             y: 20.0,
344///             width: 10.0,
345///             height: 10.0,
346///             color: Color::Red
347///         });
348///     });
349/// ```
350pub struct Canvas<'a, F>
351where
352    F: Fn(&mut Context),
353{
354    block: Option<Block<'a>>,
355    x_bounds: [f64; 2],
356    y_bounds: [f64; 2],
357    painter: Option<F>,
358    background_color: Color,
359    marker: symbols::Marker,
360}
361
362impl<'a, F> Default for Canvas<'a, F>
363where
364    F: Fn(&mut Context),
365{
366    fn default() -> Canvas<'a, F> {
367        Canvas {
368            block: None,
369            x_bounds: [0.0, 0.0],
370            y_bounds: [0.0, 0.0],
371            painter: None,
372            background_color: Color::Reset,
373            marker: symbols::Marker::Braille,
374        }
375    }
376}
377
378impl<'a, F> Canvas<'a, F>
379where
380    F: Fn(&mut Context),
381{
382    pub fn block(mut self, block: Block<'a>) -> Canvas<'a, F> {
383        self.block = Some(block);
384        self
385    }
386
387    pub fn x_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
388        self.x_bounds = bounds;
389        self
390    }
391
392    pub fn y_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
393        self.y_bounds = bounds;
394        self
395    }
396
397    /// Store the closure that will be used to draw to the Canvas
398    pub fn paint(mut self, f: F) -> Canvas<'a, F> {
399        self.painter = Some(f);
400        self
401    }
402
403    pub fn background_color(mut self, color: Color) -> Canvas<'a, F> {
404        self.background_color = color;
405        self
406    }
407
408    /// Change the type of points used to draw the shapes. By default the braille patterns are used
409    /// as they provide a more fine grained result but you might want to use the simple dot or
410    /// block instead if the targeted terminal does not support those symbols.
411    ///
412    /// # Examples
413    ///
414    /// ```
415    /// # use altui_core::widgets::canvas::Canvas;
416    /// # use altui_core::symbols;
417    /// Canvas::default().marker(symbols::Marker::Braille).paint(|ctx| {});
418    ///
419    /// Canvas::default().marker(symbols::Marker::Dot).paint(|ctx| {});
420    ///
421    /// Canvas::default().marker(symbols::Marker::Block).paint(|ctx| {});
422    /// ```
423    pub fn marker(mut self, marker: symbols::Marker) -> Canvas<'a, F> {
424        self.marker = marker;
425        self
426    }
427}
428
429impl<'a, F> Widget for Canvas<'a, F>
430where
431    F: Fn(&mut Context),
432{
433    fn render(&mut self, area: Rect, buf: &mut Buffer) {
434        let canvas_area = match self.block.as_mut() {
435            Some(b) => {
436                let inner_area = b.inner(area);
437                b.render(area, buf);
438                inner_area
439            }
440            None => area,
441        };
442
443        buf.set_style(canvas_area, Style::default().bg(self.background_color));
444
445        let width = canvas_area.width as usize;
446
447        let painter = match self.painter {
448            Some(ref p) => p,
449            None => return,
450        };
451
452        // Create a blank context that match the size of the canvas
453        let mut ctx = Context::new(
454            canvas_area.width,
455            canvas_area.height,
456            self.x_bounds,
457            self.y_bounds,
458            self.marker,
459        );
460        // Paint to this context
461        painter(&mut ctx);
462        ctx.finish();
463
464        // Retreive painted points for each layer
465        for layer in ctx.layers {
466            for (i, (ch, color)) in layer
467                .string
468                .chars()
469                .zip(layer.colors.into_iter())
470                .enumerate()
471            {
472                if ch != ' ' && ch != '\u{2800}' {
473                    let (x, y) = (i % width, i / width);
474                    buf.get_mut(x as u16 + canvas_area.left(), y as u16 + canvas_area.top())
475                        .set_char(ch)
476                        .set_fg(color);
477                }
478            }
479        }
480
481        // Finally draw the labels
482        let left = self.x_bounds[0];
483        let right = self.x_bounds[1];
484        let top = self.y_bounds[1];
485        let bottom = self.y_bounds[0];
486        let width = (self.x_bounds[1] - self.x_bounds[0]).abs();
487        let height = (self.y_bounds[1] - self.y_bounds[0]).abs();
488        let resolution = {
489            let width = f64::from(canvas_area.width - 1);
490            let height = f64::from(canvas_area.height - 1);
491            (width, height)
492        };
493        for label in ctx
494            .labels
495            .iter()
496            .filter(|l| l.x >= left && l.x <= right && l.y <= top && l.y >= bottom)
497        {
498            let x = ((label.x - left) * resolution.0 / width) as u16 + canvas_area.left();
499            let y = ((top - label.y) * resolution.1 / height) as u16 + canvas_area.top();
500            buf.set_spans(x, y, &label.spans, canvas_area.right() - x);
501        }
502    }
503}