Skip to main content

standard_plugin/
colour.rs

1//! Colours, attributes and styles for the cell model, and RGBA for the
2//! graphics model.
3
4use core::ops::{BitOr, BitOrAssign};
5
6/// A theme colour slot: the viewer resolves it to its current theme, so a
7/// plugin follows the user's terminal colours and "receded" (grayed out)
8/// text stays readable on every background.
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
10pub struct ThemeSlot(pub u8);
11
12impl ThemeSlot {
13    /// The theme's text colour.
14    pub const FG: Self = Self(0);
15    /// The theme's background.
16    pub const BG: Self = Self(1);
17    /// Text receded toward the background (secondary text).
18    pub const RECEDE_FG: Self = Self(2);
19    /// A background receded toward the text (a subtle panel).
20    pub const RECEDE_BG: Self = Self(3);
21    /// The theme's accent.
22    pub const ACCENT: Self = Self(4);
23}
24
25/// A cell colour. Encoded in the buffer as a `u32` whose top byte is the
26/// model:
27///
28/// | Word | Colour |
29/// | --- | --- |
30/// | `0x00______` | [`Colour::Default`]: theme text, or a transparent background |
31/// | `0x01RRGGBB` | [`Colour::Rgb`] |
32/// | `0x020000II` | [`Colour::Indexed`]: terminal palette index |
33/// | `0x030000TT` | [`Colour::Theme`]: a [`ThemeSlot`] |
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
35pub enum Colour {
36    #[default]
37    Default,
38    Rgb(u8, u8, u8),
39    Indexed(u8),
40    Theme(ThemeSlot),
41}
42
43impl Colour {
44    pub const FG: Self = Self::Theme(ThemeSlot::FG);
45    pub const BG: Self = Self::Theme(ThemeSlot::BG);
46    pub const RECEDE_FG: Self = Self::Theme(ThemeSlot::RECEDE_FG);
47    pub const RECEDE_BG: Self = Self::Theme(ThemeSlot::RECEDE_BG);
48    pub const ACCENT: Self = Self::Theme(ThemeSlot::ACCENT);
49
50    /// `0xRRGGBB` as an RGB colour.
51    pub const fn hex(rgb: u32) -> Self {
52        Self::Rgb((rgb >> 16) as u8, (rgb >> 8) as u8, rgb as u8)
53    }
54
55    /// The buffer word.
56    pub const fn encode(self) -> u32 {
57        match self {
58            Self::Default => 0,
59            Self::Rgb(r, g, b) => 0x0100_0000 | ((r as u32) << 16) | ((g as u32) << 8) | b as u32,
60            Self::Indexed(index) => 0x0200_0000 | index as u32,
61            Self::Theme(slot) => 0x0300_0000 | slot.0 as u32,
62        }
63    }
64
65    /// The colour a buffer word describes, as the viewer reads it: an
66    /// unknown model byte is the default colour, and bits a model does not
67    /// use are ignored.
68    pub const fn decode(word: u32) -> Self {
69        match word >> 24 {
70            1 => Self::Rgb((word >> 16) as u8, (word >> 8) as u8, word as u8),
71            2 => Self::Indexed(word as u8),
72            3 => Self::Theme(ThemeSlot(word as u8)),
73            _ => Self::Default,
74        }
75    }
76}
77
78/// `rgb` blended `percent` of the way toward `toward` (both `0xRRGGBB`),
79/// channel by channel: the viewer's recede. With the theme's background
80/// as `toward` it is "grayed out" (see [`crate::view::Theme::recede`]).
81pub const fn recede(rgb: u32, toward: u32, percent: u16) -> u32 {
82    let percent = if percent > 100 { 100 } else { percent } as u32;
83    blend_channel(rgb, toward, percent, 16)
84        | blend_channel(rgb, toward, percent, 8)
85        | blend_channel(rgb, toward, percent, 0)
86}
87
88const fn blend_channel(rgb: u32, toward: u32, percent: u32, shift: u32) -> u32 {
89    let from = (rgb >> shift) & 0xff;
90    let to = (toward >> shift) & 0xff;
91    ((from * (100 - percent) + to * percent) / 100) << shift
92}
93
94/// Cell attribute bits.
95#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
96pub struct Attrs(pub u16);
97
98impl Attrs {
99    pub const NONE: Self = Self(0);
100    pub const BOLD: Self = Self(1);
101    pub const DIM: Self = Self(1 << 1);
102    pub const ITALIC: Self = Self(1 << 2);
103    pub const UNDERLINE: Self = Self(1 << 3);
104    pub const REVERSE: Self = Self(1 << 4);
105    pub const STRIKETHROUGH: Self = Self(1 << 5);
106
107    pub const fn contains(self, other: Self) -> bool {
108        self.0 & other.0 == other.0
109    }
110}
111
112impl BitOr for Attrs {
113    type Output = Self;
114
115    fn bitor(self, rhs: Self) -> Self {
116        Self(self.0 | rhs.0)
117    }
118}
119
120impl BitOrAssign for Attrs {
121    fn bitor_assign(&mut self, rhs: Self) {
122        self.0 |= rhs.0;
123    }
124}
125
126/// How a cell paints: foreground, background, attributes.
127#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
128pub struct Style {
129    pub fg: Colour,
130    pub bg: Colour,
131    pub attrs: Attrs,
132}
133
134impl Style {
135    /// Theme text on a transparent background.
136    pub const DEFAULT: Self = Self {
137        fg: Colour::Default,
138        bg: Colour::Default,
139        attrs: Attrs::NONE,
140    };
141
142    pub const fn new() -> Self {
143        Self::DEFAULT
144    }
145
146    /// A style with this foreground.
147    pub const fn fg(colour: Colour) -> Self {
148        Self {
149            fg: colour,
150            ..Self::DEFAULT
151        }
152    }
153
154    pub const fn with_fg(mut self, colour: Colour) -> Self {
155        self.fg = colour;
156        self
157    }
158
159    pub const fn with_bg(mut self, colour: Colour) -> Self {
160        self.bg = colour;
161        self
162    }
163
164    pub const fn with_attrs(mut self, attrs: Attrs) -> Self {
165        self.attrs = Attrs(self.attrs.0 | attrs.0);
166        self
167    }
168
169    pub const fn bold(self) -> Self {
170        self.with_attrs(Attrs::BOLD)
171    }
172
173    pub const fn dim(self) -> Self {
174        self.with_attrs(Attrs::DIM)
175    }
176
177    pub const fn italic(self) -> Self {
178        self.with_attrs(Attrs::ITALIC)
179    }
180
181    pub const fn underline(self) -> Self {
182        self.with_attrs(Attrs::UNDERLINE)
183    }
184}
185
186/// One RGBA8 pixel of the graphics model (straight, not premultiplied,
187/// alpha). In the buffer: the bytes `r, g, b, a`.
188#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
189#[repr(C)]
190pub struct Rgba {
191    pub r: u8,
192    pub g: u8,
193    pub b: u8,
194    pub a: u8,
195}
196
197impl Rgba {
198    pub const TRANSPARENT: Self = Self::new(0, 0, 0, 0);
199    pub const BLACK: Self = Self::rgb(0, 0, 0);
200    pub const WHITE: Self = Self::rgb(255, 255, 255);
201
202    pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
203        Self { r, g, b, a }
204    }
205
206    /// Opaque.
207    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
208        Self::new(r, g, b, 255)
209    }
210
211    /// `0xRRGGBB`, opaque (the form [`crate::view::Theme`] reports).
212    pub const fn hex(rgb: u32) -> Self {
213        Self::rgb((rgb >> 16) as u8, (rgb >> 8) as u8, rgb as u8)
214    }
215
216    pub const fn with_alpha(mut self, a: u8) -> Self {
217        self.a = a;
218        self
219    }
220
221    /// The little-endian word the buffer stores.
222    pub const fn to_word(self) -> u32 {
223        u32::from_le_bytes([self.r, self.g, self.b, self.a])
224    }
225
226    pub const fn from_word(word: u32) -> Self {
227        let [r, g, b, a] = word.to_le_bytes();
228        Self { r, g, b, a }
229    }
230
231    /// `self` drawn over `below` with extra `coverage` (0 to 255), straight
232    /// alpha "source over".
233    pub fn over(self, below: Self, coverage: u8) -> Self {
234        let alpha = u32::from(self.a) * u32::from(coverage) / 255;
235        if alpha == 255 {
236            return self;
237        }
238        if alpha == 0 {
239            return below;
240        }
241        let below_alpha = u32::from(below.a) * (255 - alpha) / 255;
242        let out_alpha = alpha + below_alpha;
243        let channel = |top: u8, bottom: u8| {
244            ((u32::from(top) * alpha + u32::from(bottom) * below_alpha + out_alpha / 2) / out_alpha)
245                as u8
246        };
247        Self {
248            r: channel(self.r, below.r),
249            g: channel(self.g, below.g),
250            b: channel(self.b, below.b),
251            a: out_alpha as u8,
252        }
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn receding_blends_toward_the_background_and_keeps_the_hue() {
262        // The viewer's `blend_component`: integer percent per channel.
263        assert_eq!(recede(0xff8000, 0x000000, 55), 0x723900);
264        assert_eq!(recede(0x123456, 0xffffff, 0), 0x123456);
265        assert_eq!(recede(0x123456, 0xffffff, 100), 0xffffff);
266        assert_eq!(recede(0x123456, 0xffffff, 250), 0xffffff, "clamped");
267    }
268
269    #[test]
270    fn colours_round_trip_through_their_buffer_words() {
271        let mut colours = alloc::vec![Colour::Default];
272        for value in [0u8, 1, 127, 128, 254, 255] {
273            colours.push(Colour::Indexed(value));
274            colours.push(Colour::Theme(ThemeSlot(value)));
275            colours.push(Colour::Rgb(value, 255 - value, value / 2));
276        }
277        for colour in colours {
278            assert_eq!(Colour::decode(colour.encode()), colour, "{colour:?}");
279        }
280        // The exact words of the contract.
281        assert_eq!(Colour::Rgb(0x12, 0x34, 0x56).encode(), 0x0112_3456);
282        assert_eq!(Colour::Indexed(9).encode(), 0x0200_0009);
283        assert_eq!(Colour::ACCENT.encode(), 0x0300_0004);
284        assert_eq!(Colour::hex(0xabcdef), Colour::Rgb(0xab, 0xcd, 0xef));
285        // Decoding canonicalises: unused bits and unknown models.
286        assert_eq!(Colour::decode(0x02ff_ff07), Colour::Indexed(7));
287        assert_eq!(Colour::decode(0x0412_3456), Colour::Default);
288        assert_eq!(Colour::decode(0x00ab_cdef), Colour::Default);
289        for word in [0u32, 0x0100_0000, 0x01ff_ffff, 0x0200_00ff, 0x0300_0004] {
290            assert_eq!(Colour::decode(word).encode(), word);
291        }
292        let pixel = Rgba::new(1, 2, 3, 4);
293        assert_eq!(Rgba::from_word(pixel.to_word()), pixel);
294        assert_eq!(pixel.to_word().to_le_bytes(), [1, 2, 3, 4]);
295    }
296}