simple-color 0.2.10

A simple color struct
Documentation
#![deny(missing_docs)]

//! A minimal color handling library.
//!
//! Parsing and serialization are optionally supported.
//! All color constructors support `const` evaluation.
//!
//! # Basic Usage
//!
//! ```rust
//! use simple_color::Color;
//!
//! // Predefined colors
//! let white = Color::WHITE;
//! let red = Color::RED;
//!
//! // Custom colors
//! let yellow = Color::rgb(0xFF, 0xFF, 0);
//! let transparent_blue = Color::rgba(0, 0, 0xFF, 0x80);
//! ```

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use thiserror::Error;

/// Error type for color parsing failures
#[derive(Copy, Clone, PartialEq, Eq, Debug, Error)]
pub enum ColorParsingError {
    /// Contains the invalid character found during parsing
    #[error("Invalid hex character '{0}'")]
    InvalidChar(char),

    /// Contains the unexpected input length
    #[error("Invalid length {0} - expected 1, 2, 3, 4, 6 or 8")]
    InvalidLength(usize),
}

/// RGBA color representation with 8-bit components
#[must_use]
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Color {
    /// Red component
    pub r: u8,
    /// Green component
    pub g: u8,
    /// Blue component
    pub b: u8,
    /// Alpha component
    pub a: u8,
}

impl Color {
    /// White (FFFFFF)
    pub const WHITE: Self = Self {
        r: 0xFF,
        g: 0xFF,
        b: 0xFF,
        a: 0xFF,
    };

    /// Black (000000)
    pub const BLACK: Self = Self {
        r: 0,
        g: 0,
        b: 0,
        a: 0xFF,
    };

    /// Red (FF0000)
    pub const RED: Self = Self {
        r: 0xFF,
        g: 0,
        b: 0,
        a: 0xFF,
    };

    /// Green (00FF00)
    pub const GREEN: Self = Self {
        r: 0,
        g: 0xFF,
        b: 0,
        a: 0xFF,
    };

    /// Blue (0000FF)
    pub const BLUE: Self = Self {
        r: 0,
        g: 0,
        b: 0xFF,
        a: 0xFF,
    };

    /// Cyan (00FFFF)
    pub const CYAN: Self = Self {
        r: 0,
        g: 0xFF,
        b: 0xFF,
        a: 0xFF,
    };

    /// Magenta (FF00FF)
    pub const MAGENTA: Self = Self {
        r: 0xFF,
        g: 0,
        b: 0xFF,
        a: 0xFF,
    };

    /// Yellow (FFFF00)
    pub const YELLOW: Self = Self {
        r: 0xFF,
        g: 0xFF,
        b: 0,
        a: 0xFF,
    };

    /// Create a grayscale color from a single intensity value
    ///
    /// # Arguments
    ///
    /// * `shade` - Gray intensity
    ///
    /// # Example
    ///
    /// ```
    /// use simple_color::Color;
    ///
    /// let gray = Color::gray(0x80);
    /// let dark_gray = Color::gray(0x40);
    /// let light_gray = Color::gray(0xC0);
    /// let black = Color::gray(0);
    /// let white = Color::gray(0xFF);
    ///
    /// assert_eq!(gray, Color::rgb(0x80, 0x80, 0x80));
    /// assert_eq!(black, Color::BLACK);
    /// assert_eq!(white, Color::WHITE);
    /// ```
    pub const fn gray(shade: u8) -> Self {
        Self {
            r: shade,
            g: shade,
            b: shade,
            a: 0xFF,
        }
    }

    /// Create an opaque RGB color
    ///
    /// # Arguments
    ///
    /// * `r` - Red component
    /// * `g` - Green component
    /// * `b` - Blue component
    ///
    /// # Example
    ///
    /// ```
    /// use simple_color::Color;
    ///
    /// let cyan = Color::rgb(0, 0xFF, 0xFF);
    /// assert_eq!(cyan, Color::CYAN);
    /// ```
    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
        Self { r, g, b, a: 0xFF }
    }

    /// Create a translucent RGBA color
    ///
    /// # Arguments
    ///
    /// * `r` - Red component
    /// * `g` - Green component
    /// * `b` - Blue component
    /// * `a` - Alpha component
    ///
    /// # Example
    ///
    /// ```
    /// use simple_color::Color;
    ///
    /// let semi_transparent_red = Color::rgba(0xFF, 0x00, 0x00, 0x80);
    /// assert_eq!(semi_transparent_red, Color::RED.a(0x80));
    /// ```
    pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
        Self { r, g, b, a }
    }

    /// Create a copy with modified red component
    ///
    /// # Arguments
    ///
    /// * `r` - New red value
    ///
    /// # Example
    /// ```
    /// use simple_color::Color;
    ///
    /// let dark_red = Color::BLACK.r(0x80);
    /// assert_eq!(dark_red, Color::rgb(0x80, 0, 0));
    /// ```
    pub const fn r(self, r: u8) -> Self {
        Self { r, ..self }
    }

    /// Create a copy with modified green component
    ///
    /// # Arguments
    ///
    /// * `g` - New green value
    ///
    /// # Example
    ///
    /// ```
    /// use simple_color::Color;
    ///
    /// let yellow_green = Color::RED.g(0x80);
    /// assert_eq!(yellow_green, Color::rgb(0xFF, 0x80, 0));
    /// ```
    pub const fn g(self, g: u8) -> Self {
        Self { g, ..self }
    }

    /// Create a copy with modified blue component
    ///
    /// # Arguments
    ///
    /// * `b` - New blue value
    ///
    /// # Example
    ///
    /// ```
    /// use simple_color::Color;
    ///
    /// let light_yellow = Color::WHITE.b(0x80);
    /// assert_eq!(light_yellow, Color::rgb(0xFF, 0xFF, 0x80));
    /// ```
    pub const fn b(self, b: u8) -> Self {
        Self { b, ..self }
    }

    /// Create a copy with modified alpha component
    ///
    /// # Arguments
    ///
    /// * `a` - New alpha value
    ///
    /// # Example
    ///
    /// ```
    /// use simple_color::Color;
    ///
    /// let semi_transparent_blue = Color::BLUE.a(0x80);
    /// assert_eq!(semi_transparent_blue, Color::rgba(0x00, 0x00, 0xFF, 0x80));
    /// ```
    pub const fn a(self, a: u8) -> Self {
        Self { a, ..self }
    }
}

const fn hex_from_byte(byte: u8) -> Option<u8> {
    Some(match byte {
        b'0'..=b'9' => byte - b'0',
        b'a'..=b'f' => byte - b'a' + 10,
        b'A'..=b'F' => byte - b'A' + 10,
        _ => return None,
    })
}

const fn component_from_byte(byte: u8) -> Result<u8, ColorParsingError> {
    component_from_bytes(byte, byte)
}

const fn component_from_bytes(high: u8, low: u8) -> Result<u8, ColorParsingError> {
    match [hex_from_byte(high), hex_from_byte(low)] {
        [Some(h), Some(l)] => Ok(h * 0x10 + l),
        [None, _] => Err(ColorParsingError::InvalidChar(high as char)),
        [_, _] => Err(ColorParsingError::InvalidChar(low as char)),
    }
}

impl FromStr for Color {
    type Err = ColorParsingError;

    /// Parse color from hexadecimal string.
    ///
    /// Supported formats:
    /// - Grayscale - 1 or 2 characters representing brightness
    /// - `RGB` - "RGB" or "RRGGBB"
    /// - `RGBA` - "RGBA" or "RRGGBBAA"
    ///
    /// # Example
    ///
    /// ```
    /// use simple_color::{Color, ColorParsingError};
    ///
    /// let red: Color = "FF0000".parse().unwrap();
    /// assert_eq!(red, Color::RED);
    ///
    /// let semi_transparent_orange: Color = "ff800080".parse().unwrap();
    /// assert_eq!(semi_transparent_orange, Color::rgba(0xFF, 0x80, 0, 0x80));
    ///
    /// let gray: Color = "80".parse().unwrap();
    /// assert_eq!(gray, Color::gray(0x80));
    ///
    /// let invalid_char: Result<Color, _> = "80FG00".parse();
    /// assert_eq!(invalid_char, Err(ColorParsingError::InvalidChar('G')));
    ///
    /// let invalid_length: Result<Color, _> = "FF80000".parse();
    /// assert_eq!(invalid_length, Err(ColorParsingError::InvalidLength(7)));
    /// ```
    fn from_str(name: &str) -> Result<Self, Self::Err> {
        let bytes = name.as_bytes();
        Ok(match *bytes {
            [g] => Self::gray(component_from_byte(g)?),
            [gh, gl] => Self::gray(component_from_bytes(gh, gl)?),
            [r, g, b] => Self::rgb(
                component_from_byte(r)?,
                component_from_byte(g)?,
                component_from_byte(b)?,
            ),
            [r, g, b, a] => Self::rgba(
                component_from_byte(r)?,
                component_from_byte(g)?,
                component_from_byte(b)?,
                component_from_byte(a)?,
            ),
            [rh, rl, gh, gl, bh, bl] => Self::rgb(
                component_from_bytes(rh, rl)?,
                component_from_bytes(gh, gl)?,
                component_from_bytes(bh, bl)?,
            ),
            [rh, rl, gh, gl, bh, bl, ah, al] => Self::rgba(
                component_from_bytes(rh, rl)?,
                component_from_bytes(gh, gl)?,
                component_from_bytes(bh, bl)?,
                component_from_bytes(ah, al)?,
            ),
            _ => return Err(ColorParsingError::InvalidLength(bytes.len())),
        })
    }
}

#[cfg(feature = "data-stream")]
mod data_stream {
    use crate::Color;

    use data_stream::{FromStream, ToStream, from_stream, numbers::EndianSettings, to_stream};

    use std::io::{Read, Result, Write};

    impl<S: EndianSettings> ToStream<S> for Color {
        fn to_stream<W: Write>(&self, writer: &mut W) -> Result<()> {
            to_stream::<S, _, _>(&self.r, writer)?;
            to_stream::<S, _, _>(&self.g, writer)?;
            to_stream::<S, _, _>(&self.b, writer)?;
            to_stream::<S, _, _>(&self.a, writer)?;

            Ok(())
        }
    }

    impl<S: EndianSettings> FromStream<S> for Color {
        fn from_stream<R: Read>(reader: &mut R) -> Result<Self> {
            Ok(Self {
                r: from_stream::<S, _, _>(reader)?,
                g: from_stream::<S, _, _>(reader)?,
                b: from_stream::<S, _, _>(reader)?,
                a: from_stream::<S, _, _>(reader)?,
            })
        }
    }
}

#[cfg(feature = "parser")]
mod parser {
    use crate::Color;
    use token_parser::{Context, ErrorKind, Parsable, Parser, Result, Span};
    impl<C: Context> Parsable<C> for Color {
        fn parse_symbol(name: Box<str>, _span: Span, _context: &C) -> Result<Self> {
            name.parse()
                .map_err(|_| ErrorKind::StringParsing("Color").into())
        }

        fn parse_list(parser: &mut Parser, context: &C) -> Result<Self> {
            let args: Vec<u8> = parser.parse_rest(context)?;
            Ok(match args.len() {
                0 => return Err(ErrorKind::NotEnoughElements(1).into()),
                1 => Self::gray(args[0]),
                2 => return Err(ErrorKind::NotEnoughElements(2).into()),
                3 => Self::rgb(args[0], args[1], args[2]),
                4 => Self::rgba(args[0], args[1], args[2], args[3]),
                n => return Err(ErrorKind::TooManyElements(n - 4).into()),
            })
        }
    }
}