summaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/src/color.rs69
1 files changed, 69 insertions, 0 deletions
diff --git a/core/src/color.rs b/core/src/color.rs
index 4e79defb..d9271e83 100644
--- a/core/src/color.rs
+++ b/core/src/color.rs
@@ -1,5 +1,13 @@
use palette::rgb::{Srgb, Srgba};
+#[derive(Debug, thiserror::Error)]
+/// Errors that can occur when constructing a [`Color`].
+pub enum ColorError {
+ #[error("The specified hex string is invalid. See supported formats.")]
+ /// The specified hex string is invalid. See supported formats.
+ InvalidHex,
+}
+
/// A color in the `sRGB` color space.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Color {
@@ -88,6 +96,52 @@ impl Color {
}
}
+ /// Creates a [`Color`] from a hex string. Supported formats are #rrggbb, #rrggbbaa, #rgb,
+ /// #rgba. The “#” is optional. Both uppercase and lowercase are supported.
+ pub fn from_hex(s: &str) -> Result<Color, ColorError> {
+ let hex = s.strip_prefix('#').unwrap_or(s);
+ let n_chars = hex.len();
+
+ let get_channel = |from: usize, to: usize| {
+ let num = usize::from_str_radix(&hex[from..=to], 16)
+ .map_err(|_| ColorError::InvalidHex)?
+ as f32
+ / 255.0;
+ // If we only got half a byte (one letter), expand it into a full byte (two letters)
+ Ok(if from == to { num + num * 16.0 } else { num })
+ };
+
+ if n_chars == 3 {
+ Ok(Color::from_rgb(
+ get_channel(0, 0)?,
+ get_channel(1, 1)?,
+ get_channel(2, 2)?,
+ ))
+ } else if n_chars == 6 {
+ Ok(Color::from_rgb(
+ get_channel(0, 1)?,
+ get_channel(2, 3)?,
+ get_channel(4, 5)?,
+ ))
+ } else if n_chars == 4 {
+ Ok(Color::from_rgba(
+ get_channel(0, 0)?,
+ get_channel(1, 1)?,
+ get_channel(2, 2)?,
+ get_channel(3, 3)?,
+ ))
+ } else if n_chars == 8 {
+ Ok(Color::from_rgba(
+ get_channel(0, 1)?,
+ get_channel(2, 3)?,
+ get_channel(4, 5)?,
+ get_channel(6, 7)?,
+ ))
+ } else {
+ Err(ColorError::InvalidHex)
+ }
+ }
+
/// Creates a [`Color`] from its linear RGBA components.
pub fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
// As described in:
@@ -273,4 +327,19 @@ mod tests {
assert_relative_eq!(result.b, 0.3);
assert_relative_eq!(result.a, 1.0);
}
+
+ #[test]
+ fn from_hex() -> Result<(), ColorError> {
+ let tests = [
+ ("#ff0000", [255, 0, 0, 255]),
+ ("00ff0080", [0, 255, 0, 128]),
+ ("#F80", [255, 136, 0, 255]),
+ ("#00f1", [0, 0, 255, 17]),
+ ];
+ for (arg, expected) in tests {
+ assert_eq!(Color::from_hex(arg)?.into_rgba8(), expected);
+ }
+ assert!(Color::from_hex("invalid").is_err());
+ Ok(())
+ }
}