diff --git a/Cargo.toml b/Cargo.toml index f4e2bbe..411f3c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,10 @@ serde = { version = "1.0", features = ["derive"], optional = true } serde_json = { version = "1.0", optional = true } fastsnbt = { git = "https://github.com/owengage/fastnbt", branch = "dev/snbt" } uuid = "1.3.2" +palette = { version = "0.7.3", optional = true } mc_chat_proc = { path = "./mc_chat_proc", optional = true } +clap = { version = "4.4.11", features = ["derive"] } [dev-dependencies] serde_test = "1.0" @@ -26,3 +28,4 @@ serde_test = "1.0" default = [ "serde" ] serde = [ "dep:serde", "serde_json", "uuid/serde"] macros = [ "mc_chat_proc" ] +pallette = [ "palette" ] \ No newline at end of file diff --git a/src/style.rs b/src/style.rs index afcf549..eec64c0 100644 --- a/src/style.rs +++ b/src/style.rs @@ -6,6 +6,9 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; #[cfg(feature = "serde")] pub(crate) mod serde_support; +mod colors; + +pub use colors::*; /// The style of a [`Chat`] component. /// @@ -118,40 +121,6 @@ impl Style { } } -/// The different colors a [`Chat`] component can have. -/// -/// ## TODO: Automatically find nearest value when serializing [`TextColor::Custom`] for older versions -/// --> feature PR -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum TextColor { - Black, - DarkBlue, - DarkGreen, - DarkCyan, - DarkRed, - Purple, - Gold, - Gray, - DarkGray, - Blue, - Green, - Cyan, - Red, - Pink, - Yellow, - White, - /// This field is ignored for versions older than 1.16. - /// - /// See [`TextColor::custom()`]. - Custom(FrozenStr), - Reset, -} - -impl TextColor { - pub fn custom>(color: T) -> TextColor { - TextColor::Custom(color.into()) - } -} /// A ClickEvent useful in a chat message or book. /// TODO: Discuss feature gated `open_file` option diff --git a/src/style/colors.rs b/src/style/colors.rs new file mode 100644 index 0000000..31cc279 --- /dev/null +++ b/src/style/colors.rs @@ -0,0 +1,275 @@ +use std::convert::TryFrom; +use std::fmt::Display; +#[cfg(not(feature = "palette"))] +use crate::freeze::FrozenStr; + +#[cfg(feature = "palette")] +mod rgb { + use std::fmt::Display; + use std::hash::{Hash, Hasher}; + use std::str::FromStr; + use palette::rgb::FromHexError; + use palette::Srgb; + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct Rgb(pub Srgb); + + impl Hash for Rgb { + fn hash(&self, state: &mut H) { + state.write_u8(self.0.red); + state.write_u8(self.0.green); + state.write_u8(self.0.blue); + } + } + + impl FromStr for Rgb { + type Err = FromHexError; + + fn from_str(s: &str) -> Result { + Ok(Self(s.parse()?)) + } + } + + impl From<(u8, u8, u8)> for Rgb { + fn from(value: (u8, u8, u8)) -> Self { + Self(palette::rgb::Rgb::from([value.0, value.1, value.2])) + } + } + + impl Display for Rgb { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", format!("#{:02x}{:02x}{:02x}", self.0.red, self.0.green, self.0.blue)) + } + } +} + +#[cfg(feature = "palette")] +pub use self::rgb::*; + +/// The different colors a [`crate::Chat`] component can have. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "palette", derive(Copy))] +pub enum TextColor { + /// RGB = (0, 0, 0) + Black, + /// RGB = (0, 0, 170) + DarkBlue, + /// RGB = (0, 170, 0) + DarkGreen, + /// RGB = (0, 170, 170) + DarkCyan, + /// RGB = (170, 0, 0) + DarkRed, + /// RGB = (170, 0, 170) + Purple, + /// RGB = (255, 170, 0) + Gold, + /// RGB = (170, 170, 170) + Gray, + /// RGB = (85, 85, 85) + DarkGray, + /// RGB = (85, 85, 255) + Blue, + /// RGB = (85, 255, 85) + Green, + /// RGB = (85, 255, 255) + Cyan, + /// RGB = (255, 85, 85) + Red, + /// RGB = (255, 85, 255) + Pink, + /// RGB = (255, 255, 85) + Yellow, + /// RGB = (255, 255, 255 + White, + /// This field is ignored for versions older than 1.16. + /// + /// See [`TextColor::custom()`]. + #[cfg(not(feature = "palette"))] + Custom(FrozenStr), + #[cfg(feature = "palette")] + /// This field is ignored for versions older than 1.16. + Custom(Rgb), + Reset, +} + +#[cfg(not(feature = "palette"))] +impl TextColor { + pub fn custom>(color: T) -> TextColor { + TextColor::Custom(color.into()) + } +} + +impl Display for TextColor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = String::from(match self { + TextColor::Black => "black", + TextColor::DarkBlue => "dark_blue", + TextColor::DarkGreen => "dark_green", + TextColor::DarkCyan => "dark_aqua", + TextColor::DarkRed => "dark_red", + TextColor::Purple => "dark_purple", + TextColor::Gold => "gold", + TextColor::Gray => "gray", + TextColor::DarkGray => "dark_gray", + TextColor::Blue => "blue", + TextColor::Green => "green", + TextColor::Cyan => "aqua", + TextColor::Red => "red", + TextColor::Pink => "light_purple", + TextColor::Yellow => "yellow", + TextColor::White => "white", + TextColor::Custom(color) => { + #[cfg(feature = "palette")] + return write!(f, "{}", format!("{color}")); + #[cfg(not(feature = "palette"))] + color + }, + TextColor::Reset => "reset", + }); + write!(f, "{}", str) + } +} + +impl TryFrom<&str> for TextColor { + type Error = (); + + fn try_from(value: &str) -> Result { + Ok(match value { + "black" => TextColor::Black, + "dark_blue" => TextColor::DarkBlue, + "dark_green" => TextColor::DarkGreen, + "dark_aqua" => TextColor::DarkCyan, + "dark_red" => TextColor::DarkRed, + "dark_purple" => TextColor::Purple, + "gold" => TextColor::Gold, + "gray" => TextColor::Gray, + "dark_gray" => TextColor::DarkGray, + "blue" => TextColor::Blue, + "green" => TextColor::Green, + "aqua" => TextColor::Cyan, + "red" => TextColor::Red, + "light_purple" => TextColor::Pink, + "yellow" => TextColor::Yellow, + "white" => TextColor::White, + "reset" => TextColor::Reset, + custom => { + #[cfg(not(feature = "palette"))] + if custom.len() != 7 || !custom.starts_with('#') { + return Err(()); + } else { + for c in custom.chars().skip(1) { + if c.is_ascii_hexdigit() { + return Err(()); + } + } + TextColor::custom(FrozenStr::from(custom)) + } + + #[cfg(feature = "palette")] + TextColor::Custom(custom.parse().map_err(|_| ())?) + } + }) + } +} + +#[cfg(feature = "palette")] +mod custom_colors_to_legacy { + use std::cmp::Ordering; + use palette::{IntoColor, Lab}; + use crate::{Rgb, TextColor}; + use palette::color_difference::{Ciede2000, EuclideanDistance}; + + #[derive(Clone, Copy, PartialOrd, PartialEq)] + struct Float32Wrapper(f32); + + impl Eq for Float32Wrapper {} + impl Ord for Float32Wrapper { + fn cmp(&self, other: &Self) -> Ordering { + if self.0 == other.0 { + Ordering::Equal + } else if self.0 > other.0 { + Ordering::Greater + } else { + Ordering::Less + } + } + } + + pub const RGB_COLORS: [(TextColor, (u8, u8, u8)); 16] = [ + (TextColor::Black, (0, 0, 0)), + (TextColor::DarkBlue, (0, 0, 170)), + (TextColor::DarkGreen, (0, 170, 0)), + (TextColor::DarkCyan, (0, 170, 170)), + (TextColor::DarkRed, (170, 0, 0)), + (TextColor::Purple, (170, 0, 170)), + (TextColor::Gold, (255, 170, 0)), + (TextColor::Gray, (170, 170, 170)), + (TextColor::DarkGray, (85, 85, 85)), + (TextColor::Blue, (85, 85, 255)), + (TextColor::Green, (85, 255, 85)), + (TextColor::Cyan, (85, 255, 255)), + (TextColor::Red, (255, 85, 85)), + (TextColor::Pink, (255, 85, 255)), + (TextColor::Yellow, (255, 255, 85)), + (TextColor::White, (255, 255, 255)) + ]; + + type ColorCompereFn = fn(Rgb, Rgb) -> T; + + impl TextColor { + fn into_legacy(self, delta_fn: ColorCompereFn) -> Self where T: Copy, T: Ord { + if let TextColor::Custom(data) = self { + *RGB_COLORS.iter() + .map(|(color, rgb)| { + let delta = delta_fn(data, Rgb::from(*rgb)); + (color, delta) + }) + .min_by_key(|(_, delta)| *delta) + .map_or_else( + || unreachable!(), // impossible as long as RGB_COLORS.len() != 0 + |(color, _)| color + ) + } else { self } + } + + /// Converts [`TextColor::Custom`] to legacy [`TextColor`] values using [`EuclideanDistance`] + /// + /// ```rust + /// use mc_chat::{Rgb, TextColor}; + /// assert_eq!( + /// TextColor::Custom(Rgb::from((0, 0, 0))).into_legacy_ciede2000(), + /// TextColor::Black + /// ) + /// ``` + pub fn into_legacy_ciede2000(self) -> Self { + self.into_legacy(|first, second| { + let first: Lab = first.0.into_linear().into_color(); + let second: Lab = second.0.into_linear().into_color(); + + Float32Wrapper(first.difference(second)) + }) + } + + /// Converts [`TextColor::Custom`] to legacy [`TextColor`] values using [`Ciede2000`] + /// + /// ```rust + /// use mc_chat::{Rgb, TextColor}; + /// assert_eq!( + /// TextColor::Custom(Rgb::from((255, 255, 255))).into_legacy_euclidean(), + /// TextColor::White + /// ) + /// ``` + pub fn into_legacy_euclidean(self) -> TextColor { + self.into_legacy(|first, second| { + let first: Lab = first.0.into_linear().into_color(); + let second: Lab = second.0.into_linear().into_color(); + + Float32Wrapper(first.distance(second)) + }) + } + } +} + +#[cfg(feature = "palette")] +pub use self::custom_colors_to_legacy::*; diff --git a/src/style/serde_support.rs b/src/style/serde_support.rs index 3f45a5e..c128d6c 100644 --- a/src/style/serde_support.rs +++ b/src/style/serde_support.rs @@ -11,33 +11,15 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; use uuid::Uuid; -use crate::style::{ClickEvent, HoverEvent, Style, TextColor}; +use crate::style::{ClickEvent, HoverEvent, Style}; +use crate::style::colors::TextColor; impl Serialize for TextColor { fn serialize(&self, serializer: S) -> Result where S: Serializer, { - serializer.serialize_str(match self { - TextColor::Black => "black", - TextColor::DarkBlue => "dark_blue", - TextColor::DarkGreen => "dark_green", - TextColor::DarkCyan => "dark_aqua", - TextColor::DarkRed => "dark_red", - TextColor::Purple => "dark_purple", - TextColor::Gold => "gold", - TextColor::Gray => "gray", - TextColor::DarkGray => "dark_gray", - TextColor::Blue => "blue", - TextColor::Green => "green", - TextColor::Cyan => "aqua", - TextColor::Red => "red", - TextColor::Pink => "light_purple", - TextColor::Yellow => "yellow", - TextColor::White => "white", - TextColor::Custom(color) => color, - TextColor::Reset => "reset", - }) + serializer.serialize_str(&*self.to_string()) } } @@ -48,41 +30,12 @@ impl<'de> Deserialize<'de> for TextColor { D: Deserializer<'de>, { let input = FrozenStr::deserialize(deserializer)?; - Ok(match input.deref() { - "black" => TextColor::Black, - "dark_blue" => TextColor::DarkBlue, - "dark_green" => TextColor::DarkGreen, - "dark_aqua" => TextColor::DarkCyan, - "dark_red" => TextColor::DarkRed, - "dark_purple" => TextColor::Purple, - "gold" => TextColor::Gold, - "gray" => TextColor::Gray, - "dark_gray" => TextColor::DarkGray, - "blue" => TextColor::Blue, - "green" => TextColor::Green, - "aqua" => TextColor::Cyan, - "red" => TextColor::Red, - "light_purple" => TextColor::Pink, - "yellow" => TextColor::Yellow, - "white" => TextColor::White, - "reset" => TextColor::Reset, - custom => { - let error = serde::de::Error::invalid_value( - Unexpected::Str(custom), - &"a 6 digit hex color prefixed by '#'", - ); - if custom.len() != 7 || !custom.starts_with('#') { - return Err(error); - } else { - for c in custom.chars() { - if !"0123456789abcdefABCDEF".contains(c) { - return Err(error); - } - } - TextColor::custom(input) - } - } - }) + TextColor::try_from(input.deref()).map_err(|_| + serde::de::Error::invalid_value( + Unexpected::Str(&*input), + &"a 5 digit hex color prefixed by '#'", + ) + ) } }