From 816facc2046035e669c39a69e13974b0dfb0379b Mon Sep 17 00:00:00 2001 From: Vlad-Stefan Harbuz Date: Sun, 30 Jun 2024 14:17:17 +0100 Subject: Add Color::from_hex --- core/src/color.rs | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) (limited to 'core') 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 { + 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(()) + } } -- cgit From 934667d263468e29b10137817f13ff4640fa46b1 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 11 Sep 2024 01:15:38 +0200 Subject: Improve flexibility of `color!` macro --- core/src/color.rs | 61 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 15 deletions(-) (limited to 'core') diff --git a/core/src/color.rs b/core/src/color.rs index d9271e83..4f4b5e9b 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -232,34 +232,65 @@ impl From<[f32; 4]> for Color { /// /// ``` /// # use iced_core::{Color, color}; -/// assert_eq!(color!(0, 0, 0), Color::from_rgb(0., 0., 0.)); -/// assert_eq!(color!(0, 0, 0, 0.), Color::from_rgba(0., 0., 0., 0.)); -/// assert_eq!(color!(0xffffff), Color::from_rgb(1., 1., 1.)); -/// assert_eq!(color!(0xffffff, 0.), Color::from_rgba(1., 1., 1., 0.)); +/// assert_eq!(color!(0, 0, 0), Color::BLACK); +/// assert_eq!(color!(0, 0, 0, 0.0), Color::TRANSPARENT); +/// assert_eq!(color!(0xffffff), Color::from_rgb(1.0, 1.0, 1.0)); +/// assert_eq!(color!(0xffffff, 0.), Color::from_rgba(1.0, 1.0, 1.0, 0.0)); +/// assert_eq!(color!(0x123), Color::from_rgba8(0x11, 0x22, 0x33, 1.0)); +/// assert_eq!(color!(0x123), color!(0x112233)); /// ``` #[macro_export] macro_rules! color { ($r:expr, $g:expr, $b:expr) => { color!($r, $g, $b, 1.0) }; - ($r:expr, $g:expr, $b:expr, $a:expr) => { - $crate::Color { - r: $r as f32 / 255.0, - g: $g as f32 / 255.0, - b: $b as f32 / 255.0, - a: $a, + ($r:expr, $g:expr, $b:expr, $a:expr) => {{ + let r = $r as f32 / 255.0; + let g = $g as f32 / 255.0; + let b = $b as f32 / 255.0; + + #[allow(clippy::manual_range_contains)] + { + debug_assert!( + r >= 0.0 && r <= 1.0, + "R channel must be in [0, 255] range." + ); + debug_assert!( + g >= 0.0 && g <= 1.0, + "G channel must be in [0, 255] range." + ); + debug_assert!( + b >= 0.0 && b <= 1.0, + "B channel must be in [0, 255] range." + ); } - }; + + $crate::Color { r, g, b, a: $a } + }}; ($hex:expr) => {{ color!($hex, 1.0) }}; ($hex:expr, $a:expr) => {{ let hex = $hex as u32; - let r = (hex & 0xff0000) >> 16; - let g = (hex & 0xff00) >> 8; - let b = (hex & 0xff); - color!(r, g, b, $a) + if hex <= 0xfff { + let r = (hex & 0xf00) >> 8; + let g = (hex & 0x0f0) >> 4; + let b = (hex & 0x00f); + + color!((r << 4 | r), (g << 4 | g), (b << 4 | b), $a) + } else { + debug_assert!( + hex <= 0xffffff, + "color! value must not exceed 0xffffff" + ); + + let r = (hex & 0xff0000) >> 16; + let g = (hex & 0xff00) >> 8; + let b = (hex & 0xff); + + color!(r, g, b, $a) + } }}; } -- cgit From 523708b5b1665f647bbe377a459e7c26410f7af8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 11 Sep 2024 01:20:47 +0200 Subject: Rename `Color::from_hex` to `Color::parse` --- core/src/color.rs | 94 +++++++++++++++++++++++++------------------------------ 1 file changed, 43 insertions(+), 51 deletions(-) (limited to 'core') diff --git a/core/src/color.rs b/core/src/color.rs index 4f4b5e9b..89ec0e5b 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -1,13 +1,5 @@ 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 { @@ -96,50 +88,46 @@ 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 { + /// Parses a [`Color`] from a hex string. + /// + /// Supported formats are #rrggbb, #rrggbbaa, #rgb, and #rgba. + /// The starting "#" is optional. Both uppercase and lowercase are supported. + pub fn parse(s: &str) -> Option { 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; + let parse_channel = |from: usize, to: usize| { + let num = + usize::from_str_radix(&hex[from..=to], 16).ok()? 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 }) + Some(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) - } + Some(match hex.len() { + 3 => Color::from_rgb( + parse_channel(0, 0)?, + parse_channel(1, 1)?, + parse_channel(2, 2)?, + ), + 4 => Color::from_rgba( + parse_channel(0, 0)?, + parse_channel(1, 1)?, + parse_channel(2, 2)?, + parse_channel(3, 3)?, + ), + 6 => Color::from_rgb( + parse_channel(0, 1)?, + parse_channel(2, 3)?, + parse_channel(4, 5)?, + ), + 8 => Color::from_rgba( + parse_channel(0, 1)?, + parse_channel(2, 3)?, + parse_channel(4, 5)?, + parse_channel(6, 7)?, + ), + _ => None?, + }) } /// Creates a [`Color`] from its linear RGBA components. @@ -360,17 +348,21 @@ mod tests { } #[test] - fn from_hex() -> Result<(), ColorError> { + fn parse() { 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_eq!( + Color::parse(arg).expect("color must parse").into_rgba8(), + expected + ); } - assert!(Color::from_hex("invalid").is_err()); - Ok(()) + + assert!(Color::parse("invalid").is_none()); } } -- cgit From 7901d4737c5c75467bc694e2fa37057fbf5ca111 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 11 Sep 2024 01:28:03 +0200 Subject: Encourage use of `color!` macro in `Color::parse` docs --- core/src/color.rs | 47 ++++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 21 deletions(-) (limited to 'core') diff --git a/core/src/color.rs b/core/src/color.rs index 89ec0e5b..46fe9ecd 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -88,10 +88,35 @@ impl Color { } } + /// 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: + // https://en.wikipedia.org/wiki/SRGB + fn gamma_component(u: f32) -> f32 { + if u < 0.0031308 { + 12.92 * u + } else { + 1.055 * u.powf(1.0 / 2.4) - 0.055 + } + } + + Self { + r: gamma_component(r), + g: gamma_component(g), + b: gamma_component(b), + a, + } + } + /// Parses a [`Color`] from a hex string. /// - /// Supported formats are #rrggbb, #rrggbbaa, #rgb, and #rgba. + /// Supported formats are `#rrggbb`, `#rrggbbaa`, `#rgb`, and `#rgba`. /// The starting "#" is optional. Both uppercase and lowercase are supported. + /// + /// If you have a static color string, using the [`color!`] macro should be preferred + /// since it leverages hexadecimal literal notation and arithmetic directly. + /// + /// [`color!`]: crate::color! pub fn parse(s: &str) -> Option { let hex = s.strip_prefix('#').unwrap_or(s); @@ -130,26 +155,6 @@ impl Color { }) } - /// 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: - // https://en.wikipedia.org/wiki/SRGB - fn gamma_component(u: f32) -> f32 { - if u < 0.0031308 { - 12.92 * u - } else { - 1.055 * u.powf(1.0 / 2.4) - 0.055 - } - } - - Self { - r: gamma_component(r), - g: gamma_component(g), - b: gamma_component(b), - a, - } - } - /// Converts the [`Color`] into its RGBA8 equivalent. #[must_use] pub fn into_rgba8(self) -> [u8; 4] { -- cgit