standard-plugin-sdk 0.1.0

Write Standard Code plugins in Rust: wasm components against standard:plugin@2.0.0
Documentation
//! The cell model: one grapheme, two colours and attributes per cell.
//!
//! A cell stores one Unicode scalar value, the grapheme's first. Wide
//! characters take two cells: the character in the left one and an empty
//! "spacer" to its right, as the viewer lays them out. The viewer's rule
//! decides a character's width (its scalar's East Asian width: 1 or 2);
//! control, bidi-control and zero-width characters never reach a cell.
//! Combining marks, variation selectors and joined sequences are dropped
//! with the rest of the grapheme (the grapheme pool that would carry them is
//! reserved in the contract, not implemented).

use unicode_segmentation::UnicodeSegmentation as _;
use unicode_width::UnicodeWidthChar as _;

use super::{Cells, Inner, Surface};
use crate::colour::{Attrs, Colour, Style};
use crate::geometry::Rect;

const CELL_WORDS: usize = 4;

/// One cell, read back from the buffer.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Cell {
    /// `None` paints as empty (a space in the cell's background); the right
    /// half of a wide character is empty too.
    pub grapheme: Option<char>,
    pub style: Style,
}

/// Mirrors the viewer: C0, DEL and C1 controls, and the bidi controls a
/// cell must never carry.
fn is_forbidden(character: char) -> bool {
    character.is_control()
        || matches!(
            character,
            '\u{061c}' | '\u{200e}' | '\u{200f}' | '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}'
        )
}

/// The columns a character takes in a cell, as the viewer decides: 1 or 2,
/// or 0 for what a cell never shows.
pub(crate) fn char_width(character: char) -> u32 {
    if is_forbidden(character) {
        return 0;
    }
    match character.width() {
        Some(1) => 1,
        Some(2) => 2,
        _ => 0,
    }
}

fn word_width(word: u32) -> u32 {
    if word == 0 || word >= 0x8000_0000 {
        return 0;
    }
    char::from_u32(word).map_or(0, char_width)
}

struct Grid<'a> {
    slot: &'a mut [u32],
    cols: u32,
    rows: u32,
}

impl Grid<'_> {
    fn index(&self, x: u32, y: u32) -> usize {
        (y as usize * self.cols as usize + x as usize) * CELL_WORDS
    }

    fn grapheme(&self, x: u32, y: u32) -> u32 {
        self.slot[self.index(x, y)]
    }

    fn write(&mut self, x: u32, y: u32, grapheme: u32, style: Style) {
        let index = self.index(x, y);
        self.slot[index..index + CELL_WORDS].copy_from_slice(&[
            grapheme,
            style.fg.encode(),
            style.bg.encode(),
            u32::from(style.attrs.0),
        ]);
    }

    /// Writes `character` of `width` columns at `(x, y)` (in bounds, the
    /// right half too) and returns the rectangle the viewer must re-read:
    /// the cells written, and neighbours whose wide character the write
    /// split.
    fn put(&mut self, x: u32, y: u32, character: u32, width: u32, style: Style) -> Rect {
        let mut left = x;
        let mut right = x + width;
        // Overwriting the right half of a wide character: its left half
        // would still claim this cell, so it becomes empty.
        if x > 0 && word_width(self.grapheme(x - 1, y)) == 2 {
            let index = self.index(x - 1, y);
            self.slot[index] = 0;
            left = x - 1;
        }
        // Overwriting the left half of a wide character whose right half
        // this write does not cover: that spacer must be re-read as empty.
        let last = x + width - 1;
        if word_width(self.grapheme(last, y)) == 2 && last + 1 < self.cols {
            right = last + 2;
        }
        self.write(x, y, character, style);
        if width == 2 {
            self.write(x + 1, y, 0, style);
        }
        Rect::new(left, y, right - left, 1)
    }
}

impl Inner {
    fn grid(&mut self) -> Option<(Grid<'_>, &mut super::DirtyRects)> {
        let (slot, layout, dirty) = self.back()?;
        let g = layout.geometry;
        Some((
            Grid {
                slot,
                cols: g.cols,
                rows: g.rows,
            },
            dirty,
        ))
    }
}

impl Surface<Cells> {
    /// Writes one character at `(x, y)`. Returns the columns it took: 1,
    /// 2 for a wide character, 0 when nothing was written (out of bounds,
    /// or a character a cell never shows). A wide character that does not
    /// fit before the right edge leaves one empty cell instead (the
    /// viewer would drop it).
    pub fn put(&mut self, x: u32, y: u32, character: char, style: Style) -> u32 {
        let mut inner = self.inner.borrow_mut();
        let Some((mut grid, dirty)) = inner.grid() else {
            return 0;
        };
        put_char(&mut grid, dirty, x, y, character, style)
    }

    /// Writes `text` from `(x, y)` along one row, grapheme by grapheme,
    /// clipped at the right edge. Returns the columns written. Each
    /// grapheme shows as its first character (see the module docs).
    pub fn text(&mut self, x: u32, y: u32, text: &str, style: Style) -> u32 {
        write_text_clipped(self, i64::from(x), i64::from(y), text, style)
    }

    /// Fills `rect` with `character` in `style` (clipped).
    pub fn fill(&mut self, rect: Rect, character: char, style: Style) {
        let mut inner = self.inner.borrow_mut();
        let Some((mut grid, dirty)) = inner.grid() else {
            return;
        };
        let Some(rect) = rect.clamp(grid.cols, grid.rows) else {
            return;
        };
        let step = char_width(character).max(1);
        for y in rect.y..rect.bottom() {
            let mut x = rect.x;
            while x < rect.right() {
                if x + step > rect.right() {
                    put_char(&mut grid, dirty, x, y, ' ', style);
                    break;
                }
                put_char(&mut grid, dirty, x, y, character, style);
                x += step;
            }
        }
    }

    /// Empties every cell (theme text on a transparent background). The
    /// whole surface is dirty; the previous frame is not copied forward.
    pub fn clear(&mut self) {
        self.clear_with(Style::DEFAULT);
    }

    /// Empties every cell with `style` (its background shows).
    pub fn clear_with(&mut self, style: Style) {
        let mut inner = self.inner.borrow_mut();
        let Some((slot, _)) = inner.back_discard() else {
            return;
        };
        let words = [
            0,
            style.fg.encode(),
            style.bg.encode(),
            u32::from(style.attrs.0),
        ];
        for cell in slot.chunks_exact_mut(CELL_WORDS) {
            cell.copy_from_slice(&words);
        }
    }

    /// The cell at `(x, y)` as it will be committed next.
    pub fn cell(&self, x: u32, y: u32) -> Option<Cell> {
        let mut inner = self.inner.borrow_mut();
        let (grid, _) = inner.grid()?;
        if x >= grid.cols || y >= grid.rows {
            return None;
        }
        let index = grid.index(x, y);
        let words = &grid.slot[index..index + CELL_WORDS];
        Some(Cell {
            grapheme: (word_width(words[0]) > 0)
                .then(|| char::from_u32(words[0]))
                .flatten(),
            style: Style {
                fg: Colour::decode(words[1]),
                bg: Colour::decode(words[2]),
                attrs: Attrs(words[3] as u16),
            },
        })
    }

    /// One row as text: each cell's character, a space for an empty cell,
    /// nothing for the right half of a wide character. For tests.
    pub fn row_text(&self, y: u32) -> alloc::string::String {
        let (cols, _) = self.size();
        let mut text = alloc::string::String::new();
        let mut x = 0;
        while x < cols {
            let Some(cell) = self.cell(x, y) else {
                break;
            };
            match cell.grapheme {
                Some(character) => {
                    text.push(character);
                    x += char_width(character);
                }
                None => {
                    text.push(' ');
                    x += 1;
                }
            }
        }
        text
    }
}

fn put_char(
    grid: &mut Grid<'_>,
    dirty: &mut super::DirtyRects,
    x: u32,
    y: u32,
    character: char,
    style: Style,
) -> u32 {
    if x >= grid.cols || y >= grid.rows {
        return 0;
    }
    let width = char_width(character);
    if width == 0 {
        return 0;
    }
    let (word, width) = if x + width > grid.cols {
        (0, 1)
    } else {
        (character as u32, width)
    };
    let rect = grid.put(x, y, word, width, style);
    dirty.add(rect, grid.cols, grid.rows);
    width
}

/// Writes `text` from a column that may lie left of the surface; what
/// falls outside is clipped. Returns the columns written.
pub(crate) fn write_text_clipped(
    surface: &mut Surface<Cells>,
    x: i64,
    y: i64,
    text: &str,
    style: Style,
) -> u32 {
    let mut inner = surface.inner.borrow_mut();
    let Some((mut grid, dirty)) = inner.grid() else {
        return 0;
    };
    if y < 0 || y >= i64::from(grid.rows) {
        return 0;
    }
    let mut column = x;
    let mut written = 0;
    for grapheme in text.graphemes(true) {
        let Some(character) = grapheme.chars().next() else {
            continue;
        };
        let width = char_width(character);
        if width == 0 {
            continue;
        }
        if column >= i64::from(grid.cols) {
            break;
        }
        if column >= 0 {
            written += put_char(&mut grid, dirty, column as u32, y as u32, character, style);
        } else if column + i64::from(width) > 0 {
            // A wide character straddling the left edge: its visible half
            // is an empty cell.
            written += put_char(&mut grid, dirty, 0, y as u32, ' ', style);
        }
        column += i64::from(width);
    }
    written
}