standard-plugin-sdk 0.1.0

Write Standard Code plugins in Rust: wasm components against standard:plugin@2.0.0
Documentation
//! Shapes in the cell model: box-drawing and block characters.

use super::{Area, Shapes, unit};
use crate::colour::Style;
use crate::surface::{Cells, Surface};

/// Left-aligned eighth blocks, one to seven eighths.
const EIGHTHS: [char; 7] = ['▏', '▎', '▍', '▌', '▋', '▊', '▉'];

fn put(surface: &mut Surface<Cells>, x: i64, y: i64, character: char, style: Style) {
    if (0..i64::from(u32::MAX)).contains(&x) && (0..i64::from(u32::MAX)).contains(&y) {
        surface.put(x as u32, y as u32, character, style);
    }
}

/// `[from, to)` clipped to `[0, limit)`.
fn span(from: i64, to: i64, limit: u32) -> core::ops::Range<i64> {
    from.max(0)..to.min(i64::from(limit))
}

fn frame(surface: &mut Surface<Cells>, area: Area, corners: [char; 4], style: Style) {
    if area.w == 0 || area.h == 0 {
        return;
    }
    let (cols, rows) = surface.size();
    let (left, top) = (area.left(), area.top());
    let (right, bottom) = (area.right() - 1, area.bottom() - 1);
    if area.h == 1 {
        for x in span(left, right + 1, cols) {
            put(surface, x, top, '─', style);
        }
        return;
    }
    if area.w == 1 {
        for y in span(top, bottom + 1, rows) {
            put(surface, left, y, '│', style);
        }
        return;
    }
    for x in span(left + 1, right, cols) {
        put(surface, x, top, '─', style);
        put(surface, x, bottom, '─', style);
    }
    for y in span(top + 1, bottom, rows) {
        put(surface, left, y, '│', style);
        put(surface, right, y, '│', style);
    }
    put(surface, left, top, corners[0], style);
    put(surface, right, top, corners[1], style);
    put(surface, left, bottom, corners[2], style);
    put(surface, right, bottom, corners[3], style);
}

/// Whether cell `(dx, dy)` from the centre lies inside an ellipse of radii
/// `rx` by `ry` cells.
fn inside(dx: i64, dy: i64, rx: i64, ry: i64) -> bool {
    // Cell centres against the ellipse, scaled by 4 to stay in integers.
    let (dx, dy, rx, ry) = (dx * 2, dy * 2, rx * 2 + 1, ry * 2 + 1);
    dx * dx * ry * ry + dy * dy * rx * rx <= rx * rx * ry * ry
}

fn ellipse(
    surface: &mut Surface<Cells>,
    centre: (i32, i32),
    radius: u32,
    fill: bool,
    style: Style,
) {
    let (cols, rows) = surface.size();
    let (cx, cy) = (i64::from(centre.0), i64::from(centre.1));
    let (rx, ry) = (i64::from(radius) * 2, i64::from(radius));
    for y in span(cy - ry, cy + ry + 1, rows) {
        for x in span(cx - rx, cx + rx + 1, cols) {
            let (dx, dy) = (x - cx, y - cy);
            if !inside(dx, dy, rx, ry) {
                continue;
            }
            let edge = !inside(dx - 1, dy, rx, ry)
                || !inside(dx + 1, dy, rx, ry)
                || !inside(dx, dy - 1, rx, ry)
                || !inside(dx, dy + 1, rx, ry);
            if fill {
                put(surface, x, y, '█', style);
            } else if edge {
                put(surface, x, y, '•', style);
            }
        }
    }
}

impl Shapes for Surface<Cells> {
    type Ink = Style;

    fn stroke_rect(&mut self, area: Area, ink: Style) {
        frame(self, area, ['┌', '┐', '└', '┘'], ink);
    }

    /// Full blocks in the style's foreground. For a background panel use
    /// [`Surface::fill`] with a space and a background colour.
    fn fill_rect(&mut self, area: Area, ink: Style) {
        let (cols, rows) = self.size();
        for y in span(area.top(), area.bottom(), rows) {
            for x in span(area.left(), area.right(), cols) {
                put(self, x, y, '█', ink);
            }
        }
    }

