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::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::Dot | _ => Box::new(CharGrid::new(width, height, dot)),
591 }
592 }
593
594 /// Change the marker being used in this context.
595 ///
596 /// This will save the last layer if necessary and reset the grid to use the new marker.
597 pub fn marker(&mut self, marker: Marker) {
598 self.finish();
599 self.grid = Self::marker_to_grid(self.width, self.height, marker);
600 }
601
602 /// Draw the given [`Shape`] in this context
603 pub fn draw<S>(&mut self, shape: &S)
604 where
605 S: Shape,
606 {
607 self.dirty = true;
608 let mut painter = Painter::from(self);
609 shape.draw(&mut painter);
610 }
611
612 /// Save the existing state of the grid as a layer.
613 ///
614 /// Save the existing state as a layer to be rendered and reset the grid to its initial
615 /// state for the next layer.
616 ///
617 /// This allows the canvas to be drawn in multiple layers. This is useful if you want to
618 /// draw multiple shapes on the [`Canvas`] in specific order.
619 pub fn layer(&mut self) {
620 self.layers.push(self.grid.save());
621 self.grid.reset();
622 self.dirty = false;
623 }
624
625 /// Print a [`Text`] on the [`Canvas`] at the given position.
626 ///
627 /// Note that the text is always printed on top of the canvas and is **not** affected by the
628 /// layers.
629 ///
630 /// [`Text`]: ratatui_core::text::Text
631 pub fn print<T>(&mut self, x: f64, y: f64, line: T)
632 where
633 T: Into<TextLine<'a>>,
634 {
635 self.labels.push(Label {
636 x,
637 y,
638 line: line.into(),
639 });
640 }
641
642 /// Save the last layer if necessary
643 fn finish(&mut self) {
644 if self.dirty {
645 self.layer();
646 }
647 }
648}
649
650/// The Canvas widget provides a means to draw shapes (Lines, Rectangles, Circles, etc.) on a grid.
651///
652/// By default the grid is made of Braille patterns but you may change the marker to use a different
653/// set of symbols. If your terminal or font does not support this unicode block, you will see
654/// unicode replacement characters (�) instead of braille dots. The Braille patterns (as well the
655/// octant character patterns) provide a more fine grained result with a 2x4 resolution per
656/// character, but you might want to use a simple dot, block, or bar instead by calling the
657/// [`marker`] method if your target environment does not support those symbols.
658///
659/// See [Unicode Braille Patterns](https://en.wikipedia.org/wiki/Braille_Patterns) for more info.
660///
661/// The `Octant` marker is similar to the `Braille` marker but, instead of sparse dots, displays
662/// densely packed and regularly spaced pseudo-pixels, without visible bands between rows and
663/// columns. However, it uses characters that are not yet as widely supported as the Braille
664/// unicode block.
665///
666/// The `Quadrant` and `Sextant` markers are in turn akin to the `Octant` marker, but with a 2x2
667/// and 2x3 resolution, respectively.
668///
669/// The `HalfBlock` marker is useful when you want to draw shapes with a higher resolution than a
670/// `CharGrid` but lower than a `PatternGrid`. This grid type supports a foreground and background
671/// color for each terminal cell. This allows for more flexibility than the `PatternGrid` which
672/// only supports a single foreground color for each 2x4 dots cell.
673///
674/// The Canvas widget is used by calling the [`Canvas::paint`] method and passing a closure that
675/// will be used to draw on the canvas. The closure will be passed a [`Context`] object that can be
676/// used to draw shapes on the canvas.
677///
678/// The [`Context`] object provides a [`Context::draw`] method that can be used to draw shapes on
679/// the canvas. The [`Context::layer`] method can be used to save the current state of the canvas
680/// and start a new layer. This is useful if you want to draw multiple shapes on the canvas in
681/// specific order. The [`Context`] object also provides a [`Context::print`] method that can be
682/// used to print text on the canvas. Note that the text is always printed on top of the canvas and
683/// is not affected by the layers.
684///
685/// # Examples
686///
687/// ```
688/// use ratatui::style::Color;
689/// use ratatui::widgets::Block;
690/// use ratatui::widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle};
691///
692/// Canvas::default()
693/// .block(Block::bordered().title("Canvas"))
694/// .x_bounds([-180.0, 180.0])
695/// .y_bounds([-90.0, 90.0])
696/// .paint(|ctx| {
697/// ctx.draw(&Map {
698/// resolution: MapResolution::High,
699/// color: Color::White,
700/// });
701/// ctx.layer();
702/// ctx.draw(&Line {
703/// x1: 0.0,
704/// y1: 10.0,
705/// x2: 10.0,
706/// y2: 10.0,
707/// color: Color::White,
708/// });
709/// ctx.draw(&Rectangle {
710/// x: 10.0,
711/// y: 20.0,
712/// width: 10.0,
713/// height: 10.0,
714/// color: Color::Red,
715/// });
716/// });
717/// ```
718///
719/// [`marker`]: #method.marker
720#[derive(Debug, Clone, PartialEq)]
721pub struct Canvas<'a, F>
722where
723 F: Fn(&mut Context),
724{
725 block: Option<Block<'a>>,
726 x_bounds: [f64; 2],
727 y_bounds: [f64; 2],
728 paint_func: Option<F>,
729 background_color: Color,
730 marker: Marker,
731}
732
733impl<F> Default for Canvas<'_, F>
734where
735 F: Fn(&mut Context),
736{
737 fn default() -> Self {
738 Self {
739 block: None,
740 x_bounds: [0.0, 0.0],
741 y_bounds: [0.0, 0.0],
742 paint_func: None,
743 background_color: Color::Reset,
744 marker: Marker::Braille,
745 }
746 }
747}
748
749impl<'a, F> Canvas<'a, F>
750where
751 F: Fn(&mut Context),
752{
753 /// Wraps the canvas with a custom [`Block`] widget.
754 ///
755 /// This is a fluent setter method which must be chained or used as it consumes self
756 #[must_use = "method moves the value of self and returns the modified value"]
757 pub fn block(mut self, block: Block<'a>) -> Self {
758 self.block = Some(block);
759 self
760 }
761
762 /// Define the viewport of the canvas.
763 ///
764 /// If you were to "zoom" to a certain part of the world you may want to choose different
765 /// bounds.
766 ///
767 /// This is a fluent setter method which must be chained or used as it consumes self
768 #[must_use = "method moves the value of self and returns the modified value"]
769 pub const fn x_bounds(mut self, bounds: [f64; 2]) -> Self {
770 self.x_bounds = bounds;
771 self
772 }
773
774 /// Define the viewport of the canvas.
775 ///
776 /// If you were to "zoom" to a certain part of the world you may want to choose different
777 /// bounds.
778 ///
779 /// This is a fluent setter method which must be chained or used as it consumes self
780 #[must_use = "method moves the value of self and returns the modified value"]
781 pub const fn y_bounds(mut self, bounds: [f64; 2]) -> Self {
782 self.y_bounds = bounds;
783 self
784 }
785
786 /// Store the closure that will be used to draw to the [`Canvas`]
787 ///
788 /// This is a fluent setter method which must be chained or used as it consumes self
789 #[must_use = "method moves the value of self and returns the modified value"]
790 pub fn paint(mut self, f: F) -> Self {
791 self.paint_func = Some(f);
792 self
793 }
794
795 /// Change the background [`Color`] of the entire canvas
796 ///
797 /// This is a fluent setter method which must be chained or used as it consumes self
798 #[must_use = "method moves the value of self and returns the modified value"]
799 pub const fn background_color(mut self, color: Color) -> Self {
800 self.background_color = color;
801 self
802 }
803
804 /// Change the type of points used to draw the shapes.
805 ///
806 /// By default the [`Braille`] patterns are used as they provide a more fine grained result,
807 /// but you might want to use the simple [`Dot`] or [`Block`] instead if the targeted terminal
808 /// does not support those symbols.
809 ///
810 /// The [`HalfBlock`] marker is useful when you want to draw shapes with a higher resolution
811 /// than with a grid of characters (e.g. with [`Block`] or [`Dot`]) but lower than with
812 /// [`Braille`]. This grid type supports a foreground and background color for each terminal
813 /// cell. This allows for more flexibility than the `PatternGrid` which only supports a single
814 /// foreground color for each 2x4 dots cell.
815 ///
816 /// [`Braille`]: ratatui_core::symbols::Marker::Braille
817 /// [`HalfBlock`]: ratatui_core::symbols::Marker::HalfBlock
818 /// [`Dot`]: ratatui_core::symbols::Marker::Dot
819 /// [`Block`]: ratatui_core::symbols::Marker::Block
820 ///
821 /// # Examples
822 ///
823 /// ```
824 /// use ratatui::symbols;
825 /// use ratatui::widgets::canvas::Canvas;
826 ///
827 /// Canvas::default()
828 /// .marker(symbols::Marker::Braille)
829 /// .paint(|ctx| {});
830 ///
831 /// Canvas::default()
832 /// .marker(symbols::Marker::HalfBlock)
833 /// .paint(|ctx| {});
834 ///
835 /// Canvas::default()
836 /// .marker(symbols::Marker::Dot)
837 /// .paint(|ctx| {});
838 ///
839 /// Canvas::default()
840 /// .marker(symbols::Marker::Block)
841 /// .paint(|ctx| {});
842 /// ```
843 #[must_use = "method moves the value of self and returns the modified value"]
844 pub const fn marker(mut self, marker: Marker) -> Self {
845 self.marker = marker;
846 self
847 }
848}
849
850impl<F> Widget for Canvas<'_, F>
851where
852 F: Fn(&mut Context),
853{
854 fn render(self, area: Rect, buf: &mut Buffer) {
855 Widget::render(&self, area, buf);
856 }
857}
858
859impl<F> Widget for &Canvas<'_, F>
860where
861 F: Fn(&mut Context),
862{
863 fn render(self, area: Rect, buf: &mut Buffer) {
864 self.block.as_ref().render(area, buf);
865 let canvas_area = self.block.inner_if_some(area);
866 if canvas_area.is_empty() {
867 return;
868 }
869
870 buf.set_style(canvas_area, Style::default().bg(self.background_color));
871
872 let width = canvas_area.width as usize;
873
874 let Some(ref painter) = self.paint_func else {
875 return;
876 };
877
878 // Create a blank context that match the size of the canvas
879 let mut ctx = Context::new(
880 canvas_area.width,
881 canvas_area.height,
882 self.x_bounds,
883 self.y_bounds,
884 self.marker,
885 );
886 // Paint to this context
887 painter(&mut ctx);
888 ctx.finish();
889
890 // Retrieve painted points for each layer
891 for layer in ctx.layers {
892 for (index, layer_cell) in layer.contents.iter().enumerate() {
893 let (x, y) = (
894 (index % width) as u16 + canvas_area.left(),
895 (index / width) as u16 + canvas_area.top(),
896 );
897 let cell = &mut buf[(x, y)];
898
899 if let Some(symbol) = layer_cell.symbol {
900 cell.set_char(symbol);
901 }
902 if let Some(fg) = layer_cell.fg {
903 cell.set_fg(fg);
904 }
905 if let Some(bg) = layer_cell.bg {
906 cell.set_bg(bg);
907 }
908 }
909 }
910
911 // Finally draw the labels
912 let left = self.x_bounds[0];
913 let right = self.x_bounds[1];
914 let top = self.y_bounds[1];
915 let bottom = self.y_bounds[0];
916 let width = (self.x_bounds[1] - self.x_bounds[0]).abs();
917 let height = (self.y_bounds[1] - self.y_bounds[0]).abs();
918 let resolution = {
919 let width = f64::from(canvas_area.width - 1);
920 let height = f64::from(canvas_area.height - 1);
921 (width, height)
922 };
923 for label in ctx
924 .labels
925 .iter()
926 .filter(|l| l.x >= left && l.x <= right && l.y <= top && l.y >= bottom)
927 {
928 let x = ((label.x - left) * resolution.0 / width) as u16 + canvas_area.left();
929 let y = ((top - label.y) * resolution.1 / height) as u16 + canvas_area.top();
930 buf.set_line(x, y, &label.line, canvas_area.right() - x);
931 }
932 }
933}
934
935#[cfg(test)]
936mod tests {
937 use indoc::indoc;
938 use ratatui_core::buffer::Cell;
939 use rstest::rstest;
940
941 use super::*;
942
943 #[rstest]
944 #[case::block(Marker::Block, indoc!(
945 "
946 █xxxx
947 █xxxx
948 █xxxx
949 █xxxx
950 █████"
951 ))]
952 #[case::half_block(Marker::HalfBlock, indoc!(
953 "
954 █xxxx
955 █xxxx
956 █xxxx
957 █xxxx
958 █▄▄▄▄"
959 ))]
960 #[case::bar(Marker::Bar, indoc!(
961 "
962 ▄xxxx
963 ▄xxxx
964 ▄xxxx
965 ▄xxxx
966 ▄▄▄▄▄"
967 ))]
968 #[case::braille(Marker::Braille, indoc!(
969 "
970 ⡇xxxx
971 ⡇xxxx
972 ⡇xxxx
973 ⡇xxxx
974 ⣇⣀⣀⣀⣀"
975 ))]
976 #[case::quadrant(Marker::Quadrant, indoc!(
977 "
978 ▌xxxx
979 ▌xxxx
980 ▌xxxx
981 ▌xxxx
982 ▙▄▄▄▄"
983 ))]
984 #[case::sextant(Marker::Sextant, indoc!(
985 "
986 ▌xxxx
987 ▌xxxx
988 ▌xxxx
989 ▌xxxx
990 🬲🬭🬭🬭🬭"
991 ))]
992 #[case::octant(Marker::Octant, indoc!(
993 "
994 ▌xxxx
995 ▌xxxx
996 ▌xxxx
997 ▌xxxx
998 ▂▂▂▂"
999 ))]
1000 #[case::dot(Marker::Dot, indoc!(
1001 "
1002 •xxxx
1003 •xxxx
1004 •xxxx
1005 •xxxx
1006 •••••"
1007 ))]
1008 fn test_horizontal_with_vertical(#[case] marker: Marker, #[case] expected: &'static str) {
1009 let area = Rect::new(0, 0, 5, 5);
1010 let mut buf = Buffer::filled(area, Cell::new("x"));
1011 let horizontal_line = Line {
1012 x1: 0.0,
1013 y1: 0.0,
1014 x2: 10.0,
1015 y2: 0.0,
1016 color: Color::Reset,
1017 };
1018 let vertical_line = Line {
1019 x1: 0.0,
1020 y1: 0.0,
1021 x2: 0.0,
1022 y2: 10.0,
1023 color: Color::Reset,
1024 };
1025 Canvas::default()
1026 .marker(marker)
1027 .paint(|ctx| {
1028 ctx.draw(&vertical_line);
1029 ctx.draw(&horizontal_line);
1030 })
1031 .x_bounds([0.0, 10.0])
1032 .y_bounds([0.0, 10.0])
1033 .render(area, &mut buf);
1034 assert_eq!(buf, Buffer::with_lines(expected.lines()));
1035 }
1036
1037 #[rstest]
1038 #[case::block(Marker::Block, indoc!(
1039 "
1040 █xxx█
1041 x█x█x
1042 xx█xx
1043 x█x█x
1044 █xxx█"))]
1045 #[case::half_block(Marker::HalfBlock,
1046 indoc!(
1047 "
1048 █xxx█
1049 x█x█x
1050 xx█xx
1051 x█x█x
1052 █xxx█")
1053 )]
1054 #[case::bar(Marker::Bar, indoc!(
1055 "
1056 ▄xxx▄
1057 x▄x▄x
1058 xx▄xx
1059 x▄x▄x
1060 ▄xxx▄"))]
1061 #[case::braille(Marker::Braille, indoc!(
1062 "
1063 ⢣xxx⡜
1064 x⢣x⡜x
1065 xx⣿xx
1066 x⡜x⢣x
1067 ⡜xxx⢣"
1068 ))]
1069 #[case::quadrant(Marker::Quadrant, indoc!(
1070 "
1071 ▚xxx▞
1072 x▚x▞x
1073 xx█xx
1074 x▞x▚x
1075 ▞xxx▚"
1076 ))]
1077 #[case::sextant(Marker::Sextant, indoc!(
1078 "
1079 🬧xxx🬔
1080 x🬧x🬔x
1081 xx█xx
1082 x🬘x🬣x
1083 🬘xxx🬣"
1084 ))]
1085 #[case::octant(Marker::Octant, indoc!(
1086 "
1087 ▚xxx▞
1088 x▚x▞x
1089 xx█xx
1090 x▞x▚x
1091 ▞xxx▚"
1092 ))]
1093 #[case::dot(Marker::Dot, indoc!(
1094 "
1095 •xxx•
1096 x•x•x
1097 xx•xx
1098 x•x•x
1099 •xxx•"
1100 ))]
1101 fn test_diagonal_lines(#[case] marker: Marker, #[case] expected: &'static str) {
1102 let area = Rect::new(0, 0, 5, 5);
1103 let mut buf = Buffer::filled(area, Cell::new("x"));
1104 let diagonal_up = Line {
1105 x1: 0.0,
1106 y1: 0.0,
1107 x2: 10.0,
1108 y2: 10.0,
1109 color: Color::Reset,
1110 };
1111 let diagonal_down = Line {
1112 x1: 0.0,
1113 y1: 10.0,
1114 x2: 10.0,
1115 y2: 0.0,
1116 color: Color::Reset,
1117 };
1118 Canvas::default()
1119 .marker(marker)
1120 .paint(|ctx| {
1121 ctx.draw(&diagonal_down);
1122 ctx.draw(&diagonal_up);
1123 })
1124 .x_bounds([0.0, 10.0])
1125 .y_bounds([0.0, 10.0])
1126 .render(area, &mut buf);
1127 assert_eq!(buf, Buffer::with_lines(expected.lines()));
1128 }
1129
1130 // The canvas methods work a lot with arithmetic so here we enter various width and height
1131 // values to check if there are any integer overflows we just initialize the canvas painters
1132 #[test]
1133 fn check_canvas_paint_max() {
1134 let mut b_grid = PatternGrid::<2, 4>::new(u16::MAX, 2, &OCTANTS);
1135 let mut c_grid = CharGrid::new(u16::MAX, 2, 'd');
1136
1137 let max = u16::MAX as usize;
1138
1139 b_grid.paint(0, 0, Color::Red);
1140 b_grid.paint(0, max, Color::Red);
1141 b_grid.paint(max, 0, Color::Red);
1142 b_grid.paint(max, max, Color::Red);
1143
1144 c_grid.paint(0, 0, Color::Red);
1145 c_grid.paint(0, max, Color::Red);
1146 c_grid.paint(max, 0, Color::Red);
1147 c_grid.paint(max, max, Color::Red);
1148 }
1149
1150 // We delibately cause integer overflow to check if we don't panic and don't get weird behavior
1151 #[test]
1152 fn check_canvas_paint_overflow() {
1153 let mut b_grid = PatternGrid::<2, 4>::new(u16::MAX, 3, &BRAILLE);
1154 let mut c_grid = CharGrid::new(u16::MAX, 3, 'd');
1155
1156 let max = u16::MAX as usize + 10;
1157
1158 // see if we can paint outside bounds
1159 b_grid.paint(max, max, Color::Red);
1160 c_grid.paint(max, max, Color::Red);
1161 // see if we can paint usize max bounds
1162 b_grid.paint(usize::MAX, usize::MAX, Color::Red);
1163 c_grid.paint(usize::MAX, usize::MAX, Color::Red);
1164 }
1165
1166 #[test]
1167 fn render_in_minimal_buffer() {
1168 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
1169 let canvas = Canvas::default()
1170 .x_bounds([0.0, 10.0])
1171 .y_bounds([0.0, 10.0])
1172 .paint(|_ctx| {});
1173 // This should not panic, even if the buffer is too small to render the canvas.
1174 canvas.render(buffer.area, &mut buffer);
1175 assert_eq!(buffer, Buffer::with_lines([" "]));
1176 }
1177
1178 #[test]
1179 fn render_in_zero_size_buffer() {
1180 let mut buffer = Buffer::empty(Rect::ZERO);
1181 let canvas = Canvas::default()
1182 .x_bounds([0.0, 10.0])
1183 .y_bounds([0.0, 10.0])
1184 .paint(|_ctx| {});
1185 // This should not panic, even if the buffer has zero size.
1186 canvas.render(buffer.area, &mut buffer);
1187 }
1188}