Skip to main content

rich_ext/chart/
plot_canvas.rs

1//! A canvas in world coordinates, as ratatui's `Canvas` widget: points,
2//! lines, rectangles, circles, filled areas, a world map, your own shapes
3//! and text, in layers, drawn with Braille dots, block patterns, half
4//! blocks or one glyph a cell.
5
6use 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
20/// Rows a [`Canvas`] takes unless [`Canvas::height`] says otherwise.
21const DEFAULT_HEIGHT: usize = 10;
22/// The longest side drawn, in cells.
23const MAX_SIDE: usize = u16::MAX as usize;
24
25/// How a [`Canvas`] layer, or a [`Dataset`](super::Dataset), marks what it
26/// draws.
27///
28/// On an ASCII-only console (see [`Charset`]) every marker but
29/// [`Ascii`](Self::Ascii) draws one glyph a cell: `#` for the blocks, `*`
30/// for the others.
31///
32/// [`Quadrant`](Self::Quadrant), [`Bar`](Self::Bar) and
33/// [`HalfBlock`](Self::HalfBlock) draw with Block Elements, which every
34/// terminal font has. The sextants are in Symbols for Legacy Computing
35/// (Unicode 13) and the octants in its supplement (Unicode 16): a font
36/// without them shows a box or a blank in each cell, so check the fonts
37/// your users have before choosing them.
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
39pub enum Marker {
40    /// Braille dots, 2×4 a cell.
41    #[default]
42    Braille,
43    /// Half blocks (`▀`, `▄`, `█`), 1×2 a cell. A cell whose halves differ
44    /// in colour shows the upper colour on the lower one.
45    HalfBlock,
46    /// A full block, `█`, a cell.
47    Block,
48    /// A lower half block, `▄`, a cell.
49    Bar,
50    /// Quarters of a cell (`▘`, `▚`, `▙`, ..), 2×2 a cell.
51    Quadrant,
52    /// Sixths of a cell (`🬀` to `🬻`), 2×3 a cell.
53    Sextant,
54    /// Eighths of a cell (U+1CD00 to U+1CDE5), 2×4 a cell: as fine as
55    /// Braille, and solid.
56    Octant,
57    /// A dot, `•`, a cell.
58    Dot,
59    /// The given character, a cell. One that is not one cell wide, or not
60    /// printable ASCII on an ASCII console, becomes `*`.
61    Ascii(char),
62}
63
64impl Marker {
65    /// The marker actually drawn, on an ASCII-only console or not.
66    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    /// Dots across and down one cell.
86    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    /// The glyph for a cell of a pattern marker, from its set dots (bit
97    /// `x + 2 * y`).
98    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
107/// A style spec an [`Op`] draws in: shared, so a chart's thousand line
108/// steps in one style hold one string, not a thousand.
109pub(crate) type Spec = std::sync::Arc<str>;
110
111/// One recorded drawing step, in world coordinates. Styles are specs:
112/// theme keys or definitions.
113#[derive(Clone, Debug, PartialEq)]
114pub(crate) enum Op {
115    Points(Vec<(f64, f64)>, Spec),
116    Line((f64, f64), (f64, f64), Spec),
117    /// A line, and the area between it and a level `y`.
118    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
126/// Something a [`CanvasPainter`] can draw, such as a wireframe, a sprite or
127/// a chart of your own. [`CanvasPainter::shape`] calls
128/// [`draw`](Self::draw), which draws with the painter's points, lines and
129/// text, in world coordinates.
130///
131/// ```
132/// use rich::Console;
133/// use rich_ext::chart::{Canvas, CanvasPainter, Marker, Shape};
134///
135/// /// A cross centred on a point.
136/// struct Cross(f64, f64);
137///
138/// impl Shape for Cross {
139///     fn draw(&self, painter: &mut CanvasPainter) {
140///         let Cross(x, y) = *self;
141///         painter.line(x - 1.0, y, x + 1.0, y, "red");
142///         painter.line(x, y - 1.0, x, y + 1.0, "red");
143///     }
144/// }
145///
146/// let canvas = Canvas::new()
147///     .x_bounds(0.0, 4.0)
148///     .y_bounds(0.0, 2.0)
149///     .width(5)
150///     .height(3)
151///     .marker(Marker::Ascii('+'))
152///     .paint(|p| {
153///         p.shape(&Cross(2.0, 1.0));
154///     });
155/// let console = Console::builder().width(5).color_system(None).build();
156/// assert_eq!(console.render_export(&canvas), "  +  \n +++ \n  +  \n");
157/// ```
158pub trait Shape {
159    /// Draw the shape with `painter`.
160    fn draw(&self, painter: &mut CanvasPainter);
161}
162
163/// What a [`Canvas::paint`] closure draws with.
164///
165/// Coordinates are world coordinates, `y` growing upwards, as the canvas's
166/// bounds give them. Anything outside the bounds is clipped, and NaN or
167/// infinite coordinates are skipped. Styles are theme keys or definitions
168/// (`"bold red"`); an empty one draws unstyled.
169#[derive(Clone, Debug, Default, PartialEq)]
170pub struct CanvasPainter {
171    ops: Vec<Op>,
172}
173
174impl CanvasPainter {
175    /// A dot at each point.
176    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    /// A dot at (`x`, `y`).
182    pub fn point(&mut self, x: f64, y: f64, style: &str) -> &mut Self {
183        self.points(&[(x, y)], style)
184    }
185
186    /// Draw a [`Shape`] of your own.
187    pub fn shape(&mut self, shape: &dyn Shape) -> &mut Self {
188        shape.draw(self);
189        self
190    }
191
192    /// The world's coastlines, in longitude (`x`, -180 to 180) and latitude
193    /// (`y`, -90 to 90): give the canvas those bounds for the whole world,
194    /// or narrower ones for a region. The lines are Natural Earth's, in the
195    /// public domain, simplified: see [`MapResolution`].
196    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    /// A straight line from (`x1`, `y1`) to (`x2`, `y2`), with the area
202    /// between it and the level `y = fill_to` filled in: a strip of an area
203    /// chart.
204    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    /// A straight line from (`x1`, `y1`) to (`x2`, `y2`).
219    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    /// Lines joining `points` in order. A NaN or infinite point breaks the
225    /// line; a point alone between two breaks is drawn as a dot.
226    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    /// The outline of a rectangle whose bottom left corner is (`x`, `y`).
243    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    /// The outline of a circle: a dot every degree.
252    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    /// `text` starting at the cell that holds (`x`, `y`), cut at the right
258    /// edge. Text is drawn after every layer, over whatever is there.
259    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    /// Start a new layer. Where a later layer draws in a cell, it replaces
266    /// what earlier layers drew there; within a layer, Braille dots add up
267    /// and the cell takes the colour of the last shape that drew in it.
268    pub fn layer(&mut self) -> &mut Self {
269        self.ops.push(Op::Layer);
270        self
271    }
272
273    /// Draw what follows with `marker`. A marker different from the current
274    /// one starts a new layer.
275    pub fn marker(&mut self, marker: Marker) -> &mut Self {
276        self.ops.push(Op::Marker(marker));
277        self
278    }
279}
280
281/// Shapes and text drawn in world coordinates, as ratatui's `Canvas`.
282///
283/// - The bounds ([`x_bounds`](Self::x_bounds), [`y_bounds`](Self::y_bounds))
284///   are the world's edges: `x` grows to the right and `y` upwards. They
285///   default to `0..1`; empty, reversed or infinite bounds draw nothing.
286/// - It fills the height it is given (a `Layout` region, an intuiTUIve
287///   node), or is [`height`](Self::height) rows when that is set, or 10
288///   rows; and it is as wide as the width it is given (or
289///   [`width`](Self::width)).
290/// - [`background`](Self::background) styles the cells nothing drew in,
291///   and goes under what is drawn.
292/// - [`paint`](Self::paint) runs a closure once and records what it draws,
293///   so the canvas renders the same every time and can be cloned.
294/// - [`Marker::Braille`] (the default) draws 2×4 dots a cell. An ASCII-only
295///   console gets one glyph a cell instead.
296///
297/// ```
298/// use rich::Console;
299/// use rich_ext::chart::{Canvas, Marker};
300///
301/// let console = Console::builder().width(12).color_system(None).build();
302/// let canvas = Canvas::new()
303///     .x_bounds(0.0, 11.0)
304///     .y_bounds(0.0, 3.0)
305///     .height(4)
306///     .marker(Marker::Ascii('#'))
307///     .paint(|p| {
308///         p.rect(0.0, 0.0, 11.0, 3.0, "blue");
309///         p.line(0.0, 0.0, 11.0, 3.0, "red");
310///         p.print(2.0, 2.0, "hi", "bold");
311///     });
312/// assert_eq!(
313///     console.render_export(&canvas),
314///     concat!(
315///         "############\n",
316///         "# hi  #### #\n",
317///         "# ####     #\n",
318///         "############\n",
319///     )
320/// );
321///
322/// // Braille: 2×4 dots a cell.
323/// let braille = Canvas::new()
324///     .x_bounds(0.0, 3.0)
325///     .y_bounds(0.0, 3.0)
326///     .height(1)
327///     .width(2)
328///     .paint(|p| {
329///         p.line(0.0, 0.0, 3.0, 3.0, "");
330///     });
331/// assert_eq!(console.render_export(&braille), "⡠⠊\n");
332/// ```
333#[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    /// A blank canvas over `0..1` × `0..1`.
353    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    /// The world's left and right edges.
366    pub fn x_bounds(mut self, left: f64, right: f64) -> Self {
367        self.x_bounds = [left, right];
368        self
369    }
370
371    /// The world's bottom and top edges.
372    pub fn y_bounds(mut self, bottom: f64, top: f64) -> Self {
373        self.y_bounds = [bottom, top];
374        self
375    }
376
377    /// Width in cells (default: the width it is given).
378    pub fn width(mut self, width: usize) -> Self {
379        self.width = Some(width.min(MAX_SIDE));
380        self
381    }
382
383    /// Height in rows (at most 65535), whatever height it is given. Without
384    /// it, the canvas fills the height it is given, or takes 10 rows.
385    pub fn height(mut self, rows: usize) -> Self {
386        self.height = Some(rows.min(MAX_SIDE));
387        self
388    }
389
390    /// The style of the cells nothing drew in, and under what is drawn: a
391    /// theme key or a definition. A style with only a foreground colour
392    /// (`"navy_blue"`) is taken as that background (`"on navy_blue"`).
393    pub fn background(mut self, style: impl Into<String>) -> Self {
394        self.background = Some(style.into());
395        self
396    }
397
398    /// The marker the first layer draws with (default
399    /// [`Marker::Braille`]); [`CanvasPainter::marker`] changes it.
400    pub fn marker(mut self, marker: Marker) -> Self {
401        self.marker = marker;
402        self
403    }
404
405    /// Run `draw` once and keep what it draws. Each call adds to what
406    /// earlier calls drew, in a new layer.
407    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
445/// A background style: one with a foreground colour and no background
446/// colour has the colour moved to the background.
447fn 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
476/// `Canvas, x from -180 to 180, y from -90 to 90: a world map, 4 points, 3
477/// lines`, then the text printed on it (`Text: London, Paris`). A canvas
478/// holds shapes rather than data, so it says what it drew.
479impl 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
529/// Whether to draw in ASCII only on `console`.
530pub(crate) fn is_ascii(console: &Console, options: &ConsoleOptions) -> bool {
531    Charset::Auto.resolve(console, options, Charset::Braille) == Charset::Ascii
532}
533
534/// A style spec: a `chart.*` theme key (falling back to the chart
535/// defaults), another theme key or a definition. Empty is no style.
536pub(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
547/// Where and how [`raster`] draws.
548pub(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/// The world's edges, checked to be finite and not empty.
558#[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    /// The cell of a `columns` × `rows` grid that holds `p`, counted from
583    /// the top left, as ratatui maps a point: scaled to `size - 1` and
584    /// truncated.
585    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    /// The part of the segment `a`–`b` inside the bounds (Liang–Barsky).
595    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            // Both ends inside (and the step finite): the loop below would
602            // leave t0 at 0 and t1 at 1 (a distance to an edge over a no
603            // larger step divides to at least 1), so this is what it
604            // returns, computed the same way.
605            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
647/// Style specs seen so far, resolved once each.
648struct 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        // The last spec again (a dataset's thousand steps share one).
665        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
683/// The dots one layer has set: Braille on a [`DotCanvas`]; quadrants,
684/// sextants and octants as a pattern of dots a cell, with the style that
685/// last set a dot in it; anything else as a grid of pixels holding the
686/// style that last set each.
687enum 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    /// Pixels across and down.
725    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            // Straight onto the dots: the canvas keeps to its own size.
766            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    /// The line from `a` to `b`, and the pixels between it and the level
780    /// `fill` (moved into the bounds), column by column.
781    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    /// Copy the cells this layer drew in onto `out`, its columns from
807    /// `left`.
808    fn finish(&self, out: &mut CellGrid, styles: &Styles, left: usize) {
809        let columns = self.resolution().0;
810        // Each style interned in `out` once.
811        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
864/// A half-block cell from the styles of its upper and lower pixels.
865fn 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
890/// Draw `ops` onto a `view.width` × `view.height` grid. Cells nothing drew
891/// in are left empty.
892pub(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
899/// [`raster`] into the columns of `out` from `left`, which must be empty
900/// there: what [`raster`] then [`CellGrid::blit`] at `left` would give,
901/// without a second grid.
902pub(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    // The last spec and its id: a dataset's steps share one.
920    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/// A grid of terminal cells: each empty, or a character (with any marks
996/// after it) and a style. The second cell of a wide character holds an
997/// empty string.
998///
999/// The cells are plain data: the text inline (a [`Cell`]) and the style an
1000/// id into the grid's own [`StyleTable`], each style kept once. Setting a
1001/// cell allocates nothing, and the grid becomes lines a run at a time, or
1002/// goes straight into a [`Screen`](crate::surface::Screen)'s cells (see
1003/// [`draw`](Self::draw)).
1004#[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    /// The id of `style` in this grid.
1023    pub(crate) fn intern(&mut self, style: Option<&Style>) -> StyleId {
1024        self.styles.intern(style)
1025    }
1026
1027    /// The style behind `id`.
1028    fn style(&self, id: StyleId) -> Option<&Style> {
1029        self.styles.get(id)
1030    }
1031
1032    /// Set one cell, blanking the other half of any wide character it
1033    /// cuts in two. Out of range is ignored.
1034    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    /// [`set`](Self::set) with a style already interned.
1040    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    /// [`set`](Self::set) with one character, its style already interned.
1053    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    /// Write `text` from `col`, stopping before column `end` (and the
1077    /// grid's edge). A wide character that would cross it becomes a space.
1078    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    /// Put `style` under every cell: empty cells become spaces in it, and
1106    /// drawn cells take it under their own.
1107    pub(crate) fn underlay(&mut self, style: &Style) {
1108        let under = self.intern(Some(style));
1109        // Each style combined once: by own id, the combined id.
1110        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    /// Give a drawn cell `style` in place of its own; an empty cell stays
1142    /// empty. Out of range is ignored.
1143    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    /// The text of a row, empty cells as spaces.
1153    #[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    /// The style of a cell.
1164    #[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    /// The rows as lines, a run of one style at a time.
1172    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    /// The fast path: the grid straight into `rect` of `screen`, the cells
1207    /// that [`Screen::write_lines`] of its rendered lines (`into_lines`, then
1208    /// [`lines_to_segments`]) would give, without building a line, a
1209    /// segment or a style a run.
1210    /// At most `rows` rows.
1211    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        // Every style interned once, as the rich path's segments would be:
1214        // a style that sets nothing is no style; one with metadata takes
1215        // the general path.
1216        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        // Each row's runs of one style, their text one after another in
1232        // one buffer: (start, end, screen style) and where each row ends.
1233        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        // Fully outside, beside and parallel.
1354        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        // Crossing a corner's outside.
1358        assert_eq!(b.clip((-5.0, 8.0), (2.0, 15.0)), None);
1359        // Non-finite.
1360        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        // A huge line outside draws nothing and does not loop.
1381        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        // Within a layer the dots add up; the last writer's colour wins.
1420        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        // A layer that draws nothing in a cell leaves it alone.
1431        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        // Points map as ratatui maps them, truncated.
1524        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        // Sextants: the bottom left and middle right sixths, then the
1549        // middle left and top right ones.
1550        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        // ASCII consoles: `#` for the block markers.
1554        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    /// Whether any cell within a cell of (`col`, `row`) is drawn.
1588    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            // The cell a longitude and latitude fall in, as the canvas maps
1611            // them (2 × 4 dots a cell).
1612            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            // The open Pacific and the middle of Australia are blank.
1630            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            // The map draws in its style.
1639            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        // In a styled export, every cell carries the background.
1664        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        // Above the level the fill goes up to it.
1693        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        // Its own height wins; with neither, ten rows.
1747        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        // Empty, reversed and infinite bounds draw nothing.
1768        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}