    fn line(&mut self, from: (i32, i32), to: (i32, i32), ink: Style) {
        let (cols, rows) = self.size();
        let (fx, fy) = (i64::from(from.0), i64::from(from.1));
        let (tx, ty) = (i64::from(to.0), i64::from(to.1));
        let glyph = if fy == ty {
            '─'
        } else if fx == tx {
            '│'
        } else if (tx > fx) == (ty > fy) {
            '╲'
        } else {
            '╱'
        };
        let Some(((mut x, mut y), (x1, y1))) = super::clip_segment((fx, fy), (tx, ty), cols, rows)
        else {
            return;
        };
        let (dx, dy) = ((x1 - x).abs(), -(y1 - y).abs());
        let (sx, sy) = (if x < x1 { 1 } else { -1 }, if y < y1 { 1 } else { -1 });
        let mut error = dx + dy;
        loop {
            put(self, x, y, glyph, ink);
            if x == x1 && y == y1 {
                break;
            }
            let doubled = 2 * error;
            if doubled >= dy {
                error += dy;
                x += sx;
            }
            if doubled <= dx {
                error += dx;
                y += sy;
            }
        }
    }

    /// Full blocks in `fill`, one eighth block for the partial cell, and
    /// `░` in `track` for the rest.
    fn bar(&mut self, area: Area, fraction: f32, fill: Style, track: Style) {
        let (cols, rows) = self.size();
        let eighths = (unit(fraction) * area.w as f32 * 8.0 + 0.5) as i64;
        for y in span(area.top(), area.bottom(), rows) {
            for x in span(area.left(), area.right(), cols) {
                let here = (eighths - (x - area.left()) * 8).clamp(0, 8);
                match here {
                    8 => put(self, x, y, '█', fill),
                    0 => put(self, x, y, '░', track),
                    partial => put(
                        self,
                        x,
                        y,
                        EIGHTHS[partial as usize - 1],
                        fill.with_bg(track.bg),
                    ),
                }
            }
        }
    }

    fn rounded_rect(&mut self, area: Area, radius: u32, ink: Style) {
        let corners = if radius == 0 {
            ['┌', '┐', '└', '┘']
        } else {
            ['╭', '╮', '╰', '╯']
        };
        frame(self, area, corners, ink);
    }

    /// Full blocks, with quarter blocks in the corners when `radius > 0`.
    fn fill_rounded_rect(&mut self, area: Area, radius: u32, ink: Style) {
        self.fill_rect(area, ink);
        if radius == 0 || area.w < 2 || area.h < 2 {
            return;
        }
        let (right, bottom) = (area.right() - 1, area.bottom() - 1);
        put(self, area.left(), area.top(), '▗', ink);
        put(self, right, area.top(), '▖', ink);
        put(self, area.left(), bottom, '▝', ink);
        put(self, right, bottom, '▘', ink);
    }

    fn circle(&mut self, centre: (f32, f32), radius: f32, ink: Style) {
        ellipse(self, cell_centre(centre), cell_radius(radius), false, ink);
    }

    fn fill_circle(&mut self, centre: (f32, f32), radius: f32, ink: Style) {
        ellipse(self, cell_centre(centre), cell_radius(radius), true, ink);
    }

    fn label(&mut self, x: i32, y: i32, text: &str, ink: Style) -> u32 {
        crate::surface::write_text_clipped(self, i64::from(x), i64::from(y), text, ink)
    }
}

/// The cell a continuous point lies in.
fn cell_centre((x, y): (f32, f32)) -> (i32, i32) {
    let whole = |v: f32| {
        if v.is_nan() {
            0
        } else {
            libm::floorf(v).clamp(i32::MIN as f32, i32::MAX as f32) as i32
        }
    };
    (whole(x), whole(y))
}

fn cell_radius(radius: f32) -> u32 {
    if radius.is_nan() {
        0
    } else {
        libm::roundf(radius).clamp(0.0, u32::MAX as f32) as u32
    }
}