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.
15mod circle;
16mod line;
17mod map;
18mod points;
19mod rectangle;
20mod world;
21
22use std::{fmt, iter::zip};
23
24use itertools::Itertools;
25
26pub use self::{
27 circle::Circle,
28 line::Line,
29 map::{Map, MapResolution},
30 points::Points,
31 rectangle::Rectangle,
32};
33use crate::{
34 buffer::Buffer,
35 layout::Rect,
36 style::{Color, Style},
37 symbols::{self, Marker},
38 text::Line as TextLine,
39 widgets::{block::BlockExt, Block, Widget, WidgetRef},
40};
41
42/// Something that can be drawn on a [`Canvas`].
43///
44/// You may implement your own canvas custom widgets by implementing this trait.
45pub trait Shape {
46 /// Draws this [`Shape`] using the given [`Painter`].
47 ///
48 /// This is the only method required to implement a custom widget that can be drawn on a
49 /// [`Canvas`].
50 fn draw(&self, painter: &mut Painter);
51}
52
53/// Label to draw some text on the canvas
54#[derive(Debug, Default, Clone, PartialEq)]
55pub struct Label<'a> {
56 x: f64,
57 y: f64,
58 line: TextLine<'a>,
59}
60
61/// A single layer of the canvas.
62///
63/// This allows the canvas to be drawn in multiple layers. This is useful if you want to draw
64/// multiple shapes on the canvas in specific order.
65#[derive(Debug)]
66struct Layer {
67 // A string of characters representing the grid. This will be wrapped to the width of the grid
68 // when rendering
69 string: String,
70 // Colors for foreground and background of each cell
71 colors: Vec<(Color, Color)>,
72}
73
74/// A grid of cells that can be painted on.
75///
76/// The grid represents a particular screen region measured in rows and columns. The underlying
77/// resolution of the grid might exceed the number of rows and columns. For example, a grid of
78/// Braille patterns will have a resolution of 2x4 dots per cell. This means that a grid of 10x10
79/// cells will have a resolution of 20x40 dots.
80trait Grid: fmt::Debug {
81 /// Get the resolution of the grid in number of dots.
82 ///
83 /// This doesn't have to be the same as the number of rows and columns of the grid. For example,
84 /// a grid of Braille patterns will have a resolution of 2x4 dots per cell. This means that a
85 /// grid of 10x10 cells will have a resolution of 20x40 dots.
86 fn resolution(&self) -> (f64, f64);
87 /// Paint a point of the grid.
88 ///
89 /// The point is expressed in number of dots starting at the origin of the grid in the top left
90 /// corner. Note that this is not the same as the `(x, y)` coordinates of the canvas.
91 fn paint(&mut self, x: usize, y: usize, color: Color);
92 /// Save the current state of the [`Grid`] as a layer to be rendered
93 fn save(&self) -> Layer;
94 /// Reset the grid to its initial state
95 fn reset(&mut self);
96}
97
98/// The `BrailleGrid` is a grid made up of cells each containing a Braille pattern.
99///
100/// This makes it possible to draw shapes with a resolution of 2x4 dots per cell. This is useful
101/// when you want to draw shapes with a high resolution. Font support for Braille patterns is
102/// required to see the dots. If your terminal or font does not support this unicode block, you
103/// will see unicode replacement characters (�) instead of braille dots.
104///
105/// This grid type only supports a single foreground color for each 2x4 dots cell. There is no way
106/// to set the individual color of each dot in the braille pattern.
107#[derive(Debug)]
108struct BrailleGrid {
109 /// Width of the grid in number of terminal columns
110 width: u16,
111 /// Height of the grid in number of terminal rows
112 height: u16,
113 /// Represents the unicode braille patterns. Will take a value between `0x2800` and `0x28FF`
114 /// this is converted to an utf16 string when converting to a layer. See
115 /// <https://en.wikipedia.org/wiki/Braille_Patterns> for more info.
116 utf16_code_points: Vec<u16>,
117 /// The color of each cell only supports foreground colors for now as there's no way to
118 /// individually set the background color of each dot in the braille pattern.
119 colors: Vec<Color>,
120}
121
122impl BrailleGrid {
123 /// Create a new `BrailleGrid` with the given width and height measured in terminal columns and
124 /// rows respectively.
125 fn new(width: u16, height: u16) -> Self {
126 let length = usize::from(width * height);
127 Self {
128 width,
129 height,
130 utf16_code_points: vec![symbols::braille::BLANK; length],
131 colors: vec![Color::Reset; length],
132 }
133 }
134}
135
136impl Grid for BrailleGrid {
137 fn resolution(&self) -> (f64, f64) {
138 (f64::from(self.width) * 2.0, f64::from(self.height) * 4.0)
139 }
140
141 fn save(&self) -> Layer {
142 let string = String::from_utf16(&self.utf16_code_points).unwrap();
143 // the background color is always reset for braille patterns
144 let colors = self.colors.iter().map(|c| (*c, Color::Reset)).collect();
145 Layer { string, colors }
146 }
147
148 fn reset(&mut self) {
149 self.utf16_code_points.fill(symbols::braille::BLANK);
150 self.colors.fill(Color::Reset);
151 }
152
153 fn paint(&mut self, x: usize, y: usize, color: Color) {
154 let index = y / 4 * self.width as usize + x / 2;
155 // using get_mut here because we are indexing the vector with usize values
156 // and we want to make sure we don't panic if the index is out of bounds
157 if let Some(c) = self.utf16_code_points.get_mut(index) {
158 *c |= symbols::braille::DOTS[y % 4][x % 2];
159 }
160 if let Some(c) = self.colors.get_mut(index) {
161 *c = color;
162 }
163 }
164}
165
166/// The `CharGrid` is a grid made up of cells each containing a single character.
167///
168/// This makes it possible to draw shapes with a resolution of 1x1 dots per cell. This is useful
169/// when you want to draw shapes with a low resolution.
170#[derive(Debug)]
171struct CharGrid {
172 /// Width of the grid in number of terminal columns
173 width: u16,
174 /// Height of the grid in number of terminal rows
175 height: u16,
176 /// Represents a single character for each cell
177 cells: Vec<char>,
178 /// The color of each cell
179 colors: Vec<Color>,
180 /// The character to use for every cell - e.g. a block, dot, etc.
181 cell_char: char,
182}
183
184impl CharGrid {
185 /// Create a new `CharGrid` with the given width and height measured in terminal columns and
186 /// rows respectively.
187 fn new(width: u16, height: u16, cell_char: char) -> Self {
188 let length = usize::from(width * height);
189 Self {
190 width,
191 height,
192 cells: vec![' '; length],
193 colors: vec![Color::Reset; length],
194 cell_char,
195 }
196 }
197}
198
199impl Grid for CharGrid {
200 fn resolution(&self) -> (f64, f64) {
201 (f64::from(self.width), f64::from(self.height))
202 }
203
204 fn save(&self) -> Layer {
205 Layer {
206 string: self.cells.iter().collect(),
207 colors: self.colors.iter().map(|c| (*c, Color::Reset)).collect(),
208 }
209 }
210
211 fn reset(&mut self) {
212 self.cells.fill(' ');
213 self.colors.fill(Color::Reset);
214 }
215
216 fn paint(&mut self, x: usize, y: usize, color: Color) {
217 let index = y * self.width as usize + x;
218 // using get_mut here because we are indexing the vector with usize values
219 // and we want to make sure we don't panic if the index is out of bounds
220 if let Some(c) = self.cells.get_mut(index) {
221 *c = self.cell_char;
222 }
223 if let Some(c) = self.colors.get_mut(index) {
224 *c = color;
225 }
226 }
227}
228
229/// The `HalfBlockGrid` is a grid made up of cells each containing a half block character.
230///
231/// In terminals, each character is usually twice as tall as it is wide. Unicode has a couple of
232/// vertical half block characters, the upper half block '▀' and lower half block '▄' which take up
233/// half the height of a normal character but the full width. Together with an empty space ' ' and a
234/// full block '█', we can effectively double the resolution of a single cell. In addition, because
235/// each character can have a foreground and background color, we can control the color of the upper
236/// and lower half of each cell. This allows us to draw shapes with a resolution of 1x2 "pixels" per
237/// cell.
238///
239/// This allows for more flexibility than the `BrailleGrid` which only supports a single
240/// foreground color for each 2x4 dots cell, and the `CharGrid` which only supports a single
241/// character for each cell.
242#[derive(Debug)]
243struct HalfBlockGrid {
244 /// Width of the grid in number of terminal columns
245 width: u16,
246 /// Height of the grid in number of terminal rows
247 height: u16,
248 /// Represents a single color for each "pixel" arranged in column, row order
249 pixels: Vec<Vec<Color>>,
250}
251
252impl HalfBlockGrid {
253 /// Create a new `HalfBlockGrid` with the given width and height measured in terminal columns
254 /// and rows respectively.
255 fn new(width: u16, height: u16) -> Self {
256 Self {
257 width,
258 height,
259 pixels: vec![vec![Color::Reset; width as usize]; height as usize * 2],
260 }
261 }
262}
263
264impl Grid for HalfBlockGrid {
265 fn resolution(&self) -> (f64, f64) {
266 (f64::from(self.width), f64::from(self.height) * 2.0)
267 }
268
269 fn save(&self) -> Layer {
270 // Given that we store the pixels in a grid, and that we want to use 2 pixels arranged
271 // vertically to form a single terminal cell, which can be either empty, upper half block,
272 // lower half block or full block, we need examine the pixels in vertical pairs to decide
273 // what character to print in each cell. So these are the 4 states we use to represent each
274 // cell:
275 //
276 // 1. upper: reset, lower: reset => ' ' fg: reset / bg: reset
277 // 2. upper: reset, lower: color => '▄' fg: lower color / bg: reset
278 // 3. upper: color, lower: reset => '▀' fg: upper color / bg: reset
279 // 4. upper: color, lower: color => '▀' fg: upper color / bg: lower color
280 //
281 // Note that because the foreground reset color (i.e. default foreground color) is usually
282 // not the same as the background reset color (i.e. default background color), we need to
283 // swap around the colors for that state (2 reset/color).
284 //
285 // When the upper and lower colors are the same, we could continue to use an upper half
286 // block, but we choose to use a full block instead. This allows us to write unit tests that
287 // treat the cell as a single character instead of two half block characters.
288
289 // first we join each adjacent row together to get an iterator that contains vertical pairs
290 // of pixels, with the lower row being the first element in the pair
291 let vertical_color_pairs = self
292 .pixels
293 .iter()
294 .tuples()
295 .flat_map(|(upper_row, lower_row)| zip(upper_row, lower_row));
296
297 // then we work out what character to print for each pair of pixels
298 let string = vertical_color_pairs
299 .clone()
300 .map(|(upper, lower)| match (upper, lower) {
301 (Color::Reset, Color::Reset) => ' ',
302 (Color::Reset, _) => symbols::half_block::LOWER,
303 (_, Color::Reset) => symbols::half_block::UPPER,
304 (&lower, &upper) => {
305 if lower == upper {
306 symbols::half_block::FULL
307 } else {
308 symbols::half_block::UPPER
309 }
310 }
311 })
312 .collect();
313
314 // then we convert these each vertical pair of pixels into a foreground and background color
315 let colors = vertical_color_pairs
316 .map(|(upper, lower)| {
317 let (fg, bg) = match (upper, lower) {
318 (Color::Reset, Color::Reset) => (Color::Reset, Color::Reset),
319 (Color::Reset, &lower) => (lower, Color::Reset),
320 (&upper, Color::Reset) => (upper, Color::Reset),
321 (&upper, &lower) => (upper, lower),
322 };
323 (fg, bg)
324 })
325 .collect();
326
327 Layer { string, colors }
328 }
329
330 fn reset(&mut self) {
331 self.pixels.fill(vec![Color::Reset; self.width as usize]);
332 }
333
334 fn paint(&mut self, x: usize, y: usize, color: Color) {
335 self.pixels[y][x] = color;
336 }
337}
338
339/// Painter is an abstraction over the [`Context`] that allows to draw shapes on the grid.
340///
341/// It is used by the [`Shape`] trait to draw shapes on the grid. It can be useful to think of this
342/// as similar to the [`Buffer`] struct that is used to draw widgets on the terminal.
343#[derive(Debug)]
344pub struct Painter<'a, 'b> {
345 context: &'a mut Context<'b>,
346 resolution: (f64, f64),
347}
348
349impl<'a, 'b> Painter<'a, 'b> {
350 /// Convert the `(x, y)` coordinates to location of a point on the grid
351 ///
352 /// `(x, y)` coordinates are expressed in the coordinate system of the canvas. The origin is in
353 /// the lower left corner of the canvas (unlike most other coordinates in `Ratatui` where the
354 /// origin is the upper left corner). The `x` and `y` bounds of the canvas define the specific
355 /// area of some coordinate system that will be drawn on the canvas. The resolution of the grid
356 /// is used to convert the `(x, y)` coordinates to the location of a point on the grid.
357 ///
358 /// The grid coordinates are expressed in the coordinate system of the grid. The origin is in
359 /// the top left corner of the grid. The x and y bounds of the grid are always `[0, width - 1]`
360 /// and `[0, height - 1]` respectively. The resolution of the grid is used to convert the
361 /// `(x, y)` coordinates to the location of a point on the grid.
362 ///
363 /// # Examples
364 ///
365 /// ```
366 /// use ratatui::{
367 /// symbols,
368 /// widgets::canvas::{Context, Painter},
369 /// };
370 ///
371 /// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
372 /// let mut painter = Painter::from(&mut ctx);
373 ///
374 /// let point = painter.get_point(1.0, 0.0);
375 /// assert_eq!(point, Some((0, 7)));
376 ///
377 /// let point = painter.get_point(1.5, 1.0);
378 /// assert_eq!(point, Some((1, 3)));
379 ///
380 /// let point = painter.get_point(0.0, 0.0);
381 /// assert_eq!(point, None);
382 ///
383 /// let point = painter.get_point(2.0, 2.0);
384 /// assert_eq!(point, Some((3, 0)));
385 ///
386 /// let point = painter.get_point(1.0, 2.0);
387 /// assert_eq!(point, Some((0, 0)));
388 /// ```
389 pub fn get_point(&self, x: f64, y: f64) -> Option<(usize, usize)> {
390 let left = self.context.x_bounds[0];
391 let right = self.context.x_bounds[1];
392 let top = self.context.y_bounds[1];
393 let bottom = self.context.y_bounds[0];
394 if x < left || x > right || y < bottom || y > top {
395 return None;
396 }
397 let width = (self.context.x_bounds[1] - self.context.x_bounds[0]).abs();
398 let height = (self.context.y_bounds[1] - self.context.y_bounds[0]).abs();
399 if width == 0.0 || height == 0.0 {
400 return None;
401 }
402 let x = ((x - left) * (self.resolution.0 - 1.0) / width) as usize;
403 let y = ((top - y) * (self.resolution.1 - 1.0) / height) as usize;
404 Some((x, y))
405 }
406
407 /// Paint a point of the grid
408 ///
409 /// # Example
410 ///
411 /// ```
412 /// use ratatui::{
413 /// style::Color,
414 /// symbols,
415 /// widgets::canvas::{Context, Painter},
416 /// };
417 ///
418 /// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
419 /// let mut painter = Painter::from(&mut ctx);
420 /// painter.paint(1, 3, Color::Red);
421 /// ```
422 pub fn paint(&mut self, x: usize, y: usize, color: Color) {
423 self.context.grid.paint(x, y, color);
424 }
425}
426
427impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
428 fn from(context: &'a mut Context<'b>) -> Self {
429 let resolution = context.grid.resolution();
430 Self {
431 context,
432 resolution,
433 }
434 }
435}
436
437/// Holds the state of the [`Canvas`] when painting to it.
438///
439/// This is used by the [`Canvas`] widget to draw shapes on the grid. It can be useful to think of
440/// this as similar to the [`Frame`] struct that is used to draw widgets on the terminal.
441///
442/// [`Frame`]: crate::Frame
443#[derive(Debug)]
444pub struct Context<'a> {
445 x_bounds: [f64; 2],
446 y_bounds: [f64; 2],
447 grid: Box<dyn Grid>,
448 dirty: bool,
449 layers: Vec<Layer>,
450 labels: Vec<Label<'a>>,
451}
452
453impl<'a> Context<'a> {
454 /// Create a new Context with the given width and height measured in terminal columns and rows
455 /// respectively. The `x` and `y` bounds define the specific area of some coordinate system that
456 /// will be drawn on the canvas. The marker defines the type of points used to draw the shapes.
457 ///
458 /// Applications should not use this directly but rather use the [`Canvas`] widget. This will be
459 /// created by the [`Canvas::paint`] method and passed to the closure that is used to draw on
460 /// the canvas.
461 ///
462 /// The `x` and `y` bounds should be specified as left/right and bottom/top respectively. For
463 /// example, if you want to draw a map of the world, you might want to use the following bounds:
464 ///
465 /// ```
466 /// use ratatui::{symbols, widgets::canvas::Context};
467 ///
468 /// let ctx = Context::new(
469 /// 100,
470 /// 100,
471 /// [-180.0, 180.0],
472 /// [-90.0, 90.0],
473 /// symbols::Marker::Braille,
474 /// );
475 /// ```
476 pub fn new(
477 width: u16,
478 height: u16,
479 x_bounds: [f64; 2],
480 y_bounds: [f64; 2],
481 marker: Marker,
482 ) -> Self {
483 let dot = symbols::DOT.chars().next().unwrap();
484 let block = symbols::block::FULL.chars().next().unwrap();
485 let bar = symbols::bar::HALF.chars().next().unwrap();
486 let grid: Box<dyn Grid> = match marker {
487 Marker::Dot => Box::new(CharGrid::new(width, height, dot)),
488 Marker::Block => Box::new(CharGrid::new(width, height, block)),
489 Marker::Bar => Box::new(CharGrid::new(width, height, bar)),
490 Marker::Braille => Box::new(BrailleGrid::new(width, height)),
491 Marker::HalfBlock => Box::new(HalfBlockGrid::new(width, height)),
492 };
493 Self {
494 x_bounds,
495 y_bounds,
496 grid,
497 dirty: false,
498 layers: Vec::new(),
499 labels: Vec::new(),
500 }
501 }
502
503 /// Draw the given [`Shape`] in this context
504 pub fn draw<S>(&mut self, shape: &S)
505 where
506 S: Shape,
507 {
508 self.dirty = true;
509 let mut painter = Painter::from(self);
510 shape.draw(&mut painter);
511 }
512
513 /// Save the existing state of the grid as a layer.
514 ///
515 /// Save the existing state as a layer to be rendered and reset the grid to its initial
516 /// state for the next layer.
517 ///
518 /// This allows the canvas to be drawn in multiple layers. This is useful if you want to
519 /// draw multiple shapes on the [`Canvas`] in specific order.
520 pub fn layer(&mut self) {
521 self.layers.push(self.grid.save());
522 self.grid.reset();
523 self.dirty = false;
524 }
525
526 /// Print a [`Text`] on the [`Canvas`] at the given position.
527 ///
528 /// Note that the text is always printed on top of the canvas and is **not** affected by the
529 /// layers.
530 ///
531 /// [`Text`]: crate::text::Text
532 pub fn print<T>(&mut self, x: f64, y: f64, line: T)
533 where
534 T: Into<TextLine<'a>>,
535 {
536 self.labels.push(Label {
537 x,
538 y,
539 line: line.into(),
540 });
541 }
542
543 /// Save the last layer if necessary
544 fn finish(&mut self) {
545 if self.dirty {
546 self.layer();
547 }
548 }
549}
550
551/// The Canvas widget provides a means to draw shapes (Lines, Rectangles, Circles, etc.) on a grid.
552///
553/// By default the grid is made of Braille patterns but you may change the marker to use a different
554/// set of symbols. If your terminal or font does not support this unicode block, you will see
555/// unicode replacement characters (�) instead of braille dots. The Braille patterns provide a more
556/// fine grained result (2x4 dots) but you might want to use a simple dot, block, or bar instead by
557/// calling the [`marker`] method if your target environment does not support those symbols,
558///
559/// See [Unicode Braille Patterns](https://en.wikipedia.org/wiki/Braille_Patterns) for more info.
560///
561/// The `HalfBlock` marker is useful when you want to draw shapes with a higher resolution than a
562/// `CharGrid` but lower than a `BrailleGrid`. This grid type supports a foreground and background
563/// color for each terminal cell. This allows for more flexibility than the `BrailleGrid` which only
564/// supports a single foreground color for each 2x4 dots cell.
565///
566/// The Canvas widget is used by calling the [`Canvas::paint`] method and passing a closure that
567/// will be used to draw on the canvas. The closure will be passed a [`Context`] object that can be
568/// used to draw shapes on the canvas.
569///
570/// The [`Context`] object provides a [`Context::draw`] method that can be used to draw shapes on
571/// the canvas. The [`Context::layer`] method can be used to save the current state of the canvas
572/// and start a new layer. This is useful if you want to draw multiple shapes on the canvas in
573/// specific order. The [`Context`] object also provides a [`Context::print`] method that can be
574/// used to print text on the canvas. Note that the text is always printed on top of the canvas and
575/// is not affected by the layers.
576///
577/// # Examples
578///
579/// ```
580/// use ratatui::{
581/// style::Color,
582/// widgets::{
583/// canvas::{Canvas, Line, Map, MapResolution, Rectangle},
584/// Block,
585/// },
586/// };
587///
588/// Canvas::default()
589/// .block(Block::bordered().title("Canvas"))
590/// .x_bounds([-180.0, 180.0])
591/// .y_bounds([-90.0, 90.0])
592/// .paint(|ctx| {
593/// ctx.draw(&Map {
594/// resolution: MapResolution::High,
595/// color: Color::White,
596/// });
597/// ctx.layer();
598/// ctx.draw(&Line {
599/// x1: 0.0,
600/// y1: 10.0,
601/// x2: 10.0,
602/// y2: 10.0,
603/// color: Color::White,
604/// });
605/// ctx.draw(&Rectangle {
606/// x: 10.0,
607/// y: 20.0,
608/// width: 10.0,
609/// height: 10.0,
610/// color: Color::Red,
611/// });
612/// });
613/// ```
614///
615/// [`marker`]: #method.marker
616#[derive(Debug, Clone, PartialEq)]
617pub struct Canvas<'a, F>
618where
619 F: Fn(&mut Context),
620{
621 block: Option<Block<'a>>,
622 x_bounds: [f64; 2],
623 y_bounds: [f64; 2],
624 paint_func: Option<F>,
625 background_color: Color,
626 marker: Marker,
627}
628
629impl<'a, F> Default for Canvas<'a, F>
630where
631 F: Fn(&mut Context),
632{
633 fn default() -> Self {
634 Self {
635 block: None,
636 x_bounds: [0.0, 0.0],
637 y_bounds: [0.0, 0.0],
638 paint_func: None,
639 background_color: Color::Reset,
640 marker: Marker::Braille,
641 }
642 }
643}
644
645impl<'a, F> Canvas<'a, F>
646where
647 F: Fn(&mut Context),
648{
649 /// Wraps the canvas with a custom [`Block`] widget.
650 ///
651 /// This is a fluent setter method which must be chained or used as it consumes self
652 #[must_use = "method moves the value of self and returns the modified value"]
653 pub fn block(mut self, block: Block<'a>) -> Self {
654 self.block = Some(block);
655 self
656 }
657
658 /// Define the viewport of the canvas.
659 ///
660 /// If you were to "zoom" to a certain part of the world you may want to choose different
661 /// bounds.
662 ///
663 /// This is a fluent setter method which must be chained or used as it consumes self
664 #[must_use = "method moves the value of self and returns the modified value"]
665 pub const fn x_bounds(mut self, bounds: [f64; 2]) -> Self {
666 self.x_bounds = bounds;
667 self
668 }
669
670 /// Define the viewport of the canvas.
671 ///
672 /// If you were to "zoom" to a certain part of the world you may want to choose different
673 /// bounds.
674 ///
675 /// This is a fluent setter method which must be chained or used as it consumes self
676 #[must_use = "method moves the value of self and returns the modified value"]
677 pub const fn y_bounds(mut self, bounds: [f64; 2]) -> Self {
678 self.y_bounds = bounds;
679 self
680 }
681
682 /// Store the closure that will be used to draw to the [`Canvas`]
683 ///
684 /// This is a fluent setter method which must be chained or used as it consumes self
685 #[must_use = "method moves the value of self and returns the modified value"]
686 pub fn paint(mut self, f: F) -> Self {
687 self.paint_func = Some(f);
688 self
689 }
690
691 /// Change the background [`Color`] of the entire canvas
692 ///
693 /// This is a fluent setter method which must be chained or used as it consumes self
694 #[must_use = "method moves the value of self and returns the modified value"]
695 pub const fn background_color(mut self, color: Color) -> Self {
696 self.background_color = color;
697 self
698 }
699
700 /// Change the type of points used to draw the shapes.
701 ///
702 /// By default the [`Braille`] patterns are used as they provide a more fine grained result,
703 /// but you might want to use the simple [`Dot`] or [`Block`] instead if the targeted terminal
704 /// does not support those symbols.
705 ///
706 /// The [`HalfBlock`] marker is useful when you want to draw shapes with a higher resolution
707 /// than with a grid of characters (e.g. with [`Block`] or [`Dot`]) but lower than with
708 /// [`Braille`]. This grid type supports a foreground and background color for each terminal
709 /// cell. This allows for more flexibility than the `BrailleGrid` which only supports a single
710 /// foreground color for each 2x4 dots cell.
711 ///
712 /// [`Braille`]: crate::symbols::Marker::Braille
713 /// [`HalfBlock`]: crate::symbols::Marker::HalfBlock
714 /// [`Dot`]: crate::symbols::Marker::Dot
715 /// [`Block`]: crate::symbols::Marker::Block
716 ///
717 /// # Examples
718 ///
719 /// ```
720 /// use ratatui::{symbols, widgets::canvas::Canvas};
721 ///
722 /// Canvas::default()
723 /// .marker(symbols::Marker::Braille)
724 /// .paint(|ctx| {});
725 ///
726 /// Canvas::default()
727 /// .marker(symbols::Marker::HalfBlock)
728 /// .paint(|ctx| {});
729 ///
730 /// Canvas::default()
731 /// .marker(symbols::Marker::Dot)
732 /// .paint(|ctx| {});
733 ///
734 /// Canvas::default()
735 /// .marker(symbols::Marker::Block)
736 /// .paint(|ctx| {});
737 /// ```
738 #[must_use = "method moves the value of self and returns the modified value"]
739 pub const fn marker(mut self, marker: Marker) -> Self {
740 self.marker = marker;
741 self
742 }
743}
744
745impl<F> Widget for Canvas<'_, F>
746where
747 F: Fn(&mut Context),
748{
749 fn render(self, area: Rect, buf: &mut Buffer) {
750 self.render_ref(area, buf);
751 }
752}
753
754impl<F> WidgetRef for Canvas<'_, F>
755where
756 F: Fn(&mut Context),
757{
758 fn render_ref(&self, area: Rect, buf: &mut Buffer) {
759 self.block.render_ref(area, buf);
760 let canvas_area = self.block.inner_if_some(area);
761 if canvas_area.is_empty() {
762 return;
763 }
764
765 buf.set_style(canvas_area, Style::default().bg(self.background_color));
766
767 let width = canvas_area.width as usize;
768
769 let Some(ref painter) = self.paint_func else {
770 return;
771 };
772
773 // Create a blank context that match the size of the canvas
774 let mut ctx = Context::new(
775 canvas_area.width,
776 canvas_area.height,
777 self.x_bounds,
778 self.y_bounds,
779 self.marker,
780 );
781 // Paint to this context
782 painter(&mut ctx);
783 ctx.finish();
784
785 // Retrieve painted points for each layer
786 for layer in ctx.layers {
787 for (index, (ch, colors)) in layer.string.chars().zip(layer.colors).enumerate() {
788 if ch != ' ' && ch != '\u{2800}' {
789 let (x, y) = (
790 (index % width) as u16 + canvas_area.left(),
791 (index / width) as u16 + canvas_area.top(),
792 );
793 let cell = buf[(x, y)].set_char(ch);
794 if colors.0 != Color::Reset {
795 cell.set_fg(colors.0);
796 }
797 if colors.1 != Color::Reset {
798 cell.set_bg(colors.1);
799 }
800 }
801 }
802 }
803
804 // Finally draw the labels
805 let left = self.x_bounds[0];
806 let right = self.x_bounds[1];
807 let top = self.y_bounds[1];
808 let bottom = self.y_bounds[0];
809 let width = (self.x_bounds[1] - self.x_bounds[0]).abs();
810 let height = (self.y_bounds[1] - self.y_bounds[0]).abs();
811 let resolution = {
812 let width = f64::from(canvas_area.width - 1);
813 let height = f64::from(canvas_area.height - 1);
814 (width, height)
815 };
816 for label in ctx
817 .labels
818 .iter()
819 .filter(|l| l.x >= left && l.x <= right && l.y <= top && l.y >= bottom)
820 {
821 let x = ((label.x - left) * resolution.0 / width) as u16 + canvas_area.left();
822 let y = ((top - label.y) * resolution.1 / height) as u16 + canvas_area.top();
823 buf.set_line(x, y, &label.line, canvas_area.right() - x);
824 }
825 }
826}
827
828#[cfg(test)]
829mod tests {
830 use indoc::indoc;
831
832 use super::*;
833 use crate::buffer::Cell;
834
835 // helper to test the canvas checks that drawing a vertical and horizontal line
836 // results in the expected output
837 fn test_marker(marker: Marker, expected: &str) {
838 let area = Rect::new(0, 0, 5, 5);
839 let mut buf = Buffer::filled(area, Cell::new("x"));
840 let horizontal_line = Line {
841 x1: 0.0,
842 y1: 0.0,
843 x2: 10.0,
844 y2: 0.0,
845 color: Color::Reset,
846 };
847 let vertical_line = Line {
848 x1: 0.0,
849 y1: 0.0,
850 x2: 0.0,
851 y2: 10.0,
852 color: Color::Reset,
853 };
854 Canvas::default()
855 .marker(marker)
856 .paint(|ctx| {
857 ctx.draw(&vertical_line);
858 ctx.draw(&horizontal_line);
859 })
860 .x_bounds([0.0, 10.0])
861 .y_bounds([0.0, 10.0])
862 .render(area, &mut buf);
863 assert_eq!(buf, Buffer::with_lines(expected.lines()));
864 }
865
866 #[test]
867 fn test_bar_marker() {
868 test_marker(
869 Marker::Bar,
870 indoc!(
871 "
872 ▄xxxx
873 ▄xxxx
874 ▄xxxx
875 ▄xxxx
876 ▄▄▄▄▄"
877 ),
878 );
879 }
880
881 #[test]
882 fn test_block_marker() {
883 test_marker(
884 Marker::Block,
885 indoc!(
886 "
887 █xxxx
888 █xxxx
889 █xxxx
890 █xxxx
891 █████"
892 ),
893 );
894 }
895
896 #[test]
897 fn test_braille_marker() {
898 test_marker(
899 Marker::Braille,
900 indoc!(
901 "
902 ⡇xxxx
903 ⡇xxxx
904 ⡇xxxx
905 ⡇xxxx
906 ⣇⣀⣀⣀⣀"
907 ),
908 );
909 }
910
911 #[test]
912 fn test_dot_marker() {
913 test_marker(
914 Marker::Dot,
915 indoc!(
916 "
917 •xxxx
918 •xxxx
919 •xxxx
920 •xxxx
921 •••••"
922 ),
923 );
924 }
925}