use std::str::FromStr;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ColorError {
InvalidHexString,
UnknownColor,
}
impl std::fmt::Display for ColorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ColorError::InvalidHexString => write!(f, "invalid hex string"),
ColorError::UnknownColor => write!(f, "unknown color"),
}
}
}
impl std::error::Error for ColorError {}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
pub struct RGB(pub u8, pub u8, pub u8);
impl RGB {
pub fn red(self) -> u8 {
self.0
}
pub fn green(self) -> u8 {
self.1
}
pub fn blue(self) -> u8 {
self.2
}
}
impl Default for RGB {
fn default() -> Self {
RGB(255, 255, 255)
}
}
impl std::fmt::Display for RGB {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self(r, g, b) = self;
write!(f, "#{:02X}{:02X}{:02X}", r, g, b)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for RGB {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let RGB(r, g, b) = *self;
let mut rgb = serializer.serialize_struct("rgb", 3)?;
rgb.serialize_field("r", &r)?;
rgb.serialize_field("g", &g)?;
rgb.serialize_field("b", &b)?;
rgb.end()
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for RGB {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
struct Inner {
r: u8,
g: u8,
b: u8,
}
let Inner { r, g, b } = Inner::deserialize(deserializer)?;
Ok(Self(r, g, b))
}
}
impl FromStr for RGB {
type Err = ColorError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let input = input.trim();
let input = match (input.chars().next(), input.len()) {
(Some('#'), 7) => &input[1..],
(_, 6) => input,
_ => return Err(ColorError::InvalidHexString),
};
u32::from_str_radix(&input, 16)
.map(|s| {
RGB(
((s >> 16) & 0xFF) as u8,
((s >> 8) & 0xFF) as u8,
(s & 0xFF) as u8,
)
})
.map_err(|_| ColorError::InvalidHexString)
}
}
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Color {
pub kind: TwitchColor,
pub rgb: RGB,
}
impl std::fmt::Display for Color {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use TwitchColor::*;
let name = match self.kind {
Blue => "Blue",
BlueViolet => "BlueViolet",
CadetBlue => "CadetBlue",
Chocolate => "Chocolate",
Coral => "Coral",
DodgerBlue => "DodgerBlue",
Firebrick => "Firebrick",
GoldenRod => "GoldenRod",
Green => "Green",
HotPink => "HotPink",
OrangeRed => "OrangeRed",
Red => "Red",
SeaGreen => "SeaGreen",
SpringGreen => "SpringGreen",
YellowGreen => "YellowGreen",
Turbo => return write!(f, "{}", self.rgb.to_string()),
__Nonexhaustive => unreachable!(),
};
write!(f, "{}", name)
}
}
impl Default for Color {
fn default() -> Self {
Self {
kind: TwitchColor::Turbo,
rgb: RGB::default(),
}
}
}
impl FromStr for Color {
type Err = ColorError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
use TwitchColor::*;
let find = |color| {
let colors = twitch_colors();
colors[colors.iter().position(|(d, _)| *d == color).unwrap()]
};
let (kind, rgb) = match input {
"Blue" | "blue" => find(Blue),
"BlueViolet" | "blue_violet" | "blueviolet" | "blue violet" => find(BlueViolet),
"CadetBlue" | "cadet_blue" | "cadetblue" | "cadet blue" => find(CadetBlue),
"Chocolate" | "chocolate" => find(Chocolate),
"Coral" | "coral" => find(Coral),
"DodgerBlue" | "dodger_blue" | "dodgerblue" | "dodger blue" => find(DodgerBlue),
"Firebrick" | "firebrick" => find(Firebrick),
"GoldenRod" | "golden_rod" | "goldenrod" | "golden rod" => find(GoldenRod),
"Green" | "green" => find(Green),
"HotPink" | "hot_pink" | "hotpink" | "hot pink" => find(HotPink),
"OrangeRed" | "orange_red" | "orangered" | "orange red" => find(OrangeRed),
"Red" | "red" => find(Red),
"SeaGreen" | "sea_green" | "seagreen" | "sea green" => find(SeaGreen),
"SpringGreen" | "spring_green" | "springgreen" | "spring green" => find(SpringGreen),
"YellowGreen" | "yellow_green" | "yellowgreen" | "yellow green" => find(YellowGreen),
_ => (Turbo, input.parse::<RGB>()?),
};
Ok(Self { kind, rgb })
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TwitchColor {
Blue,
BlueViolet,
CadetBlue,
Chocolate,
Coral,
DodgerBlue,
Firebrick,
GoldenRod,
Green,
HotPink,
OrangeRed,
Red,
SeaGreen,
SpringGreen,
YellowGreen,
Turbo,
#[doc(hidden)]
__Nonexhaustive,
}
impl From<RGB> for Color {
fn from(rgb: RGB) -> Self {
Color {
kind: rgb.into(),
rgb,
}
}
}
impl From<Color> for RGB {
fn from(color: Color) -> Self {
color.rgb
}
}
impl From<RGB> for TwitchColor {
fn from(rgb: RGB) -> Self {
twitch_colors()
.iter()
.find(|(_, color)| *color == rgb)
.map(|&(c, _)| c)
.unwrap_or_else(|| TwitchColor::Turbo)
}
}
impl From<TwitchColor> for RGB {
fn from(color: TwitchColor) -> Self {
twitch_colors()
.iter()
.find(|(c, _)| *c == color)
.map(|&(_, r)| r)
.unwrap_or_default()
}
}
pub const fn twitch_colors() -> [(TwitchColor, RGB); 15] {
use TwitchColor::*;
[
(Blue, RGB(0x00, 0x00, 0xFF)),
(BlueViolet, RGB(0x8A, 0x2B, 0xE2)),
(CadetBlue, RGB(0x5F, 0x9E, 0xA0)),
(Chocolate, RGB(0xD2, 0x69, 0x1E)),
(Coral, RGB(0xFF, 0x7F, 0x50)),
(DodgerBlue, RGB(0x1E, 0x90, 0xFF)),
(Firebrick, RGB(0xB2, 0x22, 0x22)),
(GoldenRod, RGB(0xDA, 0xA5, 0x20)),
(Green, RGB(0x00, 0x80, 0x00)),
(HotPink, RGB(0xFF, 0x69, 0xB4)),
(OrangeRed, RGB(0xFF, 0x45, 0x00)),
(Red, RGB(0xFF, 0x00, 0x00)),
(SeaGreen, RGB(0x2E, 0x8B, 0x57)),
(SpringGreen, RGB(0x00, 0xFF, 0x7F)),
(YellowGreen, RGB(0xAD, 0xFF, 0x2F)),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_color() {
let color: Color = "blue".parse().unwrap();
assert_eq!(color.kind, TwitchColor::Blue);
assert_eq!(color.rgb, RGB(0, 0, 255));
assert_eq!(color.to_string(), "Blue");
}
#[test]
fn parse_turbo_color() {
let color: Color = "#FAFAFA".parse().unwrap();
assert_eq!(color.kind, TwitchColor::Turbo);
assert_eq!(color.rgb, RGB(250, 250, 250));
assert_eq!(color.to_string(), "#FAFAFA");
let color: Color = "FAFAFA".parse().unwrap();
assert_eq!(color.kind, TwitchColor::Turbo);
assert_eq!(color.rgb, RGB(250, 250, 250));
assert_eq!(color.to_string(), "#FAFAFA");
}
#[cfg(feature = "serde")]
#[test]
fn round_trip_color() {
let color: Color = "blue".parse().unwrap();
let json = serde_json::to_string(&color).unwrap();
let parsed: Color = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, color);
assert_eq!(color.kind, TwitchColor::Blue);
assert_eq!(color.rgb, RGB(0, 0, 255));
assert_eq!(color.to_string(), "Blue");
}
#[cfg(feature = "serde")]
#[test]
fn round_trip_turbo_color() {
let color: Color = "#FAFAFA".parse().unwrap();
let json = serde_json::to_string(&color).unwrap();
let parsed: Color = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, color);
assert_eq!(color.kind, TwitchColor::Turbo);
assert_eq!(color.rgb, RGB(250, 250, 250));
assert_eq!(color.to_string(), "#FAFAFA");
let color: Color = "FAFAFA".parse().unwrap();
let json = serde_json::to_string(&color).unwrap();
let parsed: Color = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, color);
assert_eq!(color.kind, TwitchColor::Turbo);
assert_eq!(color.rgb, RGB(250, 250, 250));
assert_eq!(color.to_string(), "#FAFAFA");
}
}