ratatui_widgets/canvas.rs
1//! A [`Canvas`] and a collection of [`Shape`]s.
2//!
3//! The [`Canvas`] is a blank space on which you can draw anything manually or use one of the
4//! predefined [`Shape`]s.
5//!
6//! The available shapes are:
7//!
8//! - [`Circle`]: A basic circle
9//! - [`Line`]: A line between two points
10//! - [`Map`]: A world map
11//! - [`Points`]: A scatter of points
12//! - [`Rectangle`]: A basic rectangle
13//!
14//! You can also implement your own custom [`Shape`]s.
15
16use alloc::boxed::Box;
17use alloc::vec;
18use alloc::vec::Vec;
19use core::fmt;
20use core::iter::zip;
21
22use itertools::Itertools;
23use ratatui_core::buffer::Buffer;
24use ratatui_core::layout::Rect;
25use ratatui_core::style::{Color, Style};
26use ratatui_core::symbols::braille::BRAILLE;
27use ratatui_core::symbols::pixel::{OCTANTS, QUADRANTS, SEXTANTS};
28use ratatui_core::symbols::{self, Marker};
29use ratatui_core::text::Line as TextLine;
30use ratatui_core::widgets::Widget;
31
32pub use self::circle::Circle;
33pub use self::line::{FilledLine, Line};
34pub use self::map::{Map, MapResolution};
35pub use self::points::Points;
36pub use self::rectangle::Rectangle;
37use crate::block::{Block, BlockExt};
38#[cfg(not(feature = "std"))]
39use crate::polyfills::F64Polyfills;
40
41mod circle;
42mod line;
43mod map;
44mod points;
45mod rectangle;
46mod world;
47
48/// Something that can be drawn on a [`Canvas`].
49///
50/// You may implement your own canvas custom widgets by implementing this trait.
51pub trait Shape {
52 /// Draws this [`Shape`] using the given [`Painter`].
53 ///
54 /// This is the only method required to implement a custom widget that can be drawn on a
55 /// [`Canvas`].
56 fn draw(&self, painter: &mut Painter);
57}
58
59/// Label to draw some text on the canvas
60#[derive(Debug, Default, Clone, PartialEq)]
61pub struct Label<'a> {
62 x: f64,
63 y: f64,
64 line: TextLine<'a>,
65}
66
67/// A single layer of the canvas.
68///
69/// This allows the canvas to be drawn in multiple layers. This is useful if you want to draw
70/// multiple shapes on the canvas in specific order.
71#[derive(Debug)]
72struct Layer {
73 contents: Vec<LayerCell>,
74}
75
76/// A cell within a layer.
77///
78/// If a [`Context`] contains multiple layers, then the symbol, foreground, and background colors
79/// for a character will be determined by the top-most layer that provides a value for that
80/// character. For example, a chart drawn with [`Marker::Block`] may provide the background color,
81/// and a later chart drawn with [`Marker::Braille`] may provide the symbol and foreground color.
82#[derive(Debug)]
83struct LayerCell {
84 symbol: Option<char>,
85 fg: Option<Color>,
86 bg: Option<Color>,
87}
88
89/// A grid of cells that can be painted on.
90///
91/// The grid represents a particular screen region measured in rows and columns. The underlying
92/// resolution of the grid might exceed the number of rows and columns. For example, a grid of
93/// Braille patterns will have a resolution of 2x4 dots per cell. This means that a grid of 10x10
94/// cells will have a resolution of 20x40 dots.
95trait Grid: fmt::Debug {
96 /// Get the resolution of the grid in number of dots.
97 ///
98 /// This doesn't have to be the same as the number of rows and columns of the grid. For example,
99 /// a grid of Braille patterns will have a resolution of 2x4 dots per cell. This means that a
100 /// grid of 10x10 cells will have a resolution of 20x40 dots.
101 fn resolution(&self) -> (f64, f64);
102 /// Paint a point of the grid.
103 ///
104 /// The point is expressed in number of dots starting at the origin of the grid in the top left
105 /// corner. Note that this is not the same as the `(x, y)` coordinates of the canvas.
106 fn paint(&mut self, x: usize, y: usize, color: Color);
107 /// Save the current state of the [`Grid`] as a layer to be rendered
108 fn save(&self) -> Layer;
109 /// Reset the grid to its initial state
110 fn reset(&mut self);
111}
112
113/// The pattern and color of a `PatternGrid` cell.
114#[derive(Copy, Clone, Debug, Default)]
115struct PatternCell {
116 /// The pattern of a grid character.
117 ///
118 /// The pattern is stored in the lower bits in a row-major order. For instance, for a 2x4
119 /// pattern marker, bits 0 to 7 of this field should represent the following pseudo-pixels:
120 ///
121 /// | 0 1 |
122 /// | 2 3 |
123 /// | 4 5 |
124 /// | 6 7 |
125 pattern: u8,
126 /// The color of a cell only supports foreground colors for now as there's no way to
127 /// individually set the background color of each pseudo-pixel in a pattern character.
128 color: Option<Color>,
129}
130
131/// The `PatternGrid` is a grid made up of cells each containing a `W`x`H` pattern character.
132///
133/// This makes it possible to draw shapes with a resolution of e.g. 2x4 (Braille or unicode octant)
134/// per cell.
135/// Font support for the relevant pattern character is required. If your terminal or font does not
136/// support the relevant unicode block, you will see unicode replacement characters (�) instead.
137///
138/// This grid type only supports a single foreground color for each `W`x`H` pattern character.
139/// There is no way to set the individual color of each pseudo-pixel.
140#[derive(Debug)]
141struct PatternGrid<const W: usize, const H: usize> {
142 /// Width of the grid in number of terminal columns
143 width: u16,
144 /// Height of the grid in number of terminal rows
145 height: u16,
146 /// Pattern and color of the cells.
147 cells: Vec<PatternCell>,
148 /// Lookup table mapping patterns to characters.
149 char_table: &'static [char],
150}
151
152impl<const W: usize, const H: usize> PatternGrid<W, H> {
153 /// Statically check that the dimension of the pattern is supported.
154 const _PATTERN_DIMENSION_CHECK: usize = u8::BITS as usize - W * H;
155
156 /// Create a new `PatternGrid` with the given width and height measured in terminal columns
157 /// and rows respectively.
158 fn new(width: u16, height: u16, char_table: &'static [char]) -> Self {
159 // Cause a static error if the pattern doesn't fit within 8 bits.
160 let _ = Self::_PATTERN_DIMENSION_CHECK;
161
162 let length = usize::from(width) * usize::from(height);
163 Self {
164 width,
165 height,
166 cells: vec![PatternCell::default(); length],
167 char_table,
168 }
169 }
170}
171
172impl<const W: usize, const H: usize> Grid for PatternGrid<W, H> {
173 fn resolution(&self) -> (f64, f64) {
174 (
175 f64::from(self.width) * W as f64,
176 f64::from(self.height) * H as f64,
177 )
178 }
179
180 fn save(&self) -> Layer {
181 let contents = self
182 .cells
183 .iter()
184 .map(|&cell| {
185 let symbol = match cell.pattern {
186 // Skip rendering blank patterns to allow layers underneath
187 // to show through.
188 0 => None,
189 idx => Some(self.char_table[idx as usize]),
190 };
191
192 LayerCell {
193 symbol,
194 fg: cell.color,
195 // Patterns only affect foreground.
196 bg: None,
197 }
198 })
199 .collect();
200
201 Layer { contents }
202 }
203
204 fn reset(&mut self) {
205 self.cells.fill_with(Default::default);
206 }
207
208 fn paint(&mut self, x: usize, y: usize, color: Color) {
209 let index = y
210 .saturating_div(H)
211 .saturating_mul(self.width as usize)
212 .saturating_add(x.saturating_div(W));
213 // using get_mut here because we are indexing the vector with usize values
214 // and we want to make sure we don't panic if the index is out of bounds
215 if let Some(cell) = self.cells.get_mut(index) {
216 cell.pattern |= 1u8 << ((x % W) + W * (y % H));
217 cell.color = Some(color);
218 }
219 }
220}
221
222/// The `CharGrid` is a grid made up of cells each containing a single character.
223///
224/// This makes it possible to draw shapes with a resolution of 1x1 dots per cell. This is useful
225/// when you want to draw shapes with a low resolution.
226#[derive(Debug)]
227struct CharGrid {
228 /// Width of the grid in number of terminal columns
229 width: u16,
230 /// Height of the grid in number of terminal rows
231 height: u16,
232 /// The color of each cell
233 cells: Vec<Option<Color>>,
234
235 /// The character to use for every cell - e.g. a block, dot, etc.
236 cell_char: char,
237
238 /// If true, apply the color to the background as well as the foreground. This is used for
239 /// [`Marker::Block`], so that it will overwrite any previous foreground character, but also
240 /// leave a background that can be overlaid with an additional foreground character.
241 apply_color_to_bg: bool,
242}
243
244impl CharGrid {
245 /// Create a new `CharGrid` with the given width and height measured in terminal columns and
246 /// rows respectively.
247 fn new(width: u16, height: u16, cell_char: char) -> Self {
248 let length = usize::from(width) * usize::from(height);
249 Self {
250 width,
251 height,
252 cells: vec![None; length],
253 cell_char,
254 apply_color_to_bg: false,
255 }
256 }
257
258 fn apply_color_to_bg(self) -> Self {
259 Self {
260 apply_color_to_bg: true,
261 ..self
262 }
263 }
264}
265
266impl Grid for CharGrid {
267 fn resolution(&self) -> (f64, f64) {
268 (f64::from(self.width), f64::from(self.height))
269 }
270
271 fn save(&self) -> Layer {
272 Layer {
273 contents: self
274 .cells
275 .iter()
276 .map(|&color| LayerCell {
277 symbol: color.map(|_| self.cell_char),
278 fg: color,
279 bg: color.filter(|_| self.apply_color_to_bg),
280 })
281 .collect(),
282 }
283 }
284
285 fn reset(&mut self) {
286 self.cells.fill(None);
287 }
288
289 fn paint(&mut self, x: usize, y: usize, color: Color) {
290 let index = y.saturating_mul(self.width as usize).saturating_add(x);
291 // using get_mut here because we are indexing the vector with usize values
292 // and we want to make sure we don't panic if the index is out of bounds
293 if let Some(c) = self.cells.get_mut(index) {
294 *c = Some(color);
295 }
296 }
297}
298
299/// The `HalfBlockGrid` is a grid made up of cells each containing a half block character.
300///
301/// In terminals, each character is usually twice as tall as it is wide. Unicode has a couple of
302/// vertical half block characters, the upper half block '▀' and lower half block '▄' which take up
303/// half the height of a normal character but the full width. Together with an empty space ' ' and a
304/// full block '█', we can effectively double the resolution of a single cell. In addition, because
305/// each character can have a foreground and background color, we can control the color of the upper
306/// and lower half of each cell. This allows us to draw shapes with a resolution of 1x2 "pixels" per
307/// cell.
308///
309/// This allows for more flexibility than the `PatternGrid` which only supports a single
310/// foreground color for each 2x4 dots cell, and the `CharGrid` which only supports a single
311/// character for each cell.
312#[derive(Debug)]
313struct HalfBlockGrid {
314 /// Width of the grid in number of terminal columns
315 width: u16,
316 /// Height of the grid in number of terminal rows
317 height: u16,
318 /// Represents a single color for each "pixel" arranged in column, row order
319 pixels: Vec<Vec<Option<Color>>>,
320}
321
322impl HalfBlockGrid {
323 /// Create a new `HalfBlockGrid` with the given width and height measured in terminal columns
324 /// and rows respectively.
325 fn new(width: u16, height: u16) -> Self {
326 Self {
327 width,
328 height,
329 pixels: vec![vec![None; width as usize]; (height as usize) * 2],
330 }
331 }
332}
333
334impl Grid for HalfBlockGrid {
335 fn resolution(&self) -> (f64, f64) {
336 (f64::from(self.width), f64::from(self.height) * 2.0)
337 }
338
339 fn save(&self) -> Layer {
340 // Given that we store the pixels in a grid, and that we want to use 2 pixels arranged
341 // vertically to form a single terminal cell, which can be either empty, upper half block,
342 // lower half block or full block, we need examine the pixels in vertical pairs to decide
343 // what character to print in each cell. So these are the 4 states we use to represent each
344 // cell:
345 //
346 // 1. upper: reset, lower: reset => ' ' fg: reset / bg: reset
347 // 2. upper: reset, lower: color => '▄' fg: lower color / bg: reset
348 // 3. upper: color, lower: reset => '▀' fg: upper color / bg: reset
349 // 4. upper: color, lower: color => '▀' fg: upper color / bg: lower color
350 //
351 // Note that because the foreground reset color (i.e. default foreground color) is usually
352 // not the same as the background reset color (i.e. default background color), we need to
353 // swap around the colors for that state (2 reset/color).
354 //
355 // When the upper and lower colors are the same, we could continue to use an upper half
356 // block, but we choose to use a full block instead. This allows us to write unit tests that
357 // treat the cell as a single character instead of two half block characters.
358
359 // first we join each adjacent row together to get an iterator that contains vertical pairs
360 // of pixels, with the lower row being the first element in the pair
361 let vertical_color_pairs = self
362 .pixels
363 .iter()
364 .tuples()
365 .flat_map(|(upper_row, lower_row)| zip(upper_row, lower_row));
366
367 // Then we determine the character to print for each pair, along with the color of the
368 // foreground and background.
369 let contents = vertical_color_pairs
370 .map(|(upper, lower)| {
371 let (symbol, fg, bg) = match (upper, lower) {
372 (None, None) => (None, None, None),
373 (None, Some(lower)) => (Some(symbols::half_block::LOWER), Some(*lower), None),
374 (Some(upper), None) => (Some(symbols::half_block::UPPER), Some(*upper), None),
375 (Some(upper), Some(lower)) if lower == upper => {
376 (Some(symbols::half_block::FULL), Some(*upper), Some(*lower))
377 }
378 (Some(upper), Some(lower)) => {
379 (Some(symbols::half_block::UPPER), Some(*upper), Some(*lower))
380 }
381 };
382 LayerCell { symbol, fg, bg }
383 })
384 .collect();
385
386 Layer { contents }
387 }
388
389 fn reset(&mut self) {
390 self.pixels.fill(vec![None; self.width as usize]);
391 }
392
393 fn paint(&mut self, x: usize, y: usize, color: Color) {
394 self.pixels[y][x] = Some(color);
395 }
396}
397
398/// Painter is an abstraction over the [`Context`] that allows to draw shapes on the grid.
399///
400/// It is used by the [`Shape`] trait to draw shapes on the grid. It can be useful to think of this
401/// as similar to the [`Buffer`] struct that is used to draw widgets on the terminal.
402#[derive(Debug)]
403pub struct Painter<'a, 'b> {
404 context: &'a mut Context<'b>,
405 resolution: (f64, f64),
406}
407
408impl Painter<'_, '_> {
409 /// Convert the `(x, y)` coordinates to location of a point on the grid
410 ///
411 /// `(x, y)` coordinates are expressed in the coordinate system of the canvas. The origin is in
412 /// the lower left corner of the canvas (unlike most other coordinates in `Ratatui` where the
413 /// origin is the upper left corner). The `x` and `y` bounds of the canvas define the specific
414 /// area of some coordinate system that will be drawn on the canvas. The resolution of the grid
415 /// is used to convert the `(x, y)` coordinates to the location of a point on the grid.
416 ///
417 /// The grid coordinates are expressed in the coordinate system of the grid. The origin is in
418 /// the top left corner of the grid. The x and y bounds of the grid are always `[0, width - 1]`
419 /// and `[0, height - 1]` respectively. The resolution of the grid is used to convert the
420 /// `(x, y)` coordinates to the location of a point on the grid.
421 ///
422 /// Points are rounded to the nearest grid cell (with points exactly in the center of a cell
423 /// rounding up).
424 ///
425 /// # Examples
426 ///
427 /// ```
428 /// use ratatui::symbols;
429 /// use ratatui::widgets::canvas::{Context, Painter};
430 ///
431 /// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
432 /// let mut painter = Painter::from(&mut ctx);
433 ///
434 /// let point = painter.get_point(1.0, 0.0);
435 /// assert_eq!(point, Some((0, 7)));
436 ///
437 /// let point = painter.get_point(1.5, 1.0);
438 /// assert_eq!(point, Some((2, 4)));
439 ///
440 /// let point = painter.get_point(0.0, 0.0);
441 /// assert_eq!(point, None);
442 ///
443 /// let point = painter.get_point(2.0, 2.0);
444 /// assert_eq!(point, Some((3, 0)));
445 ///
446 /// let point = painter.get_point(1.0, 2.0);
447 /// assert_eq!(point, Some((0, 0)));
448 /// ```
449 pub fn get_point(&self, x: f64, y: f64) -> Option<(usize, usize)> {
450 let [left, right] = self.context.x_bounds;
451 let [bottom, top] = self.context.y_bounds;
452 if x < left || x > right || y < bottom || y > top {
453 return None;
454 }
455 let width = right - left;
456 let height = top - bottom;
457 if width <= 0.0 || height <= 0.0 {
458 return None;
459 }
460 let x = ((x - left) * (self.resolution.0 - 1.0) / width).round() as usize;
461 let y = ((top - y) * (self.resolution.1 - 1.0) / height).round() as usize;
462 Some((x, y))
463 }
464
465 /// Paint a point of the grid
466 ///
467 /// # Example
468 ///
469 /// ```
470 /// use ratatui::style::Color;
471 /// use ratatui::symbols;
472 /// use ratatui::widgets::canvas::{Context, Painter};
473 ///
474 /// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
475 /// let mut painter = Painter::from(&mut ctx);
476 /// painter.paint(1, 3, Color::Red);
477 /// ```
478 pub fn paint(&mut self, x: usize, y: usize, color: Color) {
479 self.context.grid.paint(x, y, color);
480 }
481
482 /// Canvas context bounds by axis.
483 ///
484 /// # Example
485 ///
486 /// ```
487 /// use ratatui::style::Color;
488 /// use ratatui::symbols;
489 /// use ratatui::widgets::canvas::{Context, Painter};
490 ///
491 /// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
492 /// let mut painter = Painter::from(&mut ctx);
493 /// assert_eq!(painter.bounds(), (&[0.0, 2.0], &[0.0, 2.0]));
494 /// ```
495 pub const fn bounds(&self) -> (&[f64; 2], &[f64; 2]) {
496 (&self.context.x_bounds, &self.context.y_bounds)
497 }
498}
499
500impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
501 fn from(context: &'a mut Context<'b>) -> Self {
502 let resolution = context.grid.resolution();
503 Self {
504 context,
505 resolution,
506 }
507 }
508}
509
510/// Holds the state of the [`Canvas`] when painting to it.
511///
512/// This is used by the [`Canvas`] widget to draw shapes on the grid. It can be useful to think of
513/// this as similar to the `Frame` struct that is used to draw widgets on the terminal.
514#[derive(Debug)]
515pub struct Context<'a> {
516 // Width of the canvas in cells.
517 //
518 // This is NOT the resolution in dots/pixels as this varies by marker type.
519 width: u16,
520 // Height of the canvas in cells.
521 //
522 // This is NOT the resolution in dots/pixels as this varies by marker type.
523 height: u16,
524 // Canvas coordinate system width
525 x_bounds: [f64; 2],
526 // Canvas coordinate system height
527 y_bounds: [f64; 2],
528 grid: Box<dyn Grid>,
529 dirty: bool,
530 layers: Vec<Layer>,
531 labels: Vec<Label<'a>>,
532}
533
534impl<'a> Context<'a> {
535 /// Create a new Context with the given width and height measured in terminal columns and rows
536 /// respectively. The `x` and `y` bounds define the specific area of some coordinate system that
537 /// will be drawn on the canvas. The marker defines the type of points used to draw the shapes.
538 ///
539 /// Applications should not use this directly but rather use the [`Canvas`] widget. This will be
540 /// created by the [`Canvas::paint`] method and passed to the closure that is used to draw on
541 /// the canvas.
542 ///
543 /// The `x` and `y` bounds should be specified as left/right and bottom/top respectively. For
544 /// example, if you want to draw a map of the world, you might want to use the following bounds:
545 ///
546 /// ```
547 /// use ratatui::symbols;
548 /// use ratatui::widgets::canvas::Context;
549 ///
550 /// let ctx = Context::new(
551 /// 100,
552 /// 100,
553 /// [-180.0, 180.0],
554 /// [-90.0, 90.0],
555 /// symbols::Marker::Braille,
556 /// );
557 /// ```
558 pub fn new(
559 width: u16,
560 height: u16,
561 x_bounds: [f64; 2],
562 y_bounds: [f64; 2],
563 marker: Marker,
564 ) -> Self {
565 let grid = Self::marker_to_grid(width, height, marker);
566 Self {
567 width,
568 height,
569 x_bounds,
570 y_bounds,
571 grid,
572 dirty: false,
573 layers: Vec::new(),
574 labels: Vec::new(),
575 }
576 }
577
578 fn marker_to_grid(width: u16, height: u16, marker: Marker) -> Box<dyn Grid> {
579 let dot = symbols::DOT.chars().next().unwrap();
580 let block = symbols::block::FULL.chars().next().unwrap();
581 let bar = symbols::bar::HALF.chars().next().unwrap();
582 match marker {
583 Marker::Block => Box::new(CharGrid::new(width, height, block).apply_color_to_bg()),
584 Marker::Bar => Box::new(CharGrid::new(width, height, bar)),
585 Marker::Braille => Box::new(PatternGrid::<2, 4>::new(width, height, &BRAILLE)),
586 Marker::HalfBlock => Box::new(HalfBlockGrid::new(width, height)),
587 Marker::Quadrant => Box::new(PatternGrid::<2, 2>::new(width, height, &QUADRANTS)),
588 Marker::Sextant => Box::new(PatternGrid::<2, 3>::new(width, height, &SEXTANTS)),
589 Marker::Octant => Box::new(PatternGrid::<2, 4>::new(width, height, &OCTANTS)),
590 Marker::Custom(char) => Box::new(CharGrid::new(width, height, char)),
591 Marker::Dot | _ => Box::new(CharGrid::new(width, height, dot)),
592 }
593 }
594
595 /// Change the marker being used in this context.
596 ///
597 /// This will save the last layer if necessary and reset the grid to use the new marker.
598 pub fn marker(&mut self, marker: Marker) {
599 self.finish();
600 self.grid = Self::marker_to_grid(self.width, self.height, marker);
601 }
602
603 /// Draw the given [`Shape`] in this context
604 pub fn draw<S>(&mut self, shape: &S)
605 where
606 S: Shape,
607 {
608 self.dirty = true;
609 let mut painter = Painter::from(self);
610 shape.draw(&mut painter);
611 }
612
613 /// Save the existing state of the grid as a layer.
614 ///
615 /// Save the existing state as a layer to be rendered and reset the grid to its initial
616 /// state for the next layer.
617 ///
618 /// This allows the canvas to be drawn in multiple layers. This is useful if you want to
619 /// draw multiple shapes on the [`Canvas`] in specific order.
620 pub fn layer(&mut self) {
621 self.layers.push(self.grid.save());
622 self.grid.reset();
623 self.dirty = false;
624 }
625
626 /// Print a [`Text`] on the [`Canvas`] at the given position.
627 ///
628 /// Note that the text is always printed on top of the canvas and is **not** affected by the
629 /// layers.
630 ///
631 /// [`Text`]: ratatui_core::text::Text
632 pub fn print<T>(&mut self, x: f64, y: f64, line: T)
633 where
634 T: Into<TextLine<'a>>,
635 {
636 self.labels.push(Label {
637 x,
638 y,
639 line: line.into(),
640 });
641 }
642
643 /// Save the last layer if necessary
644 fn finish(&mut self) {
645 if self.dirty {
646 self.layer();
647 }
648 }
649}
650
651/// The Canvas widget provides a means to draw shapes (Lines, Rectangles, Circles, etc.) on a grid.
652///
653/// By default the grid is made of Braille patterns but you may change the marker to use a different
654/// set of symbols. If your terminal or font does not support this unicode block, you will see
655/// unicode replacement characters (�) instead of braille dots. The Braille patterns (as well the
656/// octant character patterns) provide a more fine grained result with a 2x4 resolution per
657/// character, but you might want to use a simple dot, block, or bar instead by calling the
658/// [`marker`] method if your target environment does not support those symbols.
659///
660/// See [Unicode Braille Patterns](https://en.wikipedia.org/wiki/Braille_Patterns) for more info.
661///
662/// The `Octant` marker is similar to the `Braille` marker but, instead of sparse dots, displays
663/// densely packed and regularly spaced pseudo-pixels, without visible bands between rows and
664/// columns. However, it uses characters that are not yet as widely supported as the Braille
665/// unicode block.
666///
667/// The `Quadrant` and `Sextant` markers are in turn akin to the `Octant` marker, but with a 2x2
668/// and 2x3 resolution, respectively.
669///
670/// The `HalfBlock` marker is useful when you want to draw shapes with a higher resolution than a
671/// `CharGrid` but lower than a `PatternGrid`. This grid type supports a foreground and background
672/// color for each terminal cell. This allows for more flexibility than the `PatternGrid` which
673/// only supports a single foreground color for each 2x4 dots cell.
674///
675/// The Canvas widget is used by calling the [`Canvas::paint`] method and passing a closure that
676/// will be used to draw on the canvas. The closure will be passed a [`Context`] object that can be
677/// used to draw shapes on the canvas.
678///
679/// The [`Context`] object provides a [`Context::draw`] method that can be used to draw shapes on
680/// the canvas. The [`Context::layer`] method can be used to save the current state of the canvas
681/// and start a new layer. This is useful if you want to draw multiple shapes on the canvas in
682/// specific order. The [`Context`] object also provides a [`Context::print`] method that can be
683/// used to print text on the canvas. Note that the text is always printed on top of the canvas and
684/// is not affected by the layers.
685///
686/// # Examples
687///
688/// ```
689/// use ratatui::style::Color;
690/// use ratatui::widgets::Block;
691/// use ratatui::widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle};
692///
693/// Canvas::default()
694/// .block(Block::bordered().title("Canvas"))
695/// .x_bounds([-180.0, 180.0])
696/// .y_bounds([-90.0, 90.0])
697/// .paint(|ctx| {
698/// ctx.draw(&Map {
699/// resolution: MapResolution::High,
700/// color: Color::White,
701/// });
702/// ctx.layer();
703/// ctx.draw(&Line {
704/// x1: 0.0,
705/// y1: 10.0,
706/// x2: 10.0,
707/// y2: 10.0,
708/// color: Color::White,
709/// });
710/// ctx.draw(&Rectangle {
711/// x: 10.0,
712/// y: 20.0,
713/// width: 10.0,
714/// height: 10.0,
715/// color: Color::Red,
716/// });
717/// });
718/// ```
719///
720/// [`marker`]: #method.marker
721#[derive(Debug, Clone, PartialEq)]
722pub struct Canvas<'a, F>
723where
724 F: Fn(&mut Context),
725{
726 block: Option<Block<'a>>,
727 x_bounds: [f64; 2],
728 y_bounds: [f64; 2],
729 paint_func: Option<F>,
730 background_color: Color,
731 marker: Marker,
732}
733
734impl<F> Default for Canvas<'_, F>
735where
736 F: Fn(&mut Context),
737{
738 fn default() -> Self {
739 Self {
740 block: None,
741 x_bounds: [0.0, 0.0],
742 y_bounds: [0.0, 0.0],
743 paint_func: None,
744 background_color: Color::Reset,
745 marker: Marker::Braille,
746 }
747 }
748}
749
750impl<'a, F> Canvas<'a, F>
751where
752 F: Fn(&mut Context),
753{
754 /// Wraps the canvas with a custom [`Block`] widget.
755 ///
756 /// This is a fluent setter method which must be chained or used as it consumes self
757 #[must_use = "method moves the value of self and returns the modified value"]
758 pub fn block(mut self, block: Block<'a>) -> Self {
759 self.block = Some(block);
760 self
761 }
762
763 /// Define the viewport of the canvas.
764 ///
765 /// If you were to "zoom" to a certain part of the world you may want to choose different
766 /// bounds.
767 ///
768 /// This is a fluent setter method which must be chained or used as it consumes self
769 #[must_use = "method moves the value of self and returns the modified value"]
770 pub const fn x_bounds(mut self, bounds: [f64; 2]) -> Self {
771 self.x_bounds = bounds;
772 self
773 }
774
775 /// Define the viewport of the canvas.
776 ///
777 /// If you were to "zoom" to a certain part of the world you may want to choose different
778 /// bounds.
779 ///
780 /// This is a fluent setter method which must be chained or used as it consumes self
781 #[must_use = "method moves the value of self and returns the modified value"]
782 pub const fn y_bounds(mut self, bounds: [f64; 2]) -> Self {
783 self.y_bounds = bounds;
784 self
785 }
786
787 /// Store the closure that will be used to draw to the [`Canvas`]
788 ///
789 /// This is a fluent setter method which must be chained or used as it consumes self
790 #[must_use = "method moves the value of self and returns the modified value"]
791 pub fn paint(mut self, f: F) -> Self {
792 self.paint_func = Some(f);
793 self
794 }
795
796 /// Change the background [`Color`] of the entire canvas
797 ///
798 /// This is a fluent setter method which must be chained or used as it consumes self
799 #[must_use = "method moves the value of self and returns the modified value"]
800 pub const fn background_color(mut self, color: Color) -> Self {
801 self.background_color = color;
802 self
803 }
804
805 /// Change the type of points used to draw the shapes.
806 ///
807 /// By default the [`Braille`] patterns are used as they provide a more fine grained result,
808 /// but you might want to use the simple [`Dot`] or [`Block`] instead if the targeted terminal
809 /// does not support those symbols.
810 ///
811 /// The [`HalfBlock`] marker is useful when you want to draw shapes with a higher resolution
812 /// than with a grid of characters (e.g. with [`Block`] or [`Dot`]) but lower than with
813 /// [`Braille`]. This grid type supports a foreground and background color for each terminal
814 /// cell. This allows for more flexibility than the `PatternGrid` which only supports a single
815 /// foreground color for each 2x4 dots cell.
816 ///
817 /// [`Braille`]: ratatui_core::symbols::Marker::Braille
818 /// [`HalfBlock`]: ratatui_core::symbols::Marker::HalfBlock
819 /// [`Dot`]: ratatui_core::symbols::Marker::Dot
820 /// [`Block`]: ratatui_core::symbols::Marker::Block
821 ///
822 /// # Examples
823 ///
824 /// ```
825 /// use ratatui::symbols;
826 /// use ratatui::widgets::canvas::Canvas;
827 ///
828 /// Canvas::default()
829 /// .marker(symbols::Marker::Braille)
830 /// .paint(|ctx| {});
831 ///
832 /// Canvas::default()
833 /// .marker(symbols::Marker::HalfBlock)
834 /// .paint(|ctx| {});
835 ///
836 /// Canvas::default()
837 /// .marker(symbols::Marker::Dot)
838 /// .paint(|ctx| {});
839 ///
840 /// Canvas::default()
841 /// .marker(symbols::Marker::Block)
842 /// .paint(|ctx| {});
843 /// ```
844 #[must_use = "method moves the value of self and returns the modified value"]
845 pub const fn marker(mut self, marker: Marker) -> Self {
846 self.marker = marker;
847 self
848 }
849}
850
851impl<F> Widget for Canvas<'_, F>
852where
853 F: Fn(&mut Context),
854{
855 fn render(self, area: Rect, buf: &mut Buffer) {
856 Widget::render(&self, area, buf);
857 }
858}
859
860impl<F> Widget for &Canvas<'_, F>
861where
862 F: Fn(&mut Context),
863{
864 fn render(self, area: Rect, buf: &mut Buffer) {
865 self.block.as_ref().render(area, buf);
866 let canvas_area = self.block.inner_if_some(area);
867 if canvas_area.is_empty() {
868 return;
869 }
870
871 buf.set_style(canvas_area, Style::default().bg(self.background_color));
872
873 let width = canvas_area.width as usize;
874
875 let Some(ref painter) = self.paint_func else {
876 return;
877 };
878
879 // Create a blank context that match the size of the canvas
880 let mut ctx = Context::new(
881 canvas_area.width,
882 canvas_area.height,
883 self.x_bounds,
884 self.y_bounds,
885 self.marker,
886 );
887 // Paint to this context
888 painter(&mut ctx);
889 ctx.finish();
890
891 // Retrieve painted points for each layer
892 for layer in ctx.layers {
893 for (index, layer_cell) in layer.contents.iter().enumerate() {
894 let (x, y) = (
895 (index % width) as u16 + canvas_area.left(),
896 (index / width) as u16 + canvas_area.top(),
897 );
898 let cell = &mut buf[(x, y)];
899
900 if let Some(symbol) = layer_cell.symbol {
901 cell.set_char(symbol);
902 }
903 if let Some(fg) = layer_cell.fg {
904 cell.set_fg(fg);
905 }
906 if let Some(bg) = layer_cell.bg {
907 cell.set_bg(bg);
908 }
909 }
910 }
911
912 // Finally draw the labels
913 let left = self.x_bounds[0];
914 let right = self.x_bounds[1];
915 let top = self.y_bounds[1];
916 let bottom = self.y_bounds[0];
917 let width = (self.x_bounds[1] - self.x_bounds[0]).abs();
918 let height = (self.y_bounds[1] - self.y_bounds[0]).abs();
919 let resolution = {
920 let width = f64::from(canvas_area.width - 1);
921 let height = f64::from(canvas_area.height - 1);
922 (width, height)
923 };
924 for label in ctx
925 .labels
926 .iter()
927 .filter(|l| l.x >= left && l.x <= right && l.y <= top && l.y >= bottom)
928 {
929 let x = ((label.x - left) * resolution.0 / width) as u16 + canvas_area.left();
930 let y = ((top - label.y) * resolution.1 / height) as u16 + canvas_area.top();
931 buf.set_line(x, y, &label.line, canvas_area.right() - x);
932 }
933 }
934}
935
936#[cfg(test)]
937mod tests {
938 use indoc::indoc;
939 use ratatui_core::buffer::Cell;
940 use rstest::rstest;
941
942 use super::*;
943
944 #[rstest]
945 #[case::block(Marker::Block, indoc!(
946 "
947 █xxxx
948 █xxxx
949 █xxxx
950 █xxxx
951 █████"
952 ))]
953 #[case::half_block(Marker::HalfBlock, indoc!(
954 "
955 █xxxx
956 █xxxx
957 █xxxx
958 █xxxx
959 █▄▄▄▄"
960 ))]
961 #[case::bar(Marker::Bar, indoc!(
962 "
963 ▄xxxx
964 ▄xxxx
965 ▄xxxx
966 ▄xxxx
967 ▄▄▄▄▄"
968 ))]
969 #[case::braille(Marker::Braille, indoc!(
970 "
971 ⡇xxxx
972 ⡇xxxx
973 ⡇xxxx
974 ⡇xxxx
975 ⣇⣀⣀⣀⣀"
976 ))]
977 #[case::quadrant(Marker::Quadrant, indoc!(
978 "
979 ▌xxxx
980 ▌xxxx
981 ▌xxxx
982 ▌xxxx
983 ▙▄▄▄▄"
984 ))]
985 #[case::sextant(Marker::Sextant, indoc!(
986 "
987 ▌xxxx
988 ▌xxxx
989 ▌xxxx
990 ▌xxxx
991 🬲🬭🬭🬭🬭"
992 ))]
993 #[case::octant(Marker::Octant, indoc!(
994 "
995 ▌xxxx
996 ▌xxxx
997 ▌xxxx
998 ▌xxxx
999 ▂▂▂▂"
1000 ))]
1001 #[case::x_sign(Marker::Custom('×'), indoc!(
1002 "
1003 ×xxxx
1004 ×xxxx
1005 ×xxxx
1006 ×xxxx
1007 ×××××"
1008 ))]
1009 #[case::plus_sign(Marker::Custom('+'), indoc!(
1010 "
1011 +xxxx
1012 +xxxx
1013 +xxxx
1014 +xxxx
1015 +++++"
1016 ))]
1017 #[case::dot(Marker::Dot, indoc!(
1018 "
1019 •xxxx
1020 •xxxx
1021 •xxxx
1022 •xxxx
1023 •••••"
1024 ))]
1025 fn test_horizontal_with_vertical(#[case] marker: Marker, #[case] expected: &'static str) {
1026 let area = Rect::new(0, 0, 5, 5);
1027 let mut buf = Buffer::filled(area, Cell::new("x"));
1028 let horizontal_line = Line {
1029 x1: 0.0,
1030 y1: 0.0,
1031 x2: 10.0,
1032 y2: 0.0,
1033 color: Color::Reset,
1034 };
1035 let vertical_line = Line {
1036 x1: 0.0,
1037 y1: 0.0,
1038 x2: 0.0,
1039 y2: 10.0,
1040 color: Color::Reset,
1041 };
1042 Canvas::default()
1043 .marker(marker)
1044 .paint(|ctx| {
1045 ctx.draw(&vertical_line);
1046 ctx.draw(&horizontal_line);
1047 })
1048 .x_bounds([0.0, 10.0])
1049 .y_bounds([0.0, 10.0])
1050 .render(area, &mut buf);
1051 assert_eq!(buf, Buffer::with_lines(expected.lines()));
1052 }
1053
1054 #[rstest]
1055 #[case::block(Marker::Block, indoc!(
1056 "
1057 █xxx█
1058 x█x█x
1059 xx█xx
1060 x█x█x
1061 █xxx█"))]
1062 #[case::half_block(Marker::HalfBlock,
1063 indoc!(
1064 "
1065 █xxx█
1066 x█x█x
1067 xx█xx
1068 x█x█x
1069 █xxx█")
1070 )]
1071 #[case::bar(Marker::Bar, indoc!(
1072 "
1073 ▄xxx▄
1074 x▄x▄x
1075 xx▄xx
1076 x▄x▄x
1077 ▄xxx▄"))]
1078 #[case::braille(Marker::Braille, indoc!(
1079 "
1080 ⢣xxx⡜
1081 x⢣x⡜x
1082 xx⣿xx
1083 x⡜x⢣x
1084 ⡜xxx⢣"
1085 ))]
1086 #[case::quadrant(Marker::Quadrant, indoc!(
1087 "
1088 ▚xxx▞
1089 x▚x▞x
1090 xx█xx
1091 x▞x▚x
1092 ▞xxx▚"
1093 ))]
1094 #[case::sextant(Marker::Sextant, indoc!(
1095 "
1096 🬧xxx🬔
1097 x🬧x🬔x
1098 xx█xx
1099 x🬘x🬣x
1100 🬘xxx🬣"
1101 ))]
1102 #[case::octant(Marker::Octant, indoc!(
1103 "
1104 ▚xxx▞
1105 x▚x▞x
1106 xx█xx
1107 x▞x▚x
1108 ▞xxx▚"
1109 ))]
1110 #[case::x_sign(Marker::Custom('×'), indoc!(
1111 "
1112 ×xxx×
1113 x×x×x
1114 xx×xx
1115 x×x×x
1116 ×xxx×"
1117 ))]
1118 #[case::plus_sign(Marker::Custom('+'), indoc!(
1119 "
1120 +xxx+
1121 x+x+x
1122 xx+xx
1123 x+x+x
1124 +xxx+"
1125 ))]
1126 #[case::dot(Marker::Dot, indoc!(
1127 "
1128 •xxx•
1129 x•x•x
1130 xx•xx
1131 x•x•x
1132 •xxx•"
1133 ))]
1134 fn test_diagonal_lines(#[case] marker: Marker, #[case] expected: &'static str) {
1135 let area = Rect::new(0, 0, 5, 5);
1136 let mut buf = Buffer::filled(area, Cell::new("x"));
1137 let diagonal_up = Line {
1138 x1: 0.0,
1139 y1: 0.0,
1140 x2: 10.0,
1141 y2: 10.0,
1142 color: Color::Reset,
1143 };
1144 let diagonal_down = Line {
1145 x1: 0.0,
1146 y1: 10.0,
1147 x2: 10.0,
1148 y2: 0.0,
1149 color: Color::Reset,
1150 };
1151 Canvas::default()
1152 .marker(marker)
1153 .paint(|ctx| {
1154 ctx.draw(&diagonal_down);
1155 ctx.draw(&diagonal_up);
1156 })
1157 .x_bounds([0.0, 10.0])
1158 .y_bounds([0.0, 10.0])
1159 .render(area, &mut buf);
1160 assert_eq!(buf, Buffer::with_lines(expected.lines()));
1161 }
1162
1163 // The canvas methods work a lot with arithmetic so here we enter various width and height
1164 // values to check if there are any integer overflows we just initialize the canvas painters
1165 #[test]
1166 fn check_canvas_paint_max() {
1167 let mut b_grid = PatternGrid::<2, 4>::new(u16::MAX, 2, &OCTANTS);
1168 let mut c_grid = CharGrid::new(u16::MAX, 2, 'd');
1169
1170 let max = u16::MAX as usize;
1171
1172 b_grid.paint(0, 0, Color::Red);
1173 b_grid.paint(0, max, Color::Red);
1174 b_grid.paint(max, 0, Color::Red);
1175 b_grid.paint(max, max, Color::Red);
1176
1177 c_grid.paint(0, 0, Color::Red);
1178 c_grid.paint(0, max, Color::Red);
1179 c_grid.paint(max, 0, Color::Red);
1180 c_grid.paint(max, max, Color::Red);
1181 }
1182
1183 // We delibately cause integer overflow to check if we don't panic and don't get weird behavior
1184 #[test]
1185 fn check_canvas_paint_overflow() {
1186 let mut b_grid = PatternGrid::<2, 4>::new(u16::MAX, 3, &BRAILLE);
1187 let mut c_grid = CharGrid::new(u16::MAX, 3, 'd');
1188
1189 let max = u16::MAX as usize + 10;
1190
1191 // see if we can paint outside bounds
1192 b_grid.paint(max, max, Color::Red);
1193 c_grid.paint(max, max, Color::Red);
1194 // see if we can paint usize max bounds
1195 b_grid.paint(usize::MAX, usize::MAX, Color::Red);
1196 c_grid.paint(usize::MAX, usize::MAX, Color::Red);
1197 }
1198
1199 #[test]
1200 fn render_in_minimal_buffer() {
1201 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
1202 let canvas = Canvas::default()
1203 .x_bounds([0.0, 10.0])
1204 .y_bounds([0.0, 10.0])
1205 .paint(|_ctx| {});
1206 // This should not panic, even if the buffer is too small to render the canvas.
1207 canvas.render(buffer.area, &mut buffer);
1208 assert_eq!(buffer, Buffer::with_lines([" "]));
1209 }
1210
1211 #[test]
1212 fn render_in_zero_size_buffer() {
1213 let mut buffer = Buffer::empty(Rect::ZERO);
1214 let canvas = Canvas::default()
1215 .x_bounds([0.0, 10.0])
1216 .y_bounds([0.0, 10.0])
1217 .paint(|_ctx| {});
1218 // This should not panic, even if the buffer has zero size.
1219 canvas.render(buffer.area, &mut buffer);
1220 }
1221}