yofi 0.2.1

minimalistic menu for wayland
Documentation
use anyhow::Context;
use raqote::SolidSource;
use serde::Deserialize;

#[derive(Deserialize, Clone, Copy)]
#[cfg_attr(test, derive(Debug, PartialEq))]
#[serde(from = "ColorDeser")]
pub struct Color(u32);

impl Color {
    pub const fn from_rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
        Self(u32::from_be_bytes([r, g, b, a]))
    }

    pub const fn to_rgba(self) -> [u8; 4] {
        self.0.to_be_bytes()
    }

    pub fn as_source(self) -> SolidSource {
        let [r, g, b, a] = self.to_rgba();
        SolidSource::from_unpremultiplied_argb(a, r, g, b)
    }
}

impl std::ops::Deref for Color {
    type Target = u32;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl From<ColorDeser> for Color {
    fn from(value: ColorDeser) -> Self {
        match value {
            ColorDeser::Int(x) => Self(x),
            ColorDeser::String(ColorDeserString(c)) => c,
        }
    }
}

#[derive(serde::Deserialize)]
#[serde(untagged)]
enum ColorDeser {
    Int(u32),
    String(ColorDeserString),
}

#[derive(serde::Deserialize)]
#[serde(try_from = "String")]
struct ColorDeserString(Color);

impl TryFrom<String> for ColorDeserString {
    type Error = anyhow::Error;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        let part = match value.chars().next() {
            None => anyhow::bail!("color cannot be empty"),
            Some('#') => value.split_at(1).1,
            Some(_) => {
                anyhow::bail!("color can be either decimal or hex number prefixed with '#'")
            }
        };

        let decoded = u32::from_str_radix(part, 16).context("parse hex number");
        Ok(Self(match (decoded, part.len()) {
            (Ok(d), 3) => {
                let (r, g, b) = (
                    ((d & 0xf00) >> 8) as u8,
                    ((d & 0xf0) >> 4) as u8,
                    (d & 0xf) as u8,
                );
                Color::from_rgba(r << 4 | r, g << 4 | g, b << 4 | b, 0xff)
            }
            (Ok(d), 6) => Color(d << 8 | 0xff),
            (Ok(d), 8) => Color(d),
            (e, _) => anyhow::bail!(
                "hex color can only be specified in #RGB, #RRGGBB, or #RRGGBBAA format, {e:?}"
            ),
        }))
    }
}

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

    #[test_case(r##"x = 1234"##, Color(1234); "decimal number")]
    #[test_case(r##"x = 0x5432"##, Color(0x5432); "hex number")]
    #[test_case(r##"x = "#123""##, Color::from_rgba(0x11, 0x22, 0x33, 0xff); "3-sym css")]
    #[test_case(r##"x = "#123456""##, Color::from_rgba(0x12, 0x34, 0x56, 0xff); "6-sym css")]
    #[test_case(r##"x = "#12345678""##, Color::from_rgba(0x12, 0x34, 0x56, 0x78); "8-sym css")]
    fn deser_color(s: &str, expected: Color) {
        #[derive(Deserialize)]
        struct T {
            x: Color,
        }
        let c = toml::from_str::<T>(s).unwrap().x;
        assert_eq!(c, expected);
    }
}