denise 0.15.0

Direct-rendering UI toolkit for embedded Linux and systems without a desktop environment.
Documentation
//! Straight (non-premultiplied) 8-bit-per-channel colour.

/// An sRGB colour with straight alpha.
///
/// Alpha is *not* premultiplied here. Premultiplication happens in the rasteriser,
/// where it can be done once per draw call rather than once per pixel.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Color {
    /// Red channel.
    pub r: u8,
    /// Green channel.
    pub g: u8,
    /// Blue channel.
    pub b: u8,
    /// Alpha channel. `255` is opaque.
    pub a: u8,
}

impl Color {
    /// Fully transparent.
    pub const TRANSPARENT: Self = Self::rgba(0, 0, 0, 0);
    /// Opaque black.
    pub const BLACK: Self = Self::rgb(0, 0, 0);
    /// Opaque white.
    pub const WHITE: Self = Self::rgb(255, 255, 255);

    /// An opaque colour.
    #[inline]
    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
        Self { r, g, b, a: 255 }
    }

    /// A colour with straight alpha.
    #[inline]
    pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
        Self { r, g, b, a }
    }

    /// Decodes a `0xAARRGGBB` word.
    #[inline]
    pub const fn from_argb8888(v: u32) -> Self {
        Self {
            a: (v >> 24) as u8,
            r: (v >> 16) as u8,
            g: (v >> 8) as u8,
            b: v as u8,
        }
    }

    /// Decodes a `0xRRGGBB` word as opaque.
    #[inline]
    pub const fn from_rgb888(v: u32) -> Self {
        Self::from_argb8888(v | 0xFF00_0000)
    }

    /// Encodes to the `0xAARRGGBB` word layout used by [`crate::PixelFormat`].
    #[inline]
    pub const fn to_argb8888(self) -> u32 {
        (self.a as u32) << 24 | (self.r as u32) << 16 | (self.g as u32) << 8 | self.b as u32
    }

    /// Returns `true` if the colour needs no blending.
    #[inline]
    pub const fn is_opaque(self) -> bool {
        self.a == 255
    }

    /// Returns `true` if the colour contributes nothing.
    #[inline]
    pub const fn is_transparent(self) -> bool {
        self.a == 0
    }

    /// This colour at a different alpha.
    #[inline]
    pub const fn with_alpha(self, a: u8) -> Color {
        Color { a, ..self }
    }

    /// Blends towards `other`. `t` of `0` is `self`, `255` is `other`.
    ///
    /// Mixed in gamma-encoded sRGB, which is what CSS `color-mix(in srgb, ...)`
    /// does and what a theme author picking two hex values expects. Perceptually
    /// even ramps want a different space; see [`crate::theme`].
    #[inline]
    pub const fn mix(self, other: Color, t: u8) -> Color {
        Color {
            r: lerp(self.r, other.r, t),
            g: lerp(self.g, other.g, t),
            b: lerp(self.b, other.b, t),
            a: lerp(self.a, other.a, t),
        }
    }
}

/// Rounded linear interpolation. Exact at both ends.
#[inline]
const fn lerp(a: u8, b: u8, t: u8) -> u8 {
    let (a, b, t) = (a as u32, b as u32, t as u32);
    ((a * (255 - t) + b * t + 127) / 255) as u8
}

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

    #[test]
    fn argb_round_trip() {
        let c = Color::rgba(0x12, 0x34, 0x56, 0x78);
        assert_eq!(c.to_argb8888(), 0x7812_3456);
        assert_eq!(Color::from_argb8888(0x7812_3456), c);
    }

    #[test]
    fn rgb_is_opaque() {
        assert!(Color::from_rgb888(0x1E1E2E).is_opaque());
        assert_eq!(Color::from_rgb888(0x1E1E2E).to_argb8888(), 0xFF1E_1E2E);
    }
}