standard-plugin-sdk 0.1.1

Write Standard Code plugins in Rust: wasm components against standard:plugin@2.0.0
Documentation
//! Colours, attributes and styles for the cell model, and RGBA for the
//! graphics model.

use core::ops::{BitOr, BitOrAssign};

/// A theme colour slot: the viewer resolves it to its current theme, so a
/// plugin follows the user's terminal colours and "receded" (grayed out)
/// text stays readable on every background.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct ThemeSlot(pub u8);

impl ThemeSlot {
    /// The theme's text colour.
    pub const FG: Self = Self(0);
    /// The theme's background.
    pub const BG: Self = Self(1);
    /// Text receded toward the background (secondary text).
    pub const RECEDE_FG: Self = Self(2);
    /// A background receded toward the text (a subtle panel).
    pub const RECEDE_BG: Self = Self(3);
    /// The theme's accent.
    pub const ACCENT: Self = Self(4);
}

/// A cell colour. Encoded in the buffer as a `u32` whose top byte is the
/// model:
///
/// | Word | Colour |
/// | --- | --- |
/// | `0x00______` | [`Colour::Default`]: theme text, or a transparent background |
/// | `0x01RRGGBB` | [`Colour::Rgb`] |
/// | `0x020000II` | [`Colour::Indexed`]: terminal palette index |
/// | `0x030000TT` | [`Colour::Theme`]: a [`ThemeSlot`] |
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Colour {
    #[default]
    Default,
    Rgb(u8, u8, u8),
    Indexed(u8),
    Theme(ThemeSlot),
}

impl Colour {
    pub const FG: Self = Self::Theme(ThemeSlot::FG);
    pub const BG: Self = Self::Theme(ThemeSlot::BG);
    pub const RECEDE_FG: Self = Self::Theme(ThemeSlot::RECEDE_FG);
    pub const RECEDE_BG: Self = Self::Theme(ThemeSlot::RECEDE_BG);
    pub const ACCENT: Self = Self::Theme(ThemeSlot::ACCENT);

    /// `0xRRGGBB` as an RGB colour.
    pub const fn hex(rgb: u32) -> Self {
        Self::Rgb((rgb >> 16) as u8, (rgb >> 8) as u8, rgb as u8)
    }

    /// The buffer word.
    pub const fn encode(self) -> u32 {
        match self {
            Self::Default => 0,
            Self::Rgb(r, g, b) => 0x0100_0000 | ((r as u32) << 16) | ((g as u32) << 8) | b as u32,
            Self::Indexed(index) => 0x0200_0000 | index as u32,
            Self::Theme(slot) => 0x0300_0000 | slot.0 as u32,
        }
    }

    /// The colour a buffer word describes, as the viewer reads it: an
    /// unknown model byte is the default colour, and bits a model does not
    /// use are ignored.
    pub const fn decode(word: u32) -> Self {
        match word >> 24 {
            1 => Self::Rgb((word >> 16) as u8, (word >> 8) as u8, word as u8),
            2 => Self::Indexed(word as u8),
            3 => Self::Theme(ThemeSlot(word as u8)),
            _ => Self::Default,
        }
    }
}

/// `rgb` blended `percent` of the way toward `toward` (both `0xRRGGBB`),
/// channel by channel: the viewer's recede. With the theme's background
/// as `toward` it is "grayed out" (see [`crate::view::Theme::recede`]).
pub const fn recede(rgb: u32, toward: u32, percent: u16) -> u32 {
    let percent = if percent > 100 { 100 } else { percent } as u32;
    blend_channel(rgb, toward, percent, 16)
        | blend_channel(rgb, toward, percent, 8)
        | blend_channel(rgb, toward, percent, 0)
}

const fn blend_channel(rgb: u32, toward: u32, percent: u32, shift: u32) -> u32 {
    let from = (rgb >> shift) & 0xff;
    let to = (toward >> shift) & 0xff;
    ((from * (100 - percent) + to * percent) / 100) << shift
}

/// Cell attribute bits.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Attrs(pub u16);

impl Attrs {
    pub const NONE: Self = Self(0);
    pub const BOLD: Self = Self(1);
    pub const DIM: Self = Self(1 << 1);
    pub const ITALIC: Self = Self(1 << 2);
    pub const UNDERLINE: Self = Self(1 << 3);
    pub const REVERSE: Self = Self(1 << 4);
    pub const STRIKETHROUGH: Self = Self(1 << 5);

    pub const fn contains(self, other: Self) -> bool {
        self.0 & other.0 == other.0
    }
}

impl BitOr for Attrs {
    type Output = Self;

    fn bitor(self, rhs: Self) -> Self {
        Self(self.0 | rhs.0)
    }
}

impl BitOrAssign for Attrs {
    fn bitor_assign(&mut self, rhs: Self) {
        self.0 |= rhs.0;
    }
}

/// How a cell paints: foreground, background, attributes.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Style {
    pub fg: Colour,
    pub bg: Colour,
    pub attrs: Attrs,
}

impl Style {
    /// Theme text on a transparent background.
    pub const DEFAULT: Self = Self {
        fg: Colour::Default,
        bg: Colour::Default,
        attrs: Attrs::NONE,
    };

    pub const fn new() -> Self {
        Self::DEFAULT
    }

    /// A style with this foreground.
    pub const fn fg(colour: Colour) -> Self {
        Self {
            fg: colour,
            ..Self::DEFAULT
        }
    }

    pub const fn with_fg(mut self, colour: Colour) -> Self {
        self.fg = colour;
        self
    }

    pub const fn with_bg(mut self, colour: Colour) -> Self {
        self.bg = colour;
        self
    }

    pub const fn with_attrs(mut self, attrs: Attrs) -> Self {
        self.attrs = Attrs(self.attrs.0 | attrs.0);
        self
    }

    pub const fn bold(self) -> Self {
        self.with_attrs(Attrs::BOLD)
    }

    pub const fn dim(self) -> Self {
        self.with_attrs(Attrs::DIM)
    }

    pub const fn italic(self) -> Self {
        self.with_attrs(Attrs::ITALIC)
    }

    pub const fn underline(self) -> Self {
        self.with_attrs(Attrs::UNDERLINE)
    }
}

/// One RGBA8 pixel of the graphics model (straight, not premultiplied,
/// alpha). In the buffer: the bytes `r, g, b, a`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[repr(C)]
pub struct Rgba {
    pub r: u8,
    pub g: u8,
    pub b: u8,
    pub a: u8,
}

impl Rgba {
    pub const TRANSPARENT: Self = Self::new(0, 0, 0, 0);
    pub const BLACK: Self = Self::rgb(0, 0, 0);
    pub const WHITE: Self = Self::rgb(255, 255, 255);

    pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
        Self { r, g, b, a }
    }

    /// Opaque.
    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
        Self::new(r, g, b, 255)
    }

    /// `0xRRGGBB`, opaque (the form [`crate::view::Theme`] reports).
    pub const fn hex(rgb: u32) -> Self {
        Self::rgb((rgb >> 16) as u8, (rgb >> 8) as u8, rgb as u8)
    }

    pub const fn with_alpha(mut self, a: u8) -> Self {
        self.a = a;
        self
    }

    /// The little-endian word the buffer stores.
    pub const fn to_word(self) -> u32 {
        u32::from_le_bytes([self.r, self.g, self.b, self.a])
    }

    pub const fn from_word(word: u32) -> Self {
        let [r, g, b, a] = word.to_le_bytes();
        Self { r, g, b, a }
    }

    /// `self` drawn over `below` with extra `coverage` (0 to 255), straight
    /// alpha "source over".
    pub fn over(self, below: Self, coverage: u8) -> Self {
        let alpha = u32::from(self.a) * u32::from(coverage) / 255;
        if alpha == 255 {
            return self;
        }
        if alpha == 0 {
            return below;
        }
        let below_alpha = u32::from(below.a) * (255 - alpha) / 255;
        let out_alpha = alpha + below_alpha;
        let channel = |top: u8, bottom: u8| {
            ((u32::from(top) * alpha + u32::from(bottom) * below_alpha + out_alpha / 2) / out_alpha)
                as u8
        };
        Self {
            r: channel(self.r, below.r),
            g: channel(self.g, below.g),
            b: channel(self.b, below.b),
            a: out_alpha as u8,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn receding_blends_toward_the_background_and_keeps_the_hue() {
        // The viewer's `blend_component`: integer percent per channel.
        assert_eq!(recede(0xff8000, 0x000000, 55), 0x723900);
        assert_eq!(recede(0x123456, 0xffffff, 0), 0x123456);
        assert_eq!(recede(0x123456, 0xffffff, 100), 0xffffff);
        assert_eq!(recede(0x123456, 0xffffff, 250), 0xffffff, "clamped");
    }

    #[test]
    fn colours_round_trip_through_their_buffer_words() {
        let mut colours = alloc::vec![Colour::Default];
        for value in [0u8, 1, 127, 128, 254, 255] {
            colours.push(Colour::Indexed(value));
            colours.push(Colour::Theme(ThemeSlot(value)));
            colours.push(Colour::Rgb(value, 255 - value, value / 2));
        }
        for colour in colours {
            assert_eq!(Colour::decode(colour.encode()), colour, "{colour:?}");
        }
        // The exact words of the contract.
        assert_eq!(Colour::Rgb(0x12, 0x34, 0x56).encode(), 0x0112_3456);
        assert_eq!(Colour::Indexed(9).encode(), 0x0200_0009);
        assert_eq!(Colour::ACCENT.encode(), 0x0300_0004);
        assert_eq!(Colour::hex(0xabcdef), Colour::Rgb(0xab, 0xcd, 0xef));
        // Decoding canonicalises: unused bits and unknown models.
        assert_eq!(Colour::decode(0x02ff_ff07), Colour::Indexed(7));
        assert_eq!(Colour::decode(0x0412_3456), Colour::Default);
        assert_eq!(Colour::decode(0x00ab_cdef), Colour::Default);
        for word in [0u32, 0x0100_0000, 0x01ff_ffff, 0x0200_00ff, 0x0300_0004] {
            assert_eq!(Colour::decode(word).encode(), word);
        }
        let pixel = Rgba::new(1, 2, 3, 4);
        assert_eq!(Rgba::from_word(pixel.to_word()), pixel);
        assert_eq!(pixel.to_word().to_le_bytes(), [1, 2, 3, 4]);
    }
}