use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HexError {
MissingHash,
BadLength(usize),
NotHex(char),
}
impl fmt::Display for HexError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HexError::MissingHash => write!(f, "missing '#' prefix"),
HexError::BadLength(n) => write!(
f,
"expected 3, 4, 6, or 8 hex digits after '#'; got {n}"
),
HexError::NotHex(c) => write!(f, "not a hex digit: {c:?}"),
}
}
}
impl std::error::Error for HexError {}
pub fn parse_hex(s: &str) -> Result<(u8, u8, u8, u8), HexError> {
let s = s.trim();
let body = s.strip_prefix('#').ok_or(HexError::MissingHash)?;
let n = body.len();
let (r, g, b, a) = match n {
3 => {
let r = expand_nibble(body, 0)?;
let g = expand_nibble(body, 1)?;
let b = expand_nibble(body, 2)?;
(r, g, b, 0xff)
}
4 => {
let r = expand_nibble(body, 0)?;
let g = expand_nibble(body, 1)?;
let b = expand_nibble(body, 2)?;
let a = expand_nibble(body, 3)?;
(r, g, b, a)
}
6 => {
let r = byte(body, 0)?;
let g = byte(body, 1)?;
let b = byte(body, 2)?;
(r, g, b, 0xff)
}
8 => {
let r = byte(body, 0)?;
let g = byte(body, 1)?;
let b = byte(body, 2)?;
let a = byte(body, 3)?;
(r, g, b, a)
}
_ => return Err(HexError::BadLength(n)),
};
Ok((r, g, b, a))
}
pub fn format_rgb(r: u8, g: u8, b: u8) -> String {
format!("#{:02x}{:02x}{:02x}", r, g, b)
}
pub fn format_rgba(r: u8, g: u8, b: u8, a: u8) -> String {
format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a)
}
pub fn to_slint_color(r: u8, g: u8, b: u8, a: u8) -> slint::Color {
slint::Color::from_argb_u8(a, r, g, b)
}
fn expand_nibble(body: &str, idx: usize) -> Result<u8, HexError> {
let c = body.chars().nth(idx).ok_or(HexError::BadLength(body.len()))?;
let n = hex_digit(c)?;
Ok((n << 4) | n)
}
fn byte(body: &str, idx: usize) -> Result<u8, HexError> {
let start = idx * 2;
let bytes = body.as_bytes();
if start + 1 >= bytes.len() {
return Err(HexError::BadLength(bytes.len()));
}
let high = hex_digit(bytes[start] as char)?;
let low = hex_digit(bytes[start + 1] as char)?;
Ok((high << 4) | low)
}
fn hex_digit(c: char) -> Result<u8, HexError> {
c.to_digit(16)
.map(|d| d as u8)
.ok_or(HexError::NotHex(c))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_short_no_alpha() {
assert_eq!(parse_hex("#0a0").unwrap(), (0x00, 0xaa, 0x00, 0xff));
assert_eq!(parse_hex("#FFF").unwrap(), (0xff, 0xff, 0xff, 0xff));
}
#[test]
fn parse_short_with_alpha() {
assert_eq!(parse_hex("#0a0f").unwrap(), (0x00, 0xaa, 0x00, 0xff));
assert_eq!(parse_hex("#0a08").unwrap(), (0x00, 0xaa, 0x00, 0x88));
}
#[test]
fn parse_full_no_alpha() {
assert_eq!(parse_hex("#ff0080").unwrap(), (255, 0, 128, 255));
assert_eq!(parse_hex("#000000").unwrap(), (0, 0, 0, 255));
}
#[test]
fn parse_full_with_alpha() {
assert_eq!(parse_hex("#ff000080").unwrap(), (255, 0, 0, 0x80));
}
#[test]
fn parse_trims_whitespace() {
assert_eq!(parse_hex(" #ff0000 ").unwrap(), (255, 0, 0, 255));
}
#[test]
fn parse_rejects_missing_hash() {
assert_eq!(parse_hex("ff0000"), Err(HexError::MissingHash));
}
#[test]
fn parse_rejects_bad_length() {
assert!(matches!(parse_hex("#ff"), Err(HexError::BadLength(2))));
assert!(matches!(parse_hex("#fffff"), Err(HexError::BadLength(5))));
assert!(matches!(parse_hex("#fffffffff"), Err(HexError::BadLength(9))));
}
#[test]
fn parse_rejects_non_hex() {
assert!(matches!(parse_hex("#zzz"), Err(HexError::NotHex('z'))));
}
#[test]
fn round_trip_format() {
let s = "#ff0080";
let (r, g, b, _a) = parse_hex(s).unwrap();
assert_eq!(format_rgb(r, g, b), s);
}
#[test]
fn round_trip_with_alpha() {
let s = "#ff008080";
let (r, g, b, a) = parse_hex(s).unwrap();
assert_eq!(format_rgba(r, g, b, a), s);
}
}