1use rich::cells::char_cell_width;
7use rich::measure::Measurement;
8use rich::{Console, ConsoleOptions, Renderable, Segment, Style};
9
10use super::canvas::{bresenham, octant, quadrant, sextant};
11use super::world::MapResolution;
12use super::{
13 Charset, DotCanvas, Line, cell_units, cells, draw_grid, lines_to_segments, theme_style,
14 user_style,
15};
16use crate::fidelity::ascii_text;
17use crate::frame::{StyleId, StyleTable};
18use crate::surface::{Cell, Draw, Rect, Screen, TextRun};
19
20const DEFAULT_HEIGHT: usize = 10;
22const MAX_SIDE: usize = u16::MAX as usize;
24
25#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
39pub enum Marker {
40 #[default]
42 Braille,
43 HalfBlock,
46 Block,
48 Bar,
50 Quadrant,
52 Sextant,
54 Octant,
57 Dot,
59 Ascii(char),
62}
63
64impl Marker {
65 pub(crate) fn resolve(self, ascii: bool) -> Marker {
67 match self {
68 Marker::Ascii(c)
69 if char_cell_width(c) == 1 && (!ascii || c.is_ascii_graphic() || c == ' ') =>
70 {
71 self
72 }
73 Marker::Ascii(_) => Marker::Ascii('*'),
74 _ if !ascii => self,
75 Marker::Block
76 | Marker::HalfBlock
77 | Marker::Bar
78 | Marker::Quadrant
79 | Marker::Sextant
80 | Marker::Octant => Marker::Ascii('#'),
81 _ => Marker::Ascii('*'),
82 }
83 }
84
85 pub(crate) fn per_cell(self) -> (usize, usize) {
87 match self {
88 Marker::Braille | Marker::Octant => (2, 4),
89 Marker::Sextant => (2, 3),
90 Marker::Quadrant => (2, 2),
91 Marker::HalfBlock => (1, 2),
92 _ => (1, 1),
93 }
94 }
95
96 fn pattern(self, bits: u8) -> char {
99 match self {
100 Marker::Quadrant => quadrant(bits),
101 Marker::Sextant => sextant(bits),
102 _ => octant(bits),
103 }
104 }
105}
106
107pub(crate) type Spec = std::sync::Arc<str>;
110
111#[derive(Clone, Debug, PartialEq)]
114pub(crate) enum Op {
115 Points(Vec<(f64, f64)>, Spec),
116 Line((f64, f64), (f64, f64), Spec),
117 Area((f64, f64), (f64, f64), f64, Spec),
119 Circle((f64, f64), f64, Spec),
120 Map(MapResolution, Spec),
121 Print((f64, f64), String, Spec),
122 Layer,
123 Marker(Marker),
124}
125
126pub trait Shape {
159 fn draw(&self, painter: &mut CanvasPainter);
161}
162
163#[derive(Clone, Debug, Default, PartialEq)]
170pub struct CanvasPainter {
171 ops: Vec<Op>,
172}
173
174impl CanvasPainter {
175 pub fn points(&mut self, points: &[(f64, f64)], style: &str) -> &mut Self {
177 self.ops.push(Op::Points(points.to_vec(), style.into()));
178 self
179 }
180
181 pub fn point(&mut self, x: f64, y: f64, style: &str) -> &mut Self {
183 self.points(&[(x, y)], style)
184 }
185
186 pub fn shape(&mut self, shape: &dyn Shape) -> &mut Self {
188 shape.draw(self);
189 self
190 }
191
192 pub fn map(&mut self, resolution: MapResolution, style: &str) -> &mut Self {
197 self.ops.push(Op::Map(resolution, style.into()));
198 self
199 }
200
201 pub fn area(
205 &mut self,
206 x1: f64,
207 y1: f64,
208 x2: f64,
209 y2: f64,
210 fill_to: f64,
211 style: &str,
212 ) -> &mut Self {
213 self.ops
214 .push(Op::Area((x1, y1), (x2, y2), fill_to, style.into()));
215 self
216 }
217
218 pub fn line(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, style: &str) -> &mut Self {
220 self.ops.push(Op::Line((x1, y1), (x2, y2), style.into()));
221 self
222 }
223
224 pub fn polyline(&mut self, points: &[(f64, f64)], style: &str) -> &mut Self {
227 let finite = |p: &(f64, f64)| p.0.is_finite() && p.1.is_finite();
228 for run in points.split(|p| !finite(p)) {
229 match run {
230 [] => {}
231 [p] => self.ops.push(Op::Points(vec![*p], style.into())),
232 _ => {
233 for pair in run.windows(2) {
234 self.ops.push(Op::Line(pair[0], pair[1], style.into()));
235 }
236 }
237 }
238 }
239 self
240 }
241
242 pub fn rect(&mut self, x: f64, y: f64, width: f64, height: f64, style: &str) -> &mut Self {
244 let (r, t) = (x + width, y + height);
245 self.line(x, y, r, y, style)
246 .line(x, t, r, t, style)
247 .line(x, y, x, t, style)
248 .line(r, y, r, t, style)
249 }
250
251 pub fn circle(&mut self, x: f64, y: f64, radius: f64, style: &str) -> &mut Self {
253 self.ops.push(Op::Circle((x, y), radius, style.into()));
254 self
255 }
256
257 pub fn print(&mut self, x: f64, y: f64, text: &str, style: &str) -> &mut Self {
260 self.ops
261 .push(Op::Print((x, y), text.to_string(), style.into()));
262 self
263 }
264
265 pub fn layer(&mut self) -> &mut Self {
269 self.ops.push(Op::Layer);
270 self
271 }
272
273 pub fn marker(&mut self, marker: Marker) -> &mut Self {
276 self.ops.push(Op::Marker(marker));
277 self
278 }
279}
280
281#[derive(Clone, Debug, PartialEq)]
334#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
335pub struct Canvas {
336 x_bounds: [f64; 2],
337 y_bounds: [f64; 2],
338 width: Option<usize>,
339 height: Option<usize>,
340 marker: Marker,
341 background: Option<String>,
342 ops: Vec<Op>,
343}
344
345impl Default for Canvas {
346 fn default() -> Self {
347 Self::new()
348 }
349}
350
351impl Canvas {
352 pub fn new() -> Self {
354 Canvas {
355 x_bounds: [0.0, 1.0],
356 y_bounds: [0.0, 1.0],
357 width: None,
358 height: None,
359 marker: Marker::Braille,
360 background: None,
361 ops: Vec::new(),
362 }
363 }
364
365 pub fn x_bounds(mut self, left: f64, right: f64) -> Self {
367 self.x_bounds = [left, right];
368 self
369 }
370
371 pub fn y_bounds(mut self, bottom: f64, top: f64) -> Self {
373 self.y_bounds = [bottom, top];
374 self
375 }
376
377 pub fn width(mut self, width: usize) -> Self {
379 self.width = Some(width.min(MAX_SIDE));
380 self
381 }
382
383 pub fn height(mut self, rows: usize) -> Self {
386 self.height = Some(rows.min(MAX_SIDE));
387 self
388 }
389
390 pub fn background(mut self, style: impl Into<String>) -> Self {
394 self.background = Some(style.into());
395 self
396 }
397
398 pub fn marker(mut self, marker: Marker) -> Self {
401 self.marker = marker;
402 self
403 }
404
405 pub fn paint(mut self, draw: impl FnOnce(&mut CanvasPainter)) -> Self {
408 let mut painter = CanvasPainter::default();
409 draw(&mut painter);
410 if !self.ops.is_empty() {
411 self.ops.push(Op::Layer);
412 }
413 self.ops.extend(painter.ops);
414 self
415 }
416
417 fn grid(&self, console: &Console, options: &ConsoleOptions) -> CellGrid {
418 let width = self
419 .width
420 .map_or(options.max_width, |w| w.min(options.max_width));
421 let view = View {
422 x: self.x_bounds,
423 y: self.y_bounds,
424 width,
425 height: self
426 .height
427 .or(options.height)
428 .unwrap_or(DEFAULT_HEIGHT)
429 .min(MAX_SIDE),
430 marker: self.marker,
431 ascii: is_ascii(console, options),
432 };
433 let mut grid = raster(&self.ops, &view, console);
434 if let Some(style) = self
435 .background
436 .as_deref()
437 .and_then(|spec| spec_style(console, spec))
438 {
439 grid.underlay(&background_style(style));
440 }
441 grid
442 }
443}
444
445fn background_style(style: Style) -> Style {
448 match (style.color(), style.bgcolor()) {
449 (Some(colour), None) => {
450 let colour = colour.clone();
451 style.without_color().with_bgcolor(colour)
452 }
453 _ => style,
454 }
455}
456
457impl Draw for Canvas {
458 fn draw(&self, console: &Console, options: &ConsoleOptions, screen: &mut Screen, rect: Rect) {
459 draw_grid(self.grid(console, options), options, screen, rect);
460 }
461}
462
463impl Renderable for Canvas {
464 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
465 lines_to_segments(self.grid(console, options).into_lines(), options.max_width)
466 }
467
468 fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
469 let max = self.width.unwrap_or(options.max_width);
470 Measurement::new(max.min(12), max)
471 .with_maximum(options.max_width)
472 .normalize()
473 }
474}
475
476impl crate::a11y::AccessibleText for Canvas {
480 fn accessible_text(&self, _width: usize) -> String {
481 use super::describe::counted;
482 let format = super::ValueFormat::Compact;
483 let axis = |[a, b]: [f64; 2]| super::describe::range(a.min(b), a.max(b), format);
484 let mut head = format!(
485 "Canvas, x {}, y {}",
486 axis(self.x_bounds),
487 axis(self.y_bounds)
488 );
489 let (mut points, mut lines, mut areas, mut circles) = (0, 0, 0, 0);
490 let mut map = false;
491 let mut text = Vec::new();
492 for op in &self.ops {
493 match op {
494 Op::Points(p, _) => points += p.len(),
495 Op::Line(..) => lines += 1,
496 Op::Area(..) => areas += 1,
497 Op::Circle(..) => circles += 1,
498 Op::Map(..) => map = true,
499 Op::Print(_, t, _) if !t.trim().is_empty() => text.push(t.trim().to_string()),
500 _ => {}
501 }
502 }
503 let mut drawn = Vec::new();
504 if map {
505 drawn.push("a world map".to_string());
506 }
507 for (n, one, many) in [
508 (points, "point", "points"),
509 (lines, "line", "lines"),
510 (areas, "area segment", "area segments"),
511 (circles, "circle", "circles"),
512 ] {
513 if n > 0 {
514 drawn.push(counted(n, one, many));
515 }
516 }
517 if drawn.is_empty() {
518 head.push_str(": empty");
519 } else {
520 head.push_str(&format!(": {}", drawn.join(", ")));
521 }
522 if !text.is_empty() {
523 head.push_str(&format!("\nText: {}", text.join(", ")));
524 }
525 head
526 }
527}
528
529pub(crate) fn is_ascii(console: &Console, options: &ConsoleOptions) -> bool {
531 Charset::Auto.resolve(console, options, Charset::Braille) == Charset::Ascii
532}
533
534pub(crate) fn spec_style(console: &Console, spec: &str) -> Option<Style> {
537 let spec = spec.trim();
538 if spec.is_empty() {
539 None
540 } else if spec.starts_with("chart.") {
541 Some(theme_style(console, spec))
542 } else {
543 Some(user_style(console, spec))
544 }
545}
546
547pub(crate) struct View {
549 pub(crate) x: [f64; 2],
550 pub(crate) y: [f64; 2],
551 pub(crate) width: usize,
552 pub(crate) height: usize,
553 pub(crate) marker: Marker,
554 pub(crate) ascii: bool,
555}
556
557#[derive(Clone, Copy)]
559struct Bounds {
560 left: f64,
561 right: f64,
562 bottom: f64,
563 top: f64,
564}
565
566impl Bounds {
567 fn new(x: [f64; 2], y: [f64; 2]) -> Option<Bounds> {
568 let b = Bounds {
569 left: x[0],
570 right: x[1],
571 bottom: y[0],
572 top: y[1],
573 };
574 let (w, h) = (b.right - b.left, b.top - b.bottom);
575 (w.is_finite() && h.is_finite() && w > 0.0 && h > 0.0).then_some(b)
576 }
577
578 fn contains(&self, (x, y): (f64, f64)) -> bool {
579 (self.left..=self.right).contains(&x) && (self.bottom..=self.top).contains(&y)
580 }
581
582 fn cell(&self, p: (f64, f64), columns: usize, rows: usize) -> Option<(usize, usize)> {
586 if !self.contains(p) || columns == 0 || rows == 0 {
587 return None;
588 }
589 let x = (p.0 - self.left) * (columns - 1) as f64 / (self.right - self.left);
590 let y = (self.top - p.1) * (rows - 1) as f64 / (self.top - self.bottom);
591 Some(((x as usize).min(columns - 1), (y as usize).min(rows - 1)))
592 }
593
594 fn clip(&self, a: (f64, f64), b: (f64, f64)) -> Option<((f64, f64), (f64, f64))> {
596 if ![a.0, a.1, b.0, b.1].iter().all(|v| v.is_finite()) {
597 return None;
598 }
599 let (dx, dy) = (b.0 - a.0, b.1 - a.1);
600 if dx.is_finite() && dy.is_finite() && self.contains(a) && self.contains(b) {
601 let end = (
606 (a.0 + dx).clamp(self.left, self.right),
607 (a.1 + dy).clamp(self.bottom, self.top),
608 );
609 return Some((a, end));
610 }
611 let (mut t0, mut t1) = (0.0f64, 1.0f64);
612 for (p, q) in [
613 (-dx, a.0 - self.left),
614 (dx, self.right - a.0),
615 (-dy, a.1 - self.bottom),
616 (dy, self.top - a.1),
617 ] {
618 if !(p.is_finite() && q.is_finite()) {
619 return None;
620 }
621 if p == 0.0 {
622 if q < 0.0 {
623 return None;
624 }
625 } else {
626 let r = q / p;
627 if p < 0.0 {
628 t0 = t0.max(r);
629 } else {
630 t1 = t1.min(r);
631 }
632 }
633 }
634 if t0 > t1 {
635 return None;
636 }
637 let at = |t: f64| {
638 (
639 (a.0 + t * dx).clamp(self.left, self.right),
640 (a.1 + t * dy).clamp(self.bottom, self.top),
641 )
642 };
643 Some((at(t0), at(t1)))
644 }
645}
646
647struct Styles<'a> {
649 console: &'a Console,
650 specs: Vec<String>,
651 resolved: Vec<Option<Style>>,
652}
653
654impl<'a> Styles<'a> {
655 fn new(console: &'a Console) -> Self {
656 Styles {
657 console,
658 specs: Vec::new(),
659 resolved: Vec::new(),
660 }
661 }
662
663 fn id(&mut self, spec: &str) -> usize {
664 if let Some(last) = self.specs.last()
666 && last == spec
667 {
668 return self.specs.len() - 1;
669 }
670 if let Some(i) = self.specs.iter().position(|s| s == spec) {
671 return i;
672 }
673 self.specs.push(spec.to_string());
674 self.resolved.push(spec_style(self.console, spec));
675 self.specs.len() - 1
676 }
677
678 fn get(&self, id: usize) -> Option<Style> {
679 self.resolved.get(id).cloned().flatten()
680 }
681}
682
683enum Pixels {
688 Braille(DotCanvas),
689 Pattern {
690 bits: Vec<u8>,
691 styles: Vec<Option<usize>>,
692 },
693 Grid(Vec<Option<usize>>),
694}
695
696struct Layer {
697 marker: Marker,
698 width: usize,
699 height: usize,
700 pixels: Pixels,
701}
702
703impl Layer {
704 fn new(marker: Marker, width: usize, height: usize) -> Layer {
705 let pixels = match marker {
706 Marker::Braille => Pixels::Braille(DotCanvas::new(width, height)),
707 Marker::Quadrant | Marker::Sextant | Marker::Octant => Pixels::Pattern {
708 bits: vec![0; width * height],
709 styles: vec![None; width * height],
710 },
711 _ => {
712 let (px, py) = marker.per_cell();
713 Pixels::Grid(vec![None; width * px * height * py])
714 }
715 };
716 Layer {
717 marker,
718 width,
719 height,
720 pixels,
721 }
722 }
723
724 fn resolution(&self) -> (usize, usize) {
726 let (px, py) = self.marker.per_cell();
727 (self.width * px, self.height * py)
728 }
729
730 fn set(&mut self, (x, y): (usize, usize), style: usize) {
731 let (columns, rows) = self.resolution();
732 if x >= columns || y >= rows {
733 return;
734 }
735 match &mut self.pixels {
736 Pixels::Braille(canvas) => canvas.set(x as i64, y as i64, style),
737 Pixels::Pattern { bits, styles } => {
738 let (px, py) = self.marker.per_cell();
739 let cell = (y / py) * self.width + x / px;
740 bits[cell] |= 1 << (x % px + px * (y % py));
741 styles[cell] = Some(style);
742 }
743 Pixels::Grid(grid) => grid[y * columns + x] = Some(style),
744 }
745 }
746
747 fn point(&mut self, bounds: &Bounds, p: (f64, f64), style: usize) {
748 let (columns, rows) = self.resolution();
749 if let Some(at) = bounds.cell(p, columns, rows) {
750 self.set(at, style);
751 }
752 }
753
754 fn line(&mut self, bounds: &Bounds, a: (f64, f64), b: (f64, f64), style: usize) {
755 let Some((a, b)) = bounds.clip(a, b) else {
756 return;
757 };
758 let (columns, rows) = self.resolution();
759 let (Some(a), Some(b)) = (bounds.cell(a, columns, rows), bounds.cell(b, columns, rows))
760 else {
761 return;
762 };
763 let to_i = |(x, y): (usize, usize)| (x as i64, y as i64);
764 match &mut self.pixels {
765 Pixels::Braille(canvas) => {
767 for (x, y) in bresenham(to_i(a), to_i(b)) {
768 canvas.set(x, y, style);
769 }
770 }
771 _ => {
772 for (x, y) in bresenham(to_i(a), to_i(b)) {
773 self.set((x as usize, y as usize), style);
774 }
775 }
776 }
777 }
778
779 fn area(&mut self, bounds: &Bounds, a: (f64, f64), b: (f64, f64), fill: f64, style: usize) {
782 if !fill.is_finite() {
783 return self.line(bounds, a, b, style);
784 }
785 let Some((a, b)) = bounds.clip(a, b) else {
786 return;
787 };
788 let (columns, rows) = self.resolution();
789 let fill = fill.clamp(bounds.bottom, bounds.top);
790 let (Some(start), Some(end), Some((_, level))) = (
791 bounds.cell(a, columns, rows),
792 bounds.cell(b, columns, rows),
793 bounds.cell((a.0, fill), columns, rows),
794 ) else {
795 return;
796 };
797 let to_i = |(x, y): (usize, usize)| (x as i64, y as i64);
798 for (x, y) in bresenham(to_i(start), to_i(end)) {
799 let (x, y) = (x as usize, y as usize);
800 for row in y.min(level)..=y.max(level) {
801 self.set((x, row), style);
802 }
803 }
804 }
805
806 fn finish(&self, out: &mut CellGrid, styles: &Styles, left: usize) {
809 let columns = self.resolution().0;
810 let ids: Vec<StyleId> = styles
812 .resolved
813 .iter()
814 .map(|style| out.intern(style.as_ref()))
815 .collect();
816 if let Pixels::Braille(canvas) = &self.pixels {
817 for row in 0..self.height {
818 for col in 0..self.width {
819 if !canvas.is_set(col, row) {
820 continue;
821 }
822 if let (c, Some(style)) = canvas.cell(col, row) {
823 out.set_char(row, left + col, c, ids[style]);
824 }
825 }
826 }
827 return;
828 }
829 for row in 0..self.height {
830 for col in 0..self.width {
831 let cell = match (&self.pixels, self.marker) {
832 (Pixels::Braille(_), _) => None,
833 (Pixels::Pattern { bits, styles: set }, marker) => {
834 let i = row * self.width + col;
835 (bits[i] != 0).then(|| {
836 let style = set[i].map_or(StyleId::NONE, |s| ids[s]);
837 (marker.pattern(bits[i]), style)
838 })
839 }
840 (Pixels::Grid(grid), Marker::HalfBlock) => {
841 let upper = grid[2 * row * columns + col];
842 let lower = grid[(2 * row + 1) * columns + col];
843 half_block(upper, lower, styles)
844 .map(|(c, style)| (c, out.intern(style.as_ref())))
845 }
846 (Pixels::Grid(grid), marker) => grid[row * columns + col].map(|s| {
847 let glyph = match marker {
848 Marker::Block => '█',
849 Marker::Bar => '▄',
850 Marker::Ascii(c) => c,
851 _ => '•',
852 };
853 (glyph, ids[s])
854 }),
855 };
856 if let Some((c, style)) = cell {
857 out.set_char(row, left + col, c, style);
858 }
859 }
860 }
861 }
862}
863
864fn half_block(
866 upper: Option<usize>,
867 lower: Option<usize>,
868 styles: &Styles,
869) -> Option<(char, Option<Style>)> {
870 match (upper, lower) {
871 (Some(a), Some(b)) if a == b => Some(('█', styles.get(a))),
872 (Some(a), Some(b)) => {
873 let style = match styles.get(b).as_ref().and_then(|s| s.color()) {
874 Some(colour) => Some(
875 styles
876 .get(a)
877 .unwrap_or_default()
878 .with_bgcolor(colour.clone()),
879 ),
880 None => styles.get(a),
881 };
882 Some(('▀', style))
883 }
884 (Some(a), None) => Some(('▀', styles.get(a))),
885 (None, Some(b)) => Some(('▄', styles.get(b))),
886 (None, None) => None,
887 }
888}
889
890pub(crate) fn raster(ops: &[Op], view: &View, console: &Console) -> CellGrid {
893 let (width, height) = (view.width.min(MAX_SIDE), view.height.min(MAX_SIDE));
894 let mut out = CellGrid::new(width, height);
895 raster_into(ops, view, console, &mut out, 0);
896 out
897}
898
899pub(crate) fn raster_into(
903 ops: &[Op],
904 view: &View,
905 console: &Console,
906 out: &mut CellGrid,
907 left: usize,
908) {
909 let (width, height) = (view.width.min(MAX_SIDE), view.height.min(MAX_SIDE));
910 let Some(bounds) = Bounds::new(view.x, view.y) else {
911 return;
912 };
913 if width == 0 || height == 0 {
914 return;
915 }
916 let mut styles = Styles::new(console);
917 let mut layer = Layer::new(view.marker.resolve(view.ascii), width, height);
918 let mut labels = Vec::new();
919 let mut last: Option<(&Spec, usize)> = None;
921 let mut id_of = |styles: &mut Styles, spec| match last {
922 Some((seen, id)) if std::sync::Arc::ptr_eq(seen, spec) => id,
923 _ => {
924 let id = styles.id(spec);
925 last = Some((spec, id));
926 id
927 }
928 };
929 for op in ops {
930 match op {
931 Op::Layer => {
932 layer.finish(out, &styles, left);
933 layer = Layer::new(layer.marker, width, height);
934 }
935 Op::Marker(marker) => {
936 let marker = marker.resolve(view.ascii);
937 if marker != layer.marker {
938 layer.finish(out, &styles, left);
939 layer = Layer::new(marker, width, height);
940 }
941 }
942 Op::Points(points, style) => {
943 let style = id_of(&mut styles, style);
944 for p in points {
945 layer.point(&bounds, *p, style);
946 }
947 }
948 Op::Line(a, b, style) => {
949 let style = id_of(&mut styles, style);
950 layer.line(&bounds, *a, *b, style);
951 }
952 Op::Area(a, b, fill, style) => {
953 let style = id_of(&mut styles, style);
954 layer.area(&bounds, *a, *b, *fill, style);
955 }
956 Op::Map(resolution, style) => {
957 let style = id_of(&mut styles, style);
958 for line in resolution.lines() {
959 for pair in line.windows(2) {
960 layer.line(&bounds, pair[0], pair[1], style);
961 }
962 }
963 }
964 Op::Circle((x, y), radius, style) => {
965 let style = id_of(&mut styles, style);
966 if radius.is_finite() {
967 for degree in 0..360 {
968 let (sin, cos) = f64::from(degree).to_radians().sin_cos();
969 layer.point(&bounds, (x + radius * cos, y + radius * sin), style);
970 }
971 }
972 }
973 Op::Print(at, text, style) => labels.push((*at, text, style)),
974 }
975 }
976 layer.finish(out, &styles, left);
977 for (at, text, style) in labels {
978 if let Some((col, row)) = bounds.cell(at, width, height) {
979 let text = if view.ascii && !text.is_ascii() {
980 ascii_text(text)
981 } else {
982 text.clone()
983 };
984 out.put_text(
985 row,
986 left + col,
987 &text,
988 spec_style(console, style),
989 left + width,
990 );
991 }
992 }
993}
994
995#[derive(Clone, Debug)]
1005pub(crate) struct CellGrid {
1006 width: usize,
1007 height: usize,
1008 cells: Vec<Option<Cell>>,
1009 styles: StyleTable,
1010}
1011
1012impl CellGrid {
1013 pub(crate) fn new(width: usize, height: usize) -> Self {
1014 CellGrid {
1015 width,
1016 height,
1017 cells: vec![None; width * height],
1018 styles: StyleTable::new(),
1019 }
1020 }
1021
1022 pub(crate) fn intern(&mut self, style: Option<&Style>) -> StyleId {
1024 self.styles.intern(style)
1025 }
1026
1027 fn style(&self, id: StyleId) -> Option<&Style> {
1029 self.styles.get(id)
1030 }
1031
1032 pub(crate) fn set(&mut self, row: usize, col: usize, text: String, style: Option<Style>) {
1035 let id = self.intern(style.as_ref());
1036 self.set_id(row, col, &text, id);
1037 }
1038
1039 pub(crate) fn set_id(&mut self, row: usize, col: usize, text: &str, style: StyleId) {
1041 if row >= self.height || col >= self.width {
1042 return;
1043 }
1044 let width = if text.len() == 1 {
1045 1
1046 } else {
1047 cells(text).min(u8::MAX as usize) as u8
1048 };
1049 self.put(row, col, Cell::new(text, width, style));
1050 }
1051
1052 pub(crate) fn set_char(&mut self, row: usize, col: usize, c: char, style: StyleId) {
1054 if row >= self.height || col >= self.width {
1055 return;
1056 }
1057 let width = char_cell_width(c).min(u8::MAX as usize) as u8;
1058 self.put(row, col, Cell::from_char(c, width, style));
1059 }
1060
1061 fn put(&mut self, row: usize, col: usize, cell: Cell) {
1062 let i = row * self.width + col;
1063 let blank = Some(Cell::from_char(' ', 1, StyleId::NONE));
1064 if !cell.is_empty_text()
1065 && col > 0
1066 && matches!(&self.cells[i], Some(c) if c.is_empty_text())
1067 {
1068 self.cells[i - 1] = blank;
1069 }
1070 if col + 1 < self.width && matches!(&self.cells[i], Some(c) if c.width > 1) {
1071 self.cells[i + 1] = blank;
1072 }
1073 self.cells[i] = Some(cell);
1074 }
1075
1076 pub(crate) fn put_text(
1079 &mut self,
1080 row: usize,
1081 col: usize,
1082 text: &str,
1083 style: Option<Style>,
1084 end: usize,
1085 ) {
1086 let id = self.intern(style.as_ref());
1087 let end = end.min(self.width);
1088 let units = cell_units(text);
1089 let mut at = col;
1090 let mut i = 0;
1091 while i < units.len() && at < end {
1092 let unit = &units[i];
1093 let wide = i + 1 < units.len() && units[i + 1].is_empty() && !unit.is_empty();
1094 if wide && at + 1 >= end {
1095 self.set_id(row, at, " ", id);
1096 break;
1097 }
1098 let unit = if unit.is_empty() && i == 0 { " " } else { unit };
1099 self.set_id(row, at, unit, id);
1100 at += 1;
1101 i += 1;
1102 }
1103 }
1104
1105 pub(crate) fn underlay(&mut self, style: &Style) {
1108 let under = self.intern(Some(style));
1109 let mut combined: Vec<Option<StyleId>> = vec![None; self.styles.len()];
1111 for at in 0..self.cells.len() {
1112 let cell = match self.cells[at] {
1113 None => Cell::new(" ", 1, under),
1114 Some(cell) => {
1115 let own = cell.style;
1116 let id = match combined.get(own.index()).copied().flatten() {
1117 Some(id) => id,
1118 None => {
1119 let id = match self.styles.get(own).cloned() {
1120 Some(own_style) => {
1121 self.styles.intern(Some(&style.combine(&own_style)))
1122 }
1123 None => under,
1124 };
1125 if combined.len() <= own.index() {
1126 combined.resize(own.index() + 1, None);
1127 }
1128 combined[own.index()] = Some(id);
1129 id
1130 }
1131 };
1132 let mut cell = cell;
1133 cell.style = id;
1134 cell
1135 }
1136 };
1137 self.cells[at] = Some(cell);
1138 }
1139 }
1140
1141 pub(crate) fn restyle(&mut self, row: usize, col: usize, style: Option<Style>) {
1144 if row < self.height && col < self.width {
1145 let id = self.intern(style.as_ref());
1146 if let Some(cell) = &mut self.cells[row * self.width + col] {
1147 cell.style = id;
1148 }
1149 }
1150 }
1151
1152 #[cfg(test)]
1154 pub(crate) fn row_string(&self, row: usize) -> String {
1155 (0..self.width)
1156 .map(|c| match &self.cells[row * self.width + c] {
1157 Some(cell) => cell.text().to_string(),
1158 None => " ".to_string(),
1159 })
1160 .collect()
1161 }
1162
1163 #[cfg(test)]
1165 pub(crate) fn style_at(&self, row: usize, col: usize) -> Option<Style> {
1166 self.cells[row * self.width + col]
1167 .as_ref()
1168 .and_then(|cell| self.style(cell.style).cloned())
1169 }
1170
1171 pub(crate) fn into_lines(self) -> Vec<Line> {
1173 let mut text = String::new();
1174 (0..self.height)
1175 .map(|row| {
1176 let mut line = Line::new();
1177 let cells = &self.cells[row * self.width..(row + 1) * self.width];
1178 let mut style = None;
1179 for cell in cells {
1180 if cell.is_some_and(|c| c.is_empty_text()) {
1181 continue;
1182 }
1183 let id = cell.map_or(StyleId::NONE, |c| c.style);
1184 if style != Some(id) {
1185 if let Some(last) = style {
1186 line.push(&text, self.style(last).cloned());
1187 }
1188 text.clear();
1189 style = Some(id);
1190 }
1191 match cell.map(|c| (c, c.single_char())) {
1192 None => text.push(' '),
1193 Some((_, Some(c))) => text.push(c),
1194 Some((c, None)) => text.push_str(c.text()),
1195 }
1196 }
1197 if let Some(last) = style {
1198 line.push(&text, self.style(last).cloned());
1199 }
1200 text.clear();
1201 line
1202 })
1203 .collect()
1204 }
1205
1206 pub(crate) fn draw(&self, screen: &mut Screen, rect: Rect, rows: usize) {
1212 let rows = self.height.min(rect.height as usize).min(rows);
1213 let mut direct = true;
1217 let screen_ids: Vec<StyleId> = (0..self.styles.len())
1218 .map(|i| match self.styles.get(StyleId::from_index(i)) {
1219 None => StyleId::NONE,
1220 Some(style) if style.is_null() => StyleId::NONE,
1221 Some(style) => {
1222 direct &= style.meta_ref().is_none();
1223 screen.intern(Some(style))
1224 }
1225 })
1226 .collect();
1227 if direct && screen.write_cell_grid(rect, self.width, rows, &self.cells, &screen_ids) {
1228 return;
1229 }
1230 let mut ids: Vec<Option<StyleId>> = vec![None; self.styles.len()];
1231 let mut text = String::with_capacity(rows * self.width);
1234 let mut spans: Vec<(usize, usize, StyleId)> = Vec::new();
1235 let mut ends = Vec::with_capacity(rows);
1236 for row in 0..rows {
1237 let cells = &self.cells[row * self.width..(row + 1) * self.width];
1238 let mut style = None;
1239 for cell in cells {
1240 let (piece, own) = match cell {
1241 Some(cell) => (cell.text(), cell.style),
1242 None => (" ", StyleId::NONE),
1243 };
1244 if piece.is_empty() {
1245 continue;
1246 }
1247 if style != Some(own) {
1248 style = Some(own);
1249 let id =
1250 *ids[own.index()].get_or_insert_with(|| screen.intern(self.style(own)));
1251 spans.push((text.len(), text.len(), id));
1252 }
1253 text.push_str(piece);
1254 if let Some(last) = spans.last_mut() {
1255 last.1 = text.len();
1256 }
1257 }
1258 ends.push(spans.len());
1259 }
1260 let runs: Vec<TextRun<'_>> = spans
1261 .iter()
1262 .map(|&(from, to, id)| TextRun::new(&text[from..to], id))
1263 .collect();
1264 let mut start = 0;
1265 let lines: Vec<&[TextRun<'_>]> = ends
1266 .iter()
1267 .map(|&end| {
1268 let line = &runs[start..end];
1269 start = end;
1270 line
1271 })
1272 .collect();
1273 screen.write_runs(rect, &lines);
1274 }
1275}
1276
1277#[cfg(test)]
1278mod tests {
1279 use super::*;
1280
1281 #[test]
1282 fn accessible_text_says_what_was_drawn() {
1283 use crate::a11y::AccessibleText;
1284 let canvas = Canvas::new()
1285 .x_bounds(-1.0, 1.0)
1286 .y_bounds(-1.0, 1.0)
1287 .paint(|p| {
1288 p.circle(0.0, 0.0, 0.8, "cyan");
1289 p.line(-1.0, -1.0, 1.0, 1.0, "red");
1290 p.points(&[(0.0, 0.0), (0.5, 0.5)], "");
1291 p.layer();
1292 p.print(-0.2, 0.0, "hi", "bold");
1293 });
1294 assert_eq!(
1295 canvas.accessible_text(80),
1296 "Canvas, x from -1 to 1, y from -1 to 1: 2 points, 1 line, 1 circle\nText: hi"
1297 );
1298 let world = Canvas::new()
1299 .x_bounds(-180.0, 180.0)
1300 .y_bounds(-90.0, 90.0)
1301 .paint(|p| {
1302 p.map(MapResolution::Low, "green");
1303 });
1304 assert_eq!(
1305 world.accessible_text(80),
1306 "Canvas, x from -180 to 180, y from -90 to 90: a world map"
1307 );
1308 assert_eq!(
1309 Canvas::new().accessible_text(80),
1310 "Canvas, x from 0 to 1, y from 0 to 1: empty"
1311 );
1312 }
1313
1314 fn console(width: usize) -> Console {
1315 Console::builder().width(width).color_system(None).build()
1316 }
1317
1318 fn grid(canvas: &Canvas, width: usize) -> CellGrid {
1319 let console = console(width);
1320 let options = console.options();
1321 canvas.grid(&console, &options)
1322 }
1323
1324 fn rows(canvas: &Canvas, width: usize) -> Vec<String> {
1325 let g = grid(canvas, width);
1326 (0..g.height).map(|r| g.row_string(r)).collect()
1327 }
1328
1329 fn ascii(c: char) -> Canvas {
1330 Canvas::new()
1331 .x_bounds(0.0, 9.0)
1332 .y_bounds(0.0, 4.0)
1333 .height(5)
1334 .width(10)
1335 .marker(Marker::Ascii(c))
1336 }
1337
1338 #[test]
1339 fn lines_crossing_the_bounds_are_clipped() {
1340 let b = Bounds::new([0.0, 10.0], [0.0, 10.0]).unwrap();
1341 assert_eq!(
1342 b.clip((-5.0, 5.0), (15.0, 5.0)),
1343 Some(((0.0, 5.0), (10.0, 5.0)))
1344 );
1345 assert_eq!(
1346 b.clip((5.0, -5.0), (5.0, 20.0)),
1347 Some(((5.0, 0.0), (5.0, 10.0)))
1348 );
1349 assert_eq!(
1350 b.clip((-10.0, -10.0), (20.0, 20.0)),
1351 Some(((0.0, 0.0), (10.0, 10.0)))
1352 );
1353 assert_eq!(b.clip((-5.0, 0.0), (-1.0, 10.0)), None);
1355 assert_eq!(b.clip((0.0, 11.0), (10.0, 11.0)), None);
1356 assert_eq!(b.clip((11.0, 0.0), (11.0, 10.0)), None);
1357 assert_eq!(b.clip((-5.0, 8.0), (2.0, 15.0)), None);
1359 assert_eq!(b.clip((f64::NAN, 0.0), (1.0, 1.0)), None);
1361 assert_eq!(b.clip((f64::INFINITY, 0.0), (1.0, 1.0)), None);
1362 }
1363
1364 #[test]
1365 fn horizontal_and_vertical_lines_cover_the_canvas() {
1366 let canvas = ascii('#').paint(|p| {
1367 p.line(-100.0, 2.0, 100.0, 2.0, "");
1368 p.line(4.0, -1e6, 4.0, 1e6, "");
1369 });
1370 assert_eq!(
1371 rows(&canvas, 10),
1372 vec![
1373 " # ",
1374 " # ",
1375 "##########",
1376 " # ",
1377 " # ",
1378 ]
1379 );
1380 let outside = ascii('#').paint(|p| {
1382 p.line(-1e300, -1e300, -1e299, 1e300, "");
1383 });
1384 assert!(rows(&outside, 10).iter().all(|r| r.trim().is_empty()));
1385 }
1386
1387 #[test]
1388 fn non_finite_points_are_skipped() {
1389 let canvas = ascii('o').paint(|p| {
1390 p.points(&[(f64::NAN, 1.0), (1.0, f64::INFINITY), (0.0, 0.0)], "");
1391 p.polyline(&[(9.0, 4.0), (f64::NAN, 0.0), (9.0, 0.0)], "");
1392 p.circle(5.0, 2.0, f64::NAN, "");
1393 p.print(f64::NAN, 1.0, "x", "");
1394 });
1395 assert_eq!(
1396 rows(&canvas, 10),
1397 vec![
1398 " o",
1399 " ",
1400 " ",
1401 " ",
1402 "o o",
1403 ]
1404 );
1405 }
1406
1407 #[test]
1408 fn later_layers_win_a_cell() {
1409 let canvas = Canvas::new()
1410 .x_bounds(0.0, 1.0)
1411 .y_bounds(0.0, 1.0)
1412 .height(1)
1413 .width(1)
1414 .paint(|p| {
1415 p.points(&[(0.0, 1.0)], "red");
1416 p.points(&[(1.0, 1.0)], "blue");
1417 });
1418 let g = grid(&canvas, 1);
1419 assert_eq!(g.row_string(0), "⠉");
1421 assert_eq!(g.style_at(0, 0), Style::parse("blue").ok());
1422
1423 let layered = canvas.clone().paint(|p| {
1424 p.points(&[(0.0, 0.0)], "green");
1425 });
1426 let g = grid(&layered, 1);
1427 assert_eq!(g.row_string(0), "⡀");
1428 assert_eq!(g.style_at(0, 0), Style::parse("green").ok());
1429
1430 let empty = canvas.paint(|p| {
1432 p.layer();
1433 });
1434 assert_eq!(grid(&empty, 1).row_string(0), "⠉");
1435 }
1436
1437 #[test]
1438 fn markers_draw_at_their_resolution() {
1439 let draw = |marker| {
1440 let c = Canvas::new()
1441 .x_bounds(0.0, 1.0)
1442 .y_bounds(0.0, 1.0)
1443 .height(1)
1444 .width(2)
1445 .marker(marker)
1446 .paint(|p| {
1447 p.points(&[(0.0, 1.0), (1.0, 0.0)], "red");
1448 });
1449 rows(&c, 2)[0].clone()
1450 };
1451 assert_eq!(draw(Marker::Braille), "⠁⢀");
1452 assert_eq!(draw(Marker::HalfBlock), "▀▄");
1453 assert_eq!(draw(Marker::Block), "██");
1454 assert_eq!(draw(Marker::Dot), "••");
1455 assert_eq!(draw(Marker::Ascii('x')), "xx");
1456 assert_eq!(draw(Marker::Ascii('日')), "**");
1457 }
1458
1459 #[test]
1460 fn half_blocks_of_two_colours_share_a_cell() {
1461 let canvas = Canvas::new()
1462 .height(1)
1463 .width(1)
1464 .marker(Marker::HalfBlock)
1465 .paint(|p| {
1466 p.points(&[(0.0, 1.0)], "red");
1467 p.points(&[(0.0, 0.0)], "blue");
1468 });
1469 let g = grid(&canvas, 1);
1470 assert_eq!(g.row_string(0), "▀");
1471 assert_eq!(g.style_at(0, 0), Style::parse("red on blue").ok());
1472 }
1473
1474 #[test]
1475 fn ascii_consoles_get_ascii_markers_and_text() {
1476 let console = Console::builder()
1477 .width(4)
1478 .color_system(None)
1479 .ascii_only(true)
1480 .build();
1481 let canvas = Canvas::new().height(1).paint(|p| {
1482 p.line(0.0, 0.5, 1.0, 0.5, "");
1483 p.print(0.0, 0.5, "日", "");
1484 });
1485 assert_eq!(console.render_export(&canvas), "??**\n");
1486 let blocks = canvas.marker(Marker::HalfBlock);
1487 assert_eq!(console.render_export(&blocks), "??##\n");
1488 }
1489
1490 #[test]
1491 fn text_is_drawn_last_and_cut_at_the_edge() {
1492 let canvas = ascii('#').paint(|p| {
1493 p.print(7.0, 4.0, "label", "");
1494 p.layer();
1495 p.line(0.0, 4.0, 9.0, 4.0, "");
1496 p.print(0.0, 0.0, "日本", "");
1497 p.print(9.0, 0.0, "日", "");
1498 });
1499 assert_eq!(
1500 rows(&canvas, 10),
1501 vec![
1502 "#######lab",
1503 " ",
1504 " ",
1505 " ",
1506 "日本 "
1507 ]
1508 );
1509 }
1510
1511 #[test]
1512 fn circles_and_rectangles() {
1513 let canvas = Canvas::new()
1514 .x_bounds(-2.0, 2.0)
1515 .y_bounds(-2.0, 2.0)
1516 .height(5)
1517 .width(5)
1518 .marker(Marker::Ascii('o'))
1519 .paint(|p| {
1520 p.circle(0.0, 0.0, 2.0, "");
1521 });
1522 let out = rows(&canvas, 5);
1523 assert_eq!(out[2], "o oo");
1525 assert!(out[0].contains('o') && out[4].contains('o'));
1526 }
1527
1528 #[test]
1529 fn every_marker_draws_a_diagonal() {
1530 let diagonal = |marker| {
1531 let canvas = Canvas::new().width(6).height(3).marker(marker).paint(|p| {
1532 p.line(0.0, 0.0, 1.0, 1.0, "");
1533 });
1534 rows(&canvas, 6)
1535 };
1536 let expect = |a: &str, b: &str| {
1537 vec![
1538 format!(" {a}{b}"),
1539 format!(" {a}{b} "),
1540 format!("{a}{b} "),
1541 ]
1542 };
1543 assert_eq!(diagonal(Marker::Braille), expect("⡠", "⠊"));
1544 assert_eq!(diagonal(Marker::HalfBlock), expect("▄", "▀"));
1545 assert_eq!(diagonal(Marker::Block), expect("█", "█"));
1546 assert_eq!(diagonal(Marker::Bar), expect("▄", "▄"));
1547 assert_eq!(diagonal(Marker::Quadrant), expect("▄", "▀"));
1548 assert_eq!(diagonal(Marker::Sextant), expect("\u{1FB16}", "\u{1FB05}"));
1551 assert_eq!(diagonal(Marker::Octant), expect("\u{1CD51}", "\u{1CD01}"));
1552 assert_eq!(diagonal(Marker::Dot), expect("•", "•"));
1553 let console = Console::builder()
1555 .width(6)
1556 .color_system(None)
1557 .ascii_only(true)
1558 .build();
1559 for marker in [
1560 Marker::Bar,
1561 Marker::Quadrant,
1562 Marker::Sextant,
1563 Marker::Octant,
1564 ] {
1565 let canvas = Canvas::new().width(6).height(1).marker(marker).paint(|p| {
1566 p.line(0.0, 0.5, 1.0, 0.5, "");
1567 });
1568 assert_eq!(console.render_export(&canvas), "######\n", "{marker:?}");
1569 }
1570 }
1571
1572 #[test]
1573 fn pattern_cells_take_the_last_style_drawn_in_them() {
1574 let canvas = Canvas::new()
1575 .width(1)
1576 .height(1)
1577 .marker(Marker::Quadrant)
1578 .paint(|p| {
1579 p.point(0.0, 1.0, "red");
1580 p.point(1.0, 0.0, "blue");
1581 });
1582 let g = grid(&canvas, 1);
1583 assert_eq!(g.row_string(0), "▚");
1584 assert_eq!(g.style_at(0, 0), Style::parse("blue").ok());
1585 }
1586
1587 fn near(rows: &[String], col: usize, row: usize) -> bool {
1589 (row.saturating_sub(1)..=row + 1).any(|r| {
1590 rows.get(r).is_some_and(|line| {
1591 line.chars()
1592 .enumerate()
1593 .any(|(c, ch)| c + 1 >= col && c <= col + 1 && ch != ' ')
1594 })
1595 })
1596 }
1597
1598 #[test]
1599 fn the_map_lights_known_coasts() {
1600 for resolution in [MapResolution::Low, MapResolution::High] {
1601 let world = Canvas::new()
1602 .x_bounds(-180.0, 180.0)
1603 .y_bounds(-90.0, 90.0)
1604 .width(80)
1605 .height(20)
1606 .paint(|p| {
1607 p.map(resolution, "green");
1608 });
1609 let out = rows(&world, 80);
1610 let cell = |lon: f64, lat: f64| {
1613 let x = ((lon + 180.0) * 159.0 / 360.0) as usize / 2;
1614 let y = ((90.0 - lat) * 79.0 / 180.0) as usize / 4;
1615 (x, y)
1616 };
1617 for (place, lon, lat) in [
1618 ("Sydney", 151.2, -33.9),
1619 ("Perth", 115.9, -31.9),
1620 ("Darwin", 130.8, -12.5),
1621 ("Cape Town", 18.4, -33.9),
1622 ("Lisbon", -9.1, 38.7),
1623 ("Rio de Janeiro", -43.2, -22.9),
1624 ("San Francisco", -122.4, 37.8),
1625 ] {
1626 let (x, y) = cell(lon, lat);
1627 assert!(near(&out, x, y), "{resolution:?}: {place} at {x}, {y}");
1628 }
1629 for (lon, lat) in [(-140.0, -10.0), (-140.0, 20.0), (134.0, -25.0)] {
1631 let (x, y) = cell(lon, lat);
1632 assert_eq!(
1633 out[y].chars().nth(x),
1634 Some(' '),
1635 "{resolution:?} {lon} {lat}"
1636 );
1637 }
1638 let g = grid(&world, 80);
1640 let (x, y) = cell(151.2, -33.9);
1641 let drawn = (y - 1..=y + 1)
1642 .flat_map(|r| (x - 1..=x + 1).map(move |c| (r, c)))
1643 .find(|&(r, c)| g.row_string(r).chars().nth(c) != Some(' '))
1644 .unwrap();
1645 assert_eq!(g.style_at(drawn.0, drawn.1), Style::parse("green").ok());
1646 }
1647 }
1648
1649 #[test]
1650 fn a_background_goes_under_everything() {
1651 let canvas = Canvas::new()
1652 .width(3)
1653 .height(1)
1654 .marker(Marker::Block)
1655 .background("navy_blue")
1656 .paint(|p| {
1657 p.point(0.0, 0.5, "red");
1658 });
1659 let g = grid(&canvas, 3);
1660 assert_eq!(g.row_string(0), "█ ");
1661 assert_eq!(g.style_at(0, 0), Style::parse("red on navy_blue").ok());
1662 assert_eq!(g.style_at(0, 1), Style::parse("on navy_blue").ok());
1663 let console = Console::builder()
1665 .width(3)
1666 .force_terminal(true)
1667 .color_system(Some(rich::ColorSystem::Standard))
1668 .no_color(false)
1669 .build();
1670 let canvas = canvas.background("on blue");
1671 assert_eq!(
1672 console.render_export(&canvas),
1673 "\u{1b}[31;44m█\u{1b}[0m\u{1b}[44m \u{1b}[0m\n"
1674 );
1675 }
1676
1677 #[test]
1678 fn areas_fill_down_to_their_level() {
1679 let canvas = Canvas::new()
1680 .x_bounds(0.0, 4.0)
1681 .y_bounds(0.0, 4.0)
1682 .width(5)
1683 .height(5)
1684 .marker(Marker::Ascii('#'))
1685 .paint(|p| {
1686 p.area(0.0, 1.0, 4.0, 3.0, 0.0, "");
1687 });
1688 assert_eq!(
1689 rows(&canvas, 5),
1690 vec![" ", " ##", " ####", "#####", "#####"]
1691 );
1692 let above = Canvas::new()
1694 .x_bounds(0.0, 4.0)
1695 .y_bounds(0.0, 4.0)
1696 .width(5)
1697 .height(5)
1698 .marker(Marker::Ascii('#'))
1699 .paint(|p| {
1700 p.area(0.0, 1.0, 4.0, 1.0, 3.0, "");
1701 });
1702 assert_eq!(
1703 rows(&above, 5),
1704 vec![" ", "#####", "#####", "#####", " "]
1705 );
1706 }
1707
1708 #[test]
1709 fn custom_shapes_draw_with_the_painter() {
1710 struct Square(f64);
1711 impl Shape for Square {
1712 fn draw(&self, painter: &mut CanvasPainter) {
1713 let s = self.0;
1714 painter
1715 .rect(-s, -s, 2.0 * s, 2.0 * s, "yellow")
1716 .point(0.0, 0.0, "red");
1717 }
1718 }
1719 let canvas = Canvas::new()
1720 .x_bounds(-2.0, 2.0)
1721 .y_bounds(-2.0, 2.0)
1722 .width(5)
1723 .height(5)
1724 .marker(Marker::Ascii('o'))
1725 .paint(|p| {
1726 p.shape(&Square(1.0));
1727 });
1728 assert_eq!(
1729 rows(&canvas, 5),
1730 vec![" ", " ooo ", " ooo ", " ooo ", " "]
1731 );
1732 let g = grid(&canvas, 5);
1733 assert_eq!(g.style_at(2, 2), Style::parse("red").ok());
1734 assert_eq!(g.style_at(1, 1), Style::parse("yellow").ok());
1735 }
1736
1737 #[test]
1738 fn a_canvas_fills_the_height_it_is_given() {
1739 let console = console(10);
1740 let canvas = Canvas::new().paint(|p| {
1741 p.line(0.0, 0.0, 1.0, 1.0, "");
1742 });
1743 let lines = console.render_lines(&canvas, &console.options().update_height(7), false);
1744 assert_eq!(lines.len(), 7);
1745 assert!(lines.iter().all(|l| !l.is_empty()));
1746 let short = canvas.clone().height(3);
1748 let lines = console.render_lines(&short, &console.options().update_height(7), false);
1749 assert_eq!(lines.iter().filter(|l| !l.is_empty()).count(), 3);
1750 assert_eq!(console.render_export(&canvas).lines().count(), 10);
1751 }
1752
1753 #[test]
1754 fn tiny_and_empty_canvases_do_not_panic() {
1755 let paint = |p: &mut CanvasPainter| {
1756 p.line(0.0, 0.0, 1.0, 1.0, "red");
1757 p.circle(0.5, 0.5, 0.5, "red");
1758 p.print(0.5, 0.5, "text", "");
1759 };
1760 for (w, h) in [(0, 0), (1, 0), (0, 1), (1, 1), (2, 1)] {
1761 for marker in [Marker::Braille, Marker::HalfBlock, Marker::Dot] {
1762 let c = Canvas::new().width(w).height(h).marker(marker).paint(paint);
1763 let console = console(10);
1764 let _ = console.render_export(&c);
1765 }
1766 }
1767 for (x, y) in [
1769 ([0.0, 0.0], [0.0, 1.0]),
1770 ([1.0, 0.0], [0.0, 1.0]),
1771 ([0.0, f64::INFINITY], [0.0, 1.0]),
1772 ([-f64::MAX, f64::MAX], [0.0, 1.0]),
1773 ] {
1774 let c = Canvas::new()
1775 .x_bounds(x[0], x[1])
1776 .y_bounds(y[0], y[1])
1777 .height(2)
1778 .paint(paint);
1779 assert!(rows(&c, 4).iter().all(|r| r.trim().is_empty()));
1780 }
1781 }
1782}