use rich::cells::char_cell_width;
use rich::measure::Measurement;
use rich::{Console, ConsoleOptions, Renderable, Segment, Style};
use super::canvas::{bresenham, octant, quadrant, sextant};
use super::world::MapResolution;
use super::{
Charset, DotCanvas, Line, cell_units, cells, draw_grid, lines_to_segments, theme_style,
user_style,
};
use crate::fidelity::ascii_text;
use crate::frame::{StyleId, StyleTable};
use crate::surface::{Cell, Draw, Rect, Screen, TextRun};
const DEFAULT_HEIGHT: usize = 10;
const MAX_SIDE: usize = u16::MAX as usize;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Marker {
#[default]
Braille,
HalfBlock,
Block,
Bar,
Quadrant,
Sextant,
Octant,
Dot,
Ascii(char),
}
impl Marker {
pub(crate) fn resolve(self, ascii: bool) -> Marker {
match self {
Marker::Ascii(c)
if char_cell_width(c) == 1 && (!ascii || c.is_ascii_graphic() || c == ' ') =>
{
self
}
Marker::Ascii(_) => Marker::Ascii('*'),
_ if !ascii => self,
Marker::Block
| Marker::HalfBlock
| Marker::Bar
| Marker::Quadrant
| Marker::Sextant
| Marker::Octant => Marker::Ascii('#'),
_ => Marker::Ascii('*'),
}
}
pub(crate) fn per_cell(self) -> (usize, usize) {
match self {
Marker::Braille | Marker::Octant => (2, 4),
Marker::Sextant => (2, 3),
Marker::Quadrant => (2, 2),
Marker::HalfBlock => (1, 2),
_ => (1, 1),
}
}
fn pattern(self, bits: u8) -> char {
match self {
Marker::Quadrant => quadrant(bits),
Marker::Sextant => sextant(bits),
_ => octant(bits),
}
}
}
pub(crate) type Spec = std::sync::Arc<str>;
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum Op {
Points(Vec<(f64, f64)>, Spec),
Line((f64, f64), (f64, f64), Spec),
Area((f64, f64), (f64, f64), f64, Spec),
Circle((f64, f64), f64, Spec),
Map(MapResolution, Spec),
Print((f64, f64), String, Spec),
Layer,
Marker(Marker),
}
pub trait Shape {
fn draw(&self, painter: &mut CanvasPainter);
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct CanvasPainter {
ops: Vec<Op>,
}
impl CanvasPainter {
pub fn points(&mut self, points: &[(f64, f64)], style: &str) -> &mut Self {
self.ops.push(Op::Points(points.to_vec(), style.into()));
self
}
pub fn point(&mut self, x: f64, y: f64, style: &str) -> &mut Self {
self.points(&[(x, y)], style)
}
pub fn shape(&mut self, shape: &dyn Shape) -> &mut Self {
shape.draw(self);
self
}
pub fn map(&mut self, resolution: MapResolution, style: &str) -> &mut Self {
self.ops.push(Op::Map(resolution, style.into()));
self
}
pub fn area(
&mut self,
x1: f64,
y1: f64,
x2: f64,
y2: f64,
fill_to: f64,
style: &str,
) -> &mut Self {
self.ops
.push(Op::Area((x1, y1), (x2, y2), fill_to, style.into()));
self
}
pub fn line(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, style: &str) -> &mut Self {
self.ops.push(Op::Line((x1, y1), (x2, y2), style.into()));
self
}
pub fn polyline(&mut self, points: &[(f64, f64)], style: &str) -> &mut Self {
let finite = |p: &(f64, f64)| p.0.is_finite() && p.1.is_finite();
for run in points.split(|p| !finite(p)) {
match run {
[] => {}
[p] => self.ops.push(Op::Points(vec![*p], style.into())),
_ => {
for pair in run.windows(2) {
self.ops.push(Op::Line(pair[0], pair[1], style.into()));
}
}
}
}
self
}
pub fn rect(&mut self, x: f64, y: f64, width: f64, height: f64, style: &str) -> &mut Self {
let (r, t) = (x + width, y + height);
self.line(x, y, r, y, style)
.line(x, t, r, t, style)
.line(x, y, x, t, style)
.line(r, y, r, t, style)
}
pub fn circle(&mut self, x: f64, y: f64, radius: f64, style: &str) -> &mut Self {
self.ops.push(Op::Circle((x, y), radius, style.into()));
self
}
pub fn print(&mut self, x: f64, y: f64, text: &str, style: &str) -> &mut Self {
self.ops
.push(Op::Print((x, y), text.to_string(), style.into()));
self
}
pub fn layer(&mut self) -> &mut Self {
self.ops.push(Op::Layer);
self
}
pub fn marker(&mut self, marker: Marker) -> &mut Self {
self.ops.push(Op::Marker(marker));
self
}
}
#[derive(Clone, Debug, PartialEq)]
#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
pub struct Canvas {
x_bounds: [f64; 2],
y_bounds: [f64; 2],
width: Option<usize>,
height: Option<usize>,
marker: Marker,
background: Option<String>,
ops: Vec<Op>,
}
impl Default for Canvas {
fn default() -> Self {
Self::new()
}
}
impl Canvas {
pub fn new() -> Self {
Canvas {
x_bounds: [0.0, 1.0],
y_bounds: [0.0, 1.0],
width: None,
height: None,
marker: Marker::Braille,
background: None,
ops: Vec::new(),
}
}
pub fn x_bounds(mut self, left: f64, right: f64) -> Self {
self.x_bounds = [left, right];
self
}
pub fn y_bounds(mut self, bottom: f64, top: f64) -> Self {
self.y_bounds = [bottom, top];
self
}
pub fn width(mut self, width: usize) -> Self {
self.width = Some(width.min(MAX_SIDE));
self
}
pub fn height(mut self, rows: usize) -> Self {
self.height = Some(rows.min(MAX_SIDE));
self
}
pub fn background(mut self, style: impl Into<String>) -> Self {
self.background = Some(style.into());
self
}
pub fn marker(mut self, marker: Marker) -> Self {
self.marker = marker;
self
}
pub fn paint(mut self, draw: impl FnOnce(&mut CanvasPainter)) -> Self {
let mut painter = CanvasPainter::default();
draw(&mut painter);
if !self.ops.is_empty() {
self.ops.push(Op::Layer);
}
self.ops.extend(painter.ops);
self
}
fn grid(&self, console: &Console, options: &ConsoleOptions) -> CellGrid {
let width = self
.width
.map_or(options.max_width, |w| w.min(options.max_width));
let view = View {
x: self.x_bounds,
y: self.y_bounds,
width,
height: self
.height
.or(options.height)
.unwrap_or(DEFAULT_HEIGHT)
.min(MAX_SIDE),
marker: self.marker,
ascii: is_ascii(console, options),
};
let mut grid = raster(&self.ops, &view, console);
if let Some(style) = self
.background
.as_deref()
.and_then(|spec| spec_style(console, spec))
{
grid.underlay(&background_style(style));
}
grid
}
}
fn background_style(style: Style) -> Style {
match (style.color(), style.bgcolor()) {
(Some(colour), None) => {
let colour = colour.clone();
style.without_color().with_bgcolor(colour)
}
_ => style,
}
}
impl Draw for Canvas {
fn draw(&self, console: &Console, options: &ConsoleOptions, screen: &mut Screen, rect: Rect) {
draw_grid(self.grid(console, options), options, screen, rect);
}
}
impl Renderable for Canvas {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
lines_to_segments(self.grid(console, options).into_lines(), options.max_width)
}
fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
let max = self.width.unwrap_or(options.max_width);
Measurement::new(max.min(12), max)
.with_maximum(options.max_width)
.normalize()
}
}
impl crate::a11y::AccessibleText for Canvas {
fn accessible_text(&self, _width: usize) -> String {
use super::describe::counted;
let format = super::ValueFormat::Compact;
let axis = |[a, b]: [f64; 2]| super::describe::range(a.min(b), a.max(b), format);
let mut head = format!(
"Canvas, x {}, y {}",
axis(self.x_bounds),
axis(self.y_bounds)
);
let (mut points, mut lines, mut areas, mut circles) = (0, 0, 0, 0);
let mut map = false;
let mut text = Vec::new();
for op in &self.ops {
match op {
Op::Points(p, _) => points += p.len(),
Op::Line(..) => lines += 1,
Op::Area(..) => areas += 1,
Op::Circle(..) => circles += 1,
Op::Map(..) => map = true,
Op::Print(_, t, _) if !t.trim().is_empty() => text.push(t.trim().to_string()),
_ => {}
}
}
let mut drawn = Vec::new();
if map {
drawn.push("a world map".to_string());
}
for (n, one, many) in [
(points, "point", "points"),
(lines, "line", "lines"),
(areas, "area segment", "area segments"),
(circles, "circle", "circles"),
] {
if n > 0 {
drawn.push(counted(n, one, many));
}
}
if drawn.is_empty() {
head.push_str(": empty");
} else {
head.push_str(&format!(": {}", drawn.join(", ")));
}
if !text.is_empty() {
head.push_str(&format!("\nText: {}", text.join(", ")));
}
head
}
}
pub(crate) fn is_ascii(console: &Console, options: &ConsoleOptions) -> bool {
Charset::Auto.resolve(console, options, Charset::Braille) == Charset::Ascii
}
pub(crate) fn spec_style(console: &Console, spec: &str) -> Option<Style> {
let spec = spec.trim();
if spec.is_empty() {
None
} else if spec.starts_with("chart.") {
Some(theme_style(console, spec))
} else {
Some(user_style(console, spec))
}
}
pub(crate) struct View {
pub(crate) x: [f64; 2],
pub(crate) y: [f64; 2],
pub(crate) width: usize,
pub(crate) height: usize,
pub(crate) marker: Marker,
pub(crate) ascii: bool,
}
#[derive(Clone, Copy)]
struct Bounds {
left: f64,
right: f64,
bottom: f64,
top: f64,
}
impl Bounds {
fn new(x: [f64; 2], y: [f64; 2]) -> Option<Bounds> {
let b = Bounds {
left: x[0],
right: x[1],
bottom: y[0],
top: y[1],
};
let (w, h) = (b.right - b.left, b.top - b.bottom);
(w.is_finite() && h.is_finite() && w > 0.0 && h > 0.0).then_some(b)
}
fn contains(&self, (x, y): (f64, f64)) -> bool {
(self.left..=self.right).contains(&x) && (self.bottom..=self.top).contains(&y)
}
fn cell(&self, p: (f64, f64), columns: usize, rows: usize) -> Option<(usize, usize)> {
if !self.contains(p) || columns == 0 || rows == 0 {
return None;
}
let x = (p.0 - self.left) * (columns - 1) as f64 / (self.right - self.left);
let y = (self.top - p.1) * (rows - 1) as f64 / (self.top - self.bottom);
Some(((x as usize).min(columns - 1), (y as usize).min(rows - 1)))
}
fn clip(&self, a: (f64, f64), b: (f64, f64)) -> Option<((f64, f64), (f64, f64))> {
if ![a.0, a.1, b.0, b.1].iter().all(|v| v.is_finite()) {
return None;
}
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
if dx.is_finite() && dy.is_finite() && self.contains(a) && self.contains(b) {
let end = (
(a.0 + dx).clamp(self.left, self.right),
(a.1 + dy).clamp(self.bottom, self.top),
);
return Some((a, end));
}
let (mut t0, mut t1) = (0.0f64, 1.0f64);
for (p, q) in [
(-dx, a.0 - self.left),
(dx, self.right - a.0),
(-dy, a.1 - self.bottom),
(dy, self.top - a.1),
] {
if !(p.is_finite() && q.is_finite()) {
return None;
}
if p == 0.0 {
if q < 0.0 {
return None;
}
} else {
let r = q / p;
if p < 0.0 {
t0 = t0.max(r);
} else {
t1 = t1.min(r);
}
}
}
if t0 > t1 {
return None;
}
let at = |t: f64| {
(
(a.0 + t * dx).clamp(self.left, self.right),
(a.1 + t * dy).clamp(self.bottom, self.top),
)
};
Some((at(t0), at(t1)))
}
}
struct Styles<'a> {
console: &'a Console,
specs: Vec<String>,
resolved: Vec<Option<Style>>,
}
impl<'a> Styles<'a> {
fn new(console: &'a Console) -> Self {
Styles {
console,
specs: Vec::new(),
resolved: Vec::new(),
}
}
fn id(&mut self, spec: &str) -> usize {
if let Some(last) = self.specs.last()
&& last == spec
{
return self.specs.len() - 1;
}
if let Some(i) = self.specs.iter().position(|s| s == spec) {
return i;
}
self.specs.push(spec.to_string());
self.resolved.push(spec_style(self.console, spec));
self.specs.len() - 1
}
fn get(&self, id: usize) -> Option<Style> {
self.resolved.get(id).cloned().flatten()
}
}
enum Pixels {
Braille(DotCanvas),
Pattern {
bits: Vec<u8>,
styles: Vec<Option<usize>>,
},
Grid(Vec<Option<usize>>),
}
struct Layer {
marker: Marker,
width: usize,
height: usize,
pixels: Pixels,
}
impl Layer {
fn new(marker: Marker, width: usize, height: usize) -> Layer {
let pixels = match marker {
Marker::Braille => Pixels::Braille(DotCanvas::new(width, height)),
Marker::Quadrant | Marker::Sextant | Marker::Octant => Pixels::Pattern {
bits: vec![0; width * height],
styles: vec![None; width * height],
},
_ => {
let (px, py) = marker.per_cell();
Pixels::Grid(vec![None; width * px * height * py])
}
};
Layer {
marker,
width,
height,
pixels,
}
}
fn resolution(&self) -> (usize, usize) {
let (px, py) = self.marker.per_cell();
(self.width * px, self.height * py)
}
fn set(&mut self, (x, y): (usize, usize), style: usize) {
let (columns, rows) = self.resolution();
if x >= columns || y >= rows {
return;
}
match &mut self.pixels {
Pixels::Braille(canvas) => canvas.set(x as i64, y as i64, style),
Pixels::Pattern { bits, styles } => {
let (px, py) = self.marker.per_cell();
let cell = (y / py) * self.width + x / px;
bits[cell] |= 1 << (x % px + px * (y % py));
styles[cell] = Some(style);
}
Pixels::Grid(grid) => grid[y * columns + x] = Some(style),
}
}
fn point(&mut self, bounds: &Bounds, p: (f64, f64), style: usize) {
let (columns, rows) = self.resolution();
if let Some(at) = bounds.cell(p, columns, rows) {
self.set(at, style);
}
}
fn line(&mut self, bounds: &Bounds, a: (f64, f64), b: (f64, f64), style: usize) {
let Some((a, b)) = bounds.clip(a, b) else {
return;
};
let (columns, rows) = self.resolution();
let (Some(a), Some(b)) = (bounds.cell(a, columns, rows), bounds.cell(b, columns, rows))
else {
return;
};
let to_i = |(x, y): (usize, usize)| (x as i64, y as i64);
match &mut self.pixels {
Pixels::Braille(canvas) => {
for (x, y) in bresenham(to_i(a), to_i(b)) {
canvas.set(x, y, style);
}
}
_ => {
for (x, y) in bresenham(to_i(a), to_i(b)) {
self.set((x as usize, y as usize), style);
}
}
}
}
fn area(&mut self, bounds: &Bounds, a: (f64, f64), b: (f64, f64), fill: f64, style: usize) {
if !fill.is_finite() {
return self.line(bounds, a, b, style);
}
let Some((a, b)) = bounds.clip(a, b) else {
return;
};
let (columns, rows) = self.resolution();
let fill = fill.clamp(bounds.bottom, bounds.top);
let (Some(start), Some(end), Some((_, level))) = (
bounds.cell(a, columns, rows),
bounds.cell(b, columns, rows),
bounds.cell((a.0, fill), columns, rows),
) else {
return;
};
let to_i = |(x, y): (usize, usize)| (x as i64, y as i64);
for (x, y) in bresenham(to_i(start), to_i(end)) {
let (x, y) = (x as usize, y as usize);
for row in y.min(level)..=y.max(level) {
self.set((x, row), style);
}
}
}
fn finish(&self, out: &mut CellGrid, styles: &Styles, left: usize) {
let columns = self.resolution().0;
let ids: Vec<StyleId> = styles
.resolved
.iter()
.map(|style| out.intern(style.as_ref()))
.collect();
if let Pixels::Braille(canvas) = &self.pixels {
for row in 0..self.height {
for col in 0..self.width {
if !canvas.is_set(col, row) {
continue;
}
if let (c, Some(style)) = canvas.cell(col, row) {
out.set_char(row, left + col, c, ids[style]);
}
}
}
return;
}
for row in 0..self.height {
for col in 0..self.width {
let cell = match (&self.pixels, self.marker) {
(Pixels::Braille(_), _) => None,
(Pixels::Pattern { bits, styles: set }, marker) => {
let i = row * self.width + col;
(bits[i] != 0).then(|| {
let style = set[i].map_or(StyleId::NONE, |s| ids[s]);
(marker.pattern(bits[i]), style)
})
}
(Pixels::Grid(grid), Marker::HalfBlock) => {
let upper = grid[2 * row * columns + col];
let lower = grid[(2 * row + 1) * columns + col];
half_block(upper, lower, styles)
.map(|(c, style)| (c, out.intern(style.as_ref())))
}
(Pixels::Grid(grid), marker) => grid[row * columns + col].map(|s| {
let glyph = match marker {
Marker::Block => '█',
Marker::Bar => '▄',
Marker::Ascii(c) => c,
_ => '•',
};
(glyph, ids[s])
}),
};
if let Some((c, style)) = cell {
out.set_char(row, left + col, c, style);
}
}
}
}
}
fn half_block(
upper: Option<usize>,
lower: Option<usize>,
styles: &Styles,
) -> Option<(char, Option<Style>)> {
match (upper, lower) {
(Some(a), Some(b)) if a == b => Some(('█', styles.get(a))),
(Some(a), Some(b)) => {
let style = match styles.get(b).as_ref().and_then(|s| s.color()) {
Some(colour) => Some(
styles
.get(a)
.unwrap_or_default()
.with_bgcolor(colour.clone()),
),
None => styles.get(a),
};
Some(('▀', style))
}
(Some(a), None) => Some(('▀', styles.get(a))),
(None, Some(b)) => Some(('▄', styles.get(b))),
(None, None) => None,
}
}
pub(crate) fn raster(ops: &[Op], view: &View, console: &Console) -> CellGrid {
let (width, height) = (view.width.min(MAX_SIDE), view.height.min(MAX_SIDE));
let mut out = CellGrid::new(width, height);
raster_into(ops, view, console, &mut out, 0);
out
}
pub(crate) fn raster_into(
ops: &[Op],
view: &View,
console: &Console,
out: &mut CellGrid,
left: usize,
) {
let (width, height) = (view.width.min(MAX_SIDE), view.height.min(MAX_SIDE));
let Some(bounds) = Bounds::new(view.x, view.y) else {
return;
};
if width == 0 || height == 0 {
return;
}
let mut styles = Styles::new(console);
let mut layer = Layer::new(view.marker.resolve(view.ascii), width, height);
let mut labels = Vec::new();
let mut last: Option<(&Spec, usize)> = None;
let mut id_of = |styles: &mut Styles, spec| match last {
Some((seen, id)) if std::sync::Arc::ptr_eq(seen, spec) => id,
_ => {
let id = styles.id(spec);
last = Some((spec, id));
id
}
};
for op in ops {
match op {
Op::Layer => {
layer.finish(out, &styles, left);
layer = Layer::new(layer.marker, width, height);
}
Op::Marker(marker) => {
let marker = marker.resolve(view.ascii);
if marker != layer.marker {
layer.finish(out, &styles, left);
layer = Layer::new(marker, width, height);
}
}
Op::Points(points, style) => {
let style = id_of(&mut styles, style);
for p in points {
layer.point(&bounds, *p, style);
}
}
Op::Line(a, b, style) => {
let style = id_of(&mut styles, style);
layer.line(&bounds, *a, *b, style);
}
Op::Area(a, b, fill, style) => {
let style = id_of(&mut styles, style);
layer.area(&bounds, *a, *b, *fill, style);
}
Op::Map(resolution, style) => {
let style = id_of(&mut styles, style);
for line in resolution.lines() {
for pair in line.windows(2) {
layer.line(&bounds, pair[0], pair[1], style);
}
}
}
Op::Circle((x, y), radius, style) => {
let style = id_of(&mut styles, style);
if radius.is_finite() {
for degree in 0..360 {
let (sin, cos) = f64::from(degree).to_radians().sin_cos();
layer.point(&bounds, (x + radius * cos, y + radius * sin), style);
}
}
}
Op::Print(at, text, style) => labels.push((*at, text, style)),
}
}
layer.finish(out, &styles, left);
for (at, text, style) in labels {
if let Some((col, row)) = bounds.cell(at, width, height) {
let text = if view.ascii && !text.is_ascii() {
ascii_text(text)
} else {
text.clone()
};
out.put_text(
row,
left + col,
&text,
spec_style(console, style),
left + width,
);
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct CellGrid {
width: usize,
height: usize,
cells: Vec<Option<Cell>>,
styles: StyleTable,
}
impl CellGrid {
pub(crate) fn new(width: usize, height: usize) -> Self {
CellGrid {
width,
height,
cells: vec![None; width * height],
styles: StyleTable::new(),
}
}
pub(crate) fn intern(&mut self, style: Option<&Style>) -> StyleId {
self.styles.intern(style)
}
fn style(&self, id: StyleId) -> Option<&Style> {
self.styles.get(id)
}
pub(crate) fn set(&mut self, row: usize, col: usize, text: String, style: Option<Style>) {
let id = self.intern(style.as_ref());
self.set_id(row, col, &text, id);
}
pub(crate) fn set_id(&mut self, row: usize, col: usize, text: &str, style: StyleId) {
if row >= self.height || col >= self.width {
return;
}
let width = if text.len() == 1 {
1
} else {
cells(text).min(u8::MAX as usize) as u8
};
self.put(row, col, Cell::new(text, width, style));
}
pub(crate) fn set_char(&mut self, row: usize, col: usize, c: char, style: StyleId) {
if row >= self.height || col >= self.width {
return;
}
let width = char_cell_width(c).min(u8::MAX as usize) as u8;
self.put(row, col, Cell::from_char(c, width, style));
}
fn put(&mut self, row: usize, col: usize, cell: Cell) {
let i = row * self.width + col;
let blank = Some(Cell::from_char(' ', 1, StyleId::NONE));
if !cell.is_empty_text()
&& col > 0
&& matches!(&self.cells[i], Some(c) if c.is_empty_text())
{
self.cells[i - 1] = blank;
}
if col + 1 < self.width && matches!(&self.cells[i], Some(c) if c.width > 1) {
self.cells[i + 1] = blank;
}
self.cells[i] = Some(cell);
}
pub(crate) fn put_text(
&mut self,
row: usize,
col: usize,
text: &str,
style: Option<Style>,
end: usize,
) {
let id = self.intern(style.as_ref());
let end = end.min(self.width);
let units = cell_units(text);
let mut at = col;
let mut i = 0;
while i < units.len() && at < end {
let unit = &units[i];
let wide = i + 1 < units.len() && units[i + 1].is_empty() && !unit.is_empty();
if wide && at + 1 >= end {
self.set_id(row, at, " ", id);
break;
}
let unit = if unit.is_empty() && i == 0 { " " } else { unit };
self.set_id(row, at, unit, id);
at += 1;
i += 1;
}
}
pub(crate) fn underlay(&mut self, style: &Style) {
let under = self.intern(Some(style));
let mut combined: Vec<Option<StyleId>> = vec![None; self.styles.len()];
for at in 0..self.cells.len() {
let cell = match self.cells[at] {
None => Cell::new(" ", 1, under),
Some(cell) => {
let own = cell.style;
let id = match combined.get(own.index()).copied().flatten() {
Some(id) => id,
None => {
let id = match self.styles.get(own).cloned() {
Some(own_style) => {
self.styles.intern(Some(&style.combine(&own_style)))
}
None => under,
};
if combined.len() <= own.index() {
combined.resize(own.index() + 1, None);
}
combined[own.index()] = Some(id);
id
}
};
let mut cell = cell;
cell.style = id;
cell
}
};
self.cells[at] = Some(cell);
}
}
pub(crate) fn restyle(&mut self, row: usize, col: usize, style: Option<Style>) {
if row < self.height && col < self.width {
let id = self.intern(style.as_ref());
if let Some(cell) = &mut self.cells[row * self.width + col] {
cell.style = id;
}
}
}
#[cfg(test)]
pub(crate) fn row_string(&self, row: usize) -> String {
(0..self.width)
.map(|c| match &self.cells[row * self.width + c] {
Some(cell) => cell.text().to_string(),
None => " ".to_string(),
})
.collect()
}
#[cfg(test)]
pub(crate) fn style_at(&self, row: usize, col: usize) -> Option<Style> {
self.cells[row * self.width + col]
.as_ref()
.and_then(|cell| self.style(cell.style).cloned())
}
pub(crate) fn into_lines(self) -> Vec<Line> {
let mut text = String::new();
(0..self.height)
.map(|row| {
let mut line = Line::new();
let cells = &self.cells[row * self.width..(row + 1) * self.width];
let mut style = None;
for cell in cells {
if cell.is_some_and(|c| c.is_empty_text()) {
continue;
}
let id = cell.map_or(StyleId::NONE, |c| c.style);
if style != Some(id) {
if let Some(last) = style {
line.push(&text, self.style(last).cloned());
}
text.clear();
style = Some(id);
}
match cell.map(|c| (c, c.single_char())) {
None => text.push(' '),
Some((_, Some(c))) => text.push(c),
Some((c, None)) => text.push_str(c.text()),
}
}
if let Some(last) = style {
line.push(&text, self.style(last).cloned());
}
text.clear();
line
})
.collect()
}
pub(crate) fn draw(&self, screen: &mut Screen, rect: Rect, rows: usize) {
let rows = self.height.min(rect.height as usize).min(rows);
let mut direct = true;
let screen_ids: Vec<StyleId> = (0..self.styles.len())
.map(|i| match self.styles.get(StyleId::from_index(i)) {
None => StyleId::NONE,
Some(style) if style.is_null() => StyleId::NONE,
Some(style) => {
direct &= style.meta_ref().is_none();
screen.intern(Some(style))
}
})
.collect();
if direct && screen.write_cell_grid(rect, self.width, rows, &self.cells, &screen_ids) {
return;
}
let mut ids: Vec<Option<StyleId>> = vec![None; self.styles.len()];
let mut text = String::with_capacity(rows * self.width);
let mut spans: Vec<(usize, usize, StyleId)> = Vec::new();
let mut ends = Vec::with_capacity(rows);
for row in 0..rows {
let cells = &self.cells[row * self.width..(row + 1) * self.width];
let mut style = None;
for cell in cells {
let (piece, own) = match cell {
Some(cell) => (cell.text(), cell.style),
None => (" ", StyleId::NONE),
};
if piece.is_empty() {
continue;
}
if style != Some(own) {
style = Some(own);
let id =
*ids[own.index()].get_or_insert_with(|| screen.intern(self.style(own)));
spans.push((text.len(), text.len(), id));
}
text.push_str(piece);
if let Some(last) = spans.last_mut() {
last.1 = text.len();
}
}
ends.push(spans.len());
}
let runs: Vec<TextRun<'_>> = spans
.iter()
.map(|&(from, to, id)| TextRun::new(&text[from..to], id))
.collect();
let mut start = 0;
let lines: Vec<&[TextRun<'_>]> = ends
.iter()
.map(|&end| {
let line = &runs[start..end];
start = end;
line
})
.collect();
screen.write_runs(rect, &lines);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accessible_text_says_what_was_drawn() {
use crate::a11y::AccessibleText;
let canvas = Canvas::new()
.x_bounds(-1.0, 1.0)
.y_bounds(-1.0, 1.0)
.paint(|p| {
p.circle(0.0, 0.0, 0.8, "cyan");
p.line(-1.0, -1.0, 1.0, 1.0, "red");
p.points(&[(0.0, 0.0), (0.5, 0.5)], "");
p.layer();
p.print(-0.2, 0.0, "hi", "bold");
});
assert_eq!(
canvas.accessible_text(80),
"Canvas, x from -1 to 1, y from -1 to 1: 2 points, 1 line, 1 circle\nText: hi"
);
let world = Canvas::new()
.x_bounds(-180.0, 180.0)
.y_bounds(-90.0, 90.0)
.paint(|p| {
p.map(MapResolution::Low, "green");
});
assert_eq!(
world.accessible_text(80),
"Canvas, x from -180 to 180, y from -90 to 90: a world map"
);
assert_eq!(
Canvas::new().accessible_text(80),
"Canvas, x from 0 to 1, y from 0 to 1: empty"
);
}
fn console(width: usize) -> Console {
Console::builder().width(width).color_system(None).build()
}
fn grid(canvas: &Canvas, width: usize) -> CellGrid {
let console = console(width);
let options = console.options();
canvas.grid(&console, &options)
}
fn rows(canvas: &Canvas, width: usize) -> Vec<String> {
let g = grid(canvas, width);
(0..g.height).map(|r| g.row_string(r)).collect()
}
fn ascii(c: char) -> Canvas {
Canvas::new()
.x_bounds(0.0, 9.0)
.y_bounds(0.0, 4.0)
.height(5)
.width(10)
.marker(Marker::Ascii(c))
}
#[test]
fn lines_crossing_the_bounds_are_clipped() {
let b = Bounds::new([0.0, 10.0], [0.0, 10.0]).unwrap();
assert_eq!(
b.clip((-5.0, 5.0), (15.0, 5.0)),
Some(((0.0, 5.0), (10.0, 5.0)))
);
assert_eq!(
b.clip((5.0, -5.0), (5.0, 20.0)),
Some(((5.0, 0.0), (5.0, 10.0)))
);
assert_eq!(
b.clip((-10.0, -10.0), (20.0, 20.0)),
Some(((0.0, 0.0), (10.0, 10.0)))
);
assert_eq!(b.clip((-5.0, 0.0), (-1.0, 10.0)), None);
assert_eq!(b.clip((0.0, 11.0), (10.0, 11.0)), None);
assert_eq!(b.clip((11.0, 0.0), (11.0, 10.0)), None);
assert_eq!(b.clip((-5.0, 8.0), (2.0, 15.0)), None);
assert_eq!(b.clip((f64::NAN, 0.0), (1.0, 1.0)), None);
assert_eq!(b.clip((f64::INFINITY, 0.0), (1.0, 1.0)), None);
}
#[test]
fn horizontal_and_vertical_lines_cover_the_canvas() {
let canvas = ascii('#').paint(|p| {
p.line(-100.0, 2.0, 100.0, 2.0, "");
p.line(4.0, -1e6, 4.0, 1e6, "");
});
assert_eq!(
rows(&canvas, 10),
vec![
" # ",
" # ",
"##########",
" # ",
" # ",
]
);
let outside = ascii('#').paint(|p| {
p.line(-1e300, -1e300, -1e299, 1e300, "");
});
assert!(rows(&outside, 10).iter().all(|r| r.trim().is_empty()));
}
#[test]
fn non_finite_points_are_skipped() {
let canvas = ascii('o').paint(|p| {
p.points(&[(f64::NAN, 1.0), (1.0, f64::INFINITY), (0.0, 0.0)], "");
p.polyline(&[(9.0, 4.0), (f64::NAN, 0.0), (9.0, 0.0)], "");
p.circle(5.0, 2.0, f64::NAN, "");
p.print(f64::NAN, 1.0, "x", "");
});
assert_eq!(
rows(&canvas, 10),
vec![
" o",
" ",
" ",
" ",
"o o",
]
);
}
#[test]
fn later_layers_win_a_cell() {
let canvas = Canvas::new()
.x_bounds(0.0, 1.0)
.y_bounds(0.0, 1.0)
.height(1)
.width(1)
.paint(|p| {
p.points(&[(0.0, 1.0)], "red");
p.points(&[(1.0, 1.0)], "blue");
});
let g = grid(&canvas, 1);
assert_eq!(g.row_string(0), "⠉");
assert_eq!(g.style_at(0, 0), Style::parse("blue").ok());
let layered = canvas.clone().paint(|p| {
p.points(&[(0.0, 0.0)], "green");
});
let g = grid(&layered, 1);
assert_eq!(g.row_string(0), "⡀");
assert_eq!(g.style_at(0, 0), Style::parse("green").ok());
let empty = canvas.paint(|p| {
p.layer();
});
assert_eq!(grid(&empty, 1).row_string(0), "⠉");
}
#[test]
fn markers_draw_at_their_resolution() {
let draw = |marker| {
let c = Canvas::new()
.x_bounds(0.0, 1.0)
.y_bounds(0.0, 1.0)
.height(1)
.width(2)
.marker(marker)
.paint(|p| {
p.points(&[(0.0, 1.0), (1.0, 0.0)], "red");
});
rows(&c, 2)[0].clone()
};
assert_eq!(draw(Marker::Braille), "⠁⢀");
assert_eq!(draw(Marker::HalfBlock), "▀▄");
assert_eq!(draw(Marker::Block), "██");
assert_eq!(draw(Marker::Dot), "••");
assert_eq!(draw(Marker::Ascii('x')), "xx");
assert_eq!(draw(Marker::Ascii('日')), "**");
}
#[test]
fn half_blocks_of_two_colours_share_a_cell() {
let canvas = Canvas::new()
.height(1)
.width(1)
.marker(Marker::HalfBlock)
.paint(|p| {
p.points(&[(0.0, 1.0)], "red");
p.points(&[(0.0, 0.0)], "blue");
});
let g = grid(&canvas, 1);
assert_eq!(g.row_string(0), "▀");
assert_eq!(g.style_at(0, 0), Style::parse("red on blue").ok());
}
#[test]
fn ascii_consoles_get_ascii_markers_and_text() {
let console = Console::builder()
.width(4)
.color_system(None)
.ascii_only(true)
.build();
let canvas = Canvas::new().height(1).paint(|p| {
p.line(0.0, 0.5, 1.0, 0.5, "");
p.print(0.0, 0.5, "日", "");
});
assert_eq!(console.render_export(&canvas), "??**\n");
let blocks = canvas.marker(Marker::HalfBlock);
assert_eq!(console.render_export(&blocks), "??##\n");
}
#[test]
fn text_is_drawn_last_and_cut_at_the_edge() {
let canvas = ascii('#').paint(|p| {
p.print(7.0, 4.0, "label", "");
p.layer();
p.line(0.0, 4.0, 9.0, 4.0, "");
p.print(0.0, 0.0, "日本", "");
p.print(9.0, 0.0, "日", "");
});
assert_eq!(
rows(&canvas, 10),
vec![
"#######lab",
" ",
" ",
" ",
"日本 "
]
);
}
#[test]
fn circles_and_rectangles() {
let canvas = Canvas::new()
.x_bounds(-2.0, 2.0)
.y_bounds(-2.0, 2.0)
.height(5)
.width(5)
.marker(Marker::Ascii('o'))
.paint(|p| {
p.circle(0.0, 0.0, 2.0, "");
});
let out = rows(&canvas, 5);
assert_eq!(out[2], "o oo");
assert!(out[0].contains('o') && out[4].contains('o'));
}
#[test]
fn every_marker_draws_a_diagonal() {
let diagonal = |marker| {
let canvas = Canvas::new().width(6).height(3).marker(marker).paint(|p| {
p.line(0.0, 0.0, 1.0, 1.0, "");
});
rows(&canvas, 6)
};
let expect = |a: &str, b: &str| {
vec![
format!(" {a}{b}"),
format!(" {a}{b} "),
format!("{a}{b} "),
]
};
assert_eq!(diagonal(Marker::Braille), expect("⡠", "⠊"));
assert_eq!(diagonal(Marker::HalfBlock), expect("▄", "▀"));
assert_eq!(diagonal(Marker::Block), expect("█", "█"));
assert_eq!(diagonal(Marker::Bar), expect("▄", "▄"));
assert_eq!(diagonal(Marker::Quadrant), expect("▄", "▀"));
assert_eq!(diagonal(Marker::Sextant), expect("\u{1FB16}", "\u{1FB05}"));
assert_eq!(diagonal(Marker::Octant), expect("\u{1CD51}", "\u{1CD01}"));
assert_eq!(diagonal(Marker::Dot), expect("•", "•"));
let console = Console::builder()
.width(6)
.color_system(None)
.ascii_only(true)
.build();
for marker in [
Marker::Bar,
Marker::Quadrant,
Marker::Sextant,
Marker::Octant,
] {
let canvas = Canvas::new().width(6).height(1).marker(marker).paint(|p| {
p.line(0.0, 0.5, 1.0, 0.5, "");
});
assert_eq!(console.render_export(&canvas), "######\n", "{marker:?}");
}
}
#[test]
fn pattern_cells_take_the_last_style_drawn_in_them() {
let canvas = Canvas::new()
.width(1)
.height(1)
.marker(Marker::Quadrant)
.paint(|p| {
p.point(0.0, 1.0, "red");
p.point(1.0, 0.0, "blue");
});
let g = grid(&canvas, 1);
assert_eq!(g.row_string(0), "▚");
assert_eq!(g.style_at(0, 0), Style::parse("blue").ok());
}
fn near(rows: &[String], col: usize, row: usize) -> bool {
(row.saturating_sub(1)..=row + 1).any(|r| {
rows.get(r).is_some_and(|line| {
line.chars()
.enumerate()
.any(|(c, ch)| c + 1 >= col && c <= col + 1 && ch != ' ')
})
})
}
#[test]
fn the_map_lights_known_coasts() {
for resolution in [MapResolution::Low, MapResolution::High] {
let world = Canvas::new()
.x_bounds(-180.0, 180.0)
.y_bounds(-90.0, 90.0)
.width(80)
.height(20)
.paint(|p| {
p.map(resolution, "green");
});
let out = rows(&world, 80);
let cell = |lon: f64, lat: f64| {
let x = ((lon + 180.0) * 159.0 / 360.0) as usize / 2;
let y = ((90.0 - lat) * 79.0 / 180.0) as usize / 4;
(x, y)
};
for (place, lon, lat) in [
("Sydney", 151.2, -33.9),
("Perth", 115.9, -31.9),
("Darwin", 130.8, -12.5),
("Cape Town", 18.4, -33.9),
("Lisbon", -9.1, 38.7),
("Rio de Janeiro", -43.2, -22.9),
("San Francisco", -122.4, 37.8),
] {
let (x, y) = cell(lon, lat);
assert!(near(&out, x, y), "{resolution:?}: {place} at {x}, {y}");
}
for (lon, lat) in [(-140.0, -10.0), (-140.0, 20.0), (134.0, -25.0)] {
let (x, y) = cell(lon, lat);
assert_eq!(
out[y].chars().nth(x),
Some(' '),
"{resolution:?} {lon} {lat}"
);
}
let g = grid(&world, 80);
let (x, y) = cell(151.2, -33.9);
let drawn = (y - 1..=y + 1)
.flat_map(|r| (x - 1..=x + 1).map(move |c| (r, c)))
.find(|&(r, c)| g.row_string(r).chars().nth(c) != Some(' '))
.unwrap();
assert_eq!(g.style_at(drawn.0, drawn.1), Style::parse("green").ok());
}
}
#[test]
fn a_background_goes_under_everything() {
let canvas = Canvas::new()
.width(3)
.height(1)
.marker(Marker::Block)
.background("navy_blue")
.paint(|p| {
p.point(0.0, 0.5, "red");
});
let g = grid(&canvas, 3);
assert_eq!(g.row_string(0), "█ ");
assert_eq!(g.style_at(0, 0), Style::parse("red on navy_blue").ok());
assert_eq!(g.style_at(0, 1), Style::parse("on navy_blue").ok());
let console = Console::builder()
.width(3)
.force_terminal(true)
.color_system(Some(rich::ColorSystem::Standard))
.no_color(false)
.build();
let canvas = canvas.background("on blue");
assert_eq!(
console.render_export(&canvas),
"\u{1b}[31;44m█\u{1b}[0m\u{1b}[44m \u{1b}[0m\n"
);
}
#[test]
fn areas_fill_down_to_their_level() {
let canvas = Canvas::new()
.x_bounds(0.0, 4.0)
.y_bounds(0.0, 4.0)
.width(5)
.height(5)
.marker(Marker::Ascii('#'))
.paint(|p| {
p.area(0.0, 1.0, 4.0, 3.0, 0.0, "");
});
assert_eq!(
rows(&canvas, 5),
vec![" ", " ##", " ####", "#####", "#####"]
);
let above = Canvas::new()
.x_bounds(0.0, 4.0)
.y_bounds(0.0, 4.0)
.width(5)
.height(5)
.marker(Marker::Ascii('#'))
.paint(|p| {
p.area(0.0, 1.0, 4.0, 1.0, 3.0, "");
});
assert_eq!(
rows(&above, 5),
vec![" ", "#####", "#####", "#####", " "]
);
}
#[test]
fn custom_shapes_draw_with_the_painter() {
struct Square(f64);
impl Shape for Square {
fn draw(&self, painter: &mut CanvasPainter) {
let s = self.0;
painter
.rect(-s, -s, 2.0 * s, 2.0 * s, "yellow")
.point(0.0, 0.0, "red");
}
}
let canvas = Canvas::new()
.x_bounds(-2.0, 2.0)
.y_bounds(-2.0, 2.0)
.width(5)
.height(5)
.marker(Marker::Ascii('o'))
.paint(|p| {
p.shape(&Square(1.0));
});
assert_eq!(
rows(&canvas, 5),
vec![" ", " ooo ", " ooo ", " ooo ", " "]
);
let g = grid(&canvas, 5);
assert_eq!(g.style_at(2, 2), Style::parse("red").ok());
assert_eq!(g.style_at(1, 1), Style::parse("yellow").ok());
}
#[test]
fn a_canvas_fills_the_height_it_is_given() {
let console = console(10);
let canvas = Canvas::new().paint(|p| {
p.line(0.0, 0.0, 1.0, 1.0, "");
});
let lines = console.render_lines(&canvas, &console.options().update_height(7), false);
assert_eq!(lines.len(), 7);
assert!(lines.iter().all(|l| !l.is_empty()));
let short = canvas.clone().height(3);
let lines = console.render_lines(&short, &console.options().update_height(7), false);
assert_eq!(lines.iter().filter(|l| !l.is_empty()).count(), 3);
assert_eq!(console.render_export(&canvas).lines().count(), 10);
}
#[test]
fn tiny_and_empty_canvases_do_not_panic() {
let paint = |p: &mut CanvasPainter| {
p.line(0.0, 0.0, 1.0, 1.0, "red");
p.circle(0.5, 0.5, 0.5, "red");
p.print(0.5, 0.5, "text", "");
};
for (w, h) in [(0, 0), (1, 0), (0, 1), (1, 1), (2, 1)] {
for marker in [Marker::Braille, Marker::HalfBlock, Marker::Dot] {
let c = Canvas::new().width(w).height(h).marker(marker).paint(paint);
let console = console(10);
let _ = console.render_export(&c);
}
}
for (x, y) in [
([0.0, 0.0], [0.0, 1.0]),
([1.0, 0.0], [0.0, 1.0]),
([0.0, f64::INFINITY], [0.0, 1.0]),
([-f64::MAX, f64::MAX], [0.0, 1.0]),
] {
let c = Canvas::new()
.x_bounds(x[0], x[1])
.y_bounds(y[0], y[1])
.height(2)
.paint(paint);
assert!(rows(&c, 4).iter().all(|r| r.trim().is_empty()));
}
}
}