diff --git a/src/style/color.rs b/src/style/color.rs index b1e42dad..4826f60b 100644 --- a/src/style/color.rs +++ b/src/style/color.rs @@ -371,30 +371,46 @@ impl Color { /// Converts a HSL representation to a `Color::Rgb` instance. /// - /// The `from_hsl` function converts the Hue, Saturation and Lightness values to a - /// corresponding `Color` RGB equivalent. + /// The `from_hsl` function converts the Hue, Saturation and Lightness values to a corresponding + /// `Color` RGB equivalent. /// - /// Hue values should be in the range [0, 360]. - /// Saturation and L values should be in the range [0, 100]. - /// Values that are not in the range are clamped to be within the range. + /// Hue values should be in the range [-180..180]. Values outside this range are normalized by + /// wrapping. + /// + /// Saturation and L values should be in the range [0.0..1.0]. Values outside this range are + /// clamped. + /// + /// Clamping to valid ranges happens before conversion to RGB. /// /// # Examples /// /// ``` /// use ratatui::{palette::Hsl, style::Color}; /// - /// // Black + /// // Minimum Lightness is black /// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 0.0)); /// assert_eq!(color, Color::Rgb(0, 0, 0)); /// - /// // White - note: 100% saturation is not converted to white - /// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 100.0)); + /// // Maximum Lightness is white + /// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 1.0)); /// assert_eq!(color, Color::Rgb(255, 255, 255)); + /// + /// // Minimum Saturation is fully desaturated red = gray + /// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 0.5)); + /// assert_eq!(color, Color::Rgb(128, 128, 128)); + /// + /// // Bright red + /// let color: Color = Color::from_hsl(Hsl::new(0.0, 1.0, 0.5)); + /// assert_eq!(color, Color::Rgb(255, 0, 0)); + /// + /// // Bright blue + /// let color: Color = Color::from_hsl(Hsl::new(-120.0, 1.0, 0.5)); + /// assert_eq!(color, Color::Rgb(0, 0, 255)); /// ``` #[cfg(feature = "palette")] pub fn from_hsl(hsl: palette::Hsl) -> Self { - use palette::{FromColor, Srgb}; - + use palette::{Clamp, FromColor, Srgb}; + let hsl = hsl.clamp(); let Srgb { red, green, @@ -410,25 +426,43 @@ impl Color { /// The `from_hsluv` function converts the Hue, Saturation and Lightness values to a /// corresponding `Color` RGB equivalent. /// - /// Hue values should be in the range [0, 360]. - /// Saturation and L values should be in the range [0, 100]. - /// Values that are not in the range are clamped to be within the range. + /// Hue values should be in the range [-180.0..180.0]. Values outside this range are normalized + /// by wrapping. + /// + /// Saturation and L values should be in the range [0.0..100.0]. Values outside this range are + /// clamped. + /// + /// Clamping to valid ranges happens before conversion to RGB. /// /// # Examples /// /// ``` /// use ratatui::{palette::Hsluv, style::Color}; /// - /// let color = Color::from_hsluv(Hsluv::new(360.0, 50.0, 75.0)); - /// assert_eq!(color, Color::Rgb(223, 171, 181)); - /// - /// let color: Color = Color::from_hsluv(Hsluv::new(0.0, 0.0, 0.0)); + /// // Minimum Lightness is black + /// let color: Color = Color::from_hsluv(Hsluv::new(0.0, 100.0, 0.0)); /// assert_eq!(color, Color::Rgb(0, 0, 0)); + /// + /// // Maximum Lightness is white + /// let color: Color = Color::from_hsluv(Hsluv::new(0.0, 0.0, 100.0)); + /// assert_eq!(color, Color::Rgb(255, 255, 255)); + /// + /// // Minimum Saturation is fully desaturated red = gray + /// let color = Color::from_hsluv(Hsluv::new(0.0, 0.0, 50.0)); + /// assert_eq!(color, Color::Rgb(119, 119, 119)); + /// + /// // Bright Red + /// let color = Color::from_hsluv(Hsluv::new(12.18, 100.0, 53.2)); + /// assert_eq!(color, Color::Rgb(255, 0, 0)); + /// + /// // Bright Blue + /// let color = Color::from_hsluv(Hsluv::new(-94.13, 100.0, 32.3)); + /// assert_eq!(color, Color::Rgb(0, 0, 255)); /// ``` #[cfg(feature = "palette")] pub fn from_hsluv(hsluv: palette::Hsluv) -> Self { - use palette::{FromColor, Srgb}; - + use palette::{Clamp, FromColor, Srgb}; + let hsluv = hsluv.clamp(); let Srgb { red, green, @@ -444,69 +478,61 @@ impl Color { mod tests { use std::error::Error; + #[cfg(feature = "palette")] + use palette::{Hsl, Hsluv}; + use rstest::rstest; #[cfg(feature = "serde")] use serde::de::{Deserialize, IntoDeserializer}; use super::*; #[cfg(feature = "palette")] - #[test] - fn test_hsl_to_rgb() { - use palette::Hsl; - - // Test with valid HSL values - let color = Color::from_hsl(Hsl::new(120.0, 50.0, 75.0)); - assert_eq!(color, Color::Rgb(159, 223, 159)); - - // Test with H value at upper bound - let color = Color::from_hsl(Hsl::new(360.0, 50.0, 75.0)); - assert_eq!(color, Color::Rgb(223, 159, 159)); - - // Test with H value exceeding the upper bound - let color = Color::from_hsl(Hsl::new(400.0, 50.0, 75.0)); - assert_eq!(color, Color::Rgb(223, 159, 159)); - - // Test with S and L values exceeding the upper bound - let color = Color::from_hsl(Hsl::new(240.0, 120.0, 150.0)); - assert_eq!(color, Color::Rgb(255, 255, 255)); - - // Test with H, S, and L values below the lower bound - let color = Color::from_hsl(Hsl::new(-20.0, -50.0, -20.0)); - assert_eq!(color, Color::Rgb(0, 0, 0)); - - // Test with S and L values below the lower bound - let color = Color::from_hsl(Hsl::new(60.0, -20.0, -10.0)); - assert_eq!(color, Color::Rgb(0, 0, 0)); + #[rstest] + #[case::black(Hsl::new(0.0, 0.0, 0.0), Color::Rgb(0, 0, 0))] + #[case::white(Hsl::new(0.0, 0.0, 1.0), Color::Rgb(255, 255, 255))] + #[case::valid(Hsl::new(120.0, 0.5, 0.75), Color::Rgb(159, 223, 159))] + #[case::min_hue(Hsl::new(-180.0, 0.5, 0.75), Color::Rgb(159, 223, 223))] + #[case::max_hue(Hsl::new(180.0, 0.5, 0.75), Color::Rgb(159, 223, 223))] + #[case::min_saturation(Hsl::new(0.0, 0.0, 0.5), Color::Rgb(128, 128, 128))] + #[case::max_saturation(Hsl::new(0.0, 1.0, 0.5), Color::Rgb(255, 0, 0))] + #[case::min_lightness(Hsl::new(0.0, 0.5, 0.0), Color::Rgb(0, 0, 0))] + #[case::max_lightness(Hsl::new(0.0, 0.5, 1.0), Color::Rgb(255, 255, 255))] + #[case::under_hue_wraps(Hsl::new(-240.0, 0.5, 0.75), Color::Rgb(159, 223, 159))] + #[case::over_hue_wraps(Hsl::new(480.0, 0.5, 0.75), Color::Rgb(159, 223, 159))] + #[case::under_saturation_clamps(Hsl::new(0.0, -0.5, 0.75), Color::Rgb(191, 191, 191))] + #[case::over_saturation_clamps(Hsl::new(0.0, 1.2, 0.75), Color::Rgb(255, 128, 128))] + #[case::under_lightness_clamps(Hsl::new(0.0, 0.5, -0.20), Color::Rgb(0, 0, 0))] + #[case::over_lightness_clamps(Hsl::new(0.0, 0.5, 1.5), Color::Rgb(255, 255, 255))] + #[case::under_saturation_lightness_clamps(Hsl::new(0.0, -0.5, -0.20), Color::Rgb(0, 0, 0))] + #[case::over_saturation_lightness_clamps(Hsl::new(0.0, 1.2, 1.5), Color::Rgb(255, 255, 255))] + fn test_hsl_to_rgb(#[case] hsl: palette::Hsl, #[case] expected: Color) { + assert_eq!(Color::from_hsl(hsl), expected); } #[cfg(feature = "palette")] - #[test] - fn test_hsluv_to_rgb() { - use palette::Hsluv; - - // Test with valid HSLuv values - let color = Color::from_hsluv(Hsluv::new(120.0, 50.0, 75.0)); - assert_eq!(color, Color::Rgb(147, 198, 129)); - - // Test with H value at upper bound - let color = Color::from_hsluv(Hsluv::new(360.0, 50.0, 75.0)); - assert_eq!(color, Color::Rgb(223, 171, 181)); - - // Test with H value exceeding the upper bound - let color = Color::from_hsluv(Hsluv::new(400.0, 50.0, 75.0)); - assert_eq!(color, Color::Rgb(226, 174, 140)); - - // Test with S and L values exceeding the upper bound - let color = Color::from_hsluv(Hsluv::new(240.0, 120.0, 150.0)); - assert_eq!(color, Color::Rgb(255, 255, 255)); - - // Test with H, S, and L values below the lower bound - let color = Color::from_hsluv(Hsluv::new(0.0, 0.0, 0.0)); - assert_eq!(color, Color::Rgb(0, 0, 0)); - - // Test with S and L values below the lower bound - let color = Color::from_hsluv(Hsluv::new(60.0, 0.0, 0.0)); - assert_eq!(color, Color::Rgb(0, 0, 0)); + #[rstest] + #[case::black(Hsluv::new(0.0, 0.0, 0.0), Color::Rgb(0, 0, 0))] + #[case::white(Hsluv::new(0.0, 0.0, 100.0), Color::Rgb(255, 255, 255))] + #[case::valid(Hsluv::new(120.0, 50.0, 75.0), Color::Rgb(147, 198, 129))] + #[case::min_hue(Hsluv::new(-180.0, 50.0, 75.0), Color::Rgb(135,196, 188))] + #[case::max_hue(Hsluv::new(180.0, 50.0, 75.0), Color::Rgb(135, 196, 188))] + #[case::min_saturation(Hsluv::new(0.0, 0.0, 75.0), Color::Rgb(185, 185, 185))] + #[case::max_saturation(Hsluv::new(0.0, 100.0, 75.0), Color::Rgb(255, 156, 177))] + #[case::min_lightness(Hsluv::new(0.0, 50.0, 0.0), Color::Rgb(0, 0, 0))] + #[case::max_lightness(Hsluv::new(0.0, 50.0, 100.0), Color::Rgb(255, 255, 255))] + #[case::under_hue_wraps(Hsluv::new(-240.0, 50.0, 75.0), Color::Rgb(147, 198, 129))] + #[case::over_hue_wraps(Hsluv::new(480.0, 50.0, 75.0), Color::Rgb(147, 198, 129))] + #[case::under_saturation_clamps(Hsluv::new(0.0, -50.0, 75.0), Color::Rgb(185, 185, 185))] + #[case::over_saturation_clamps(Hsluv::new(0.0, 150.0, 75.0), Color::Rgb(255, 156, 177))] + #[case::under_lightness_clamps(Hsluv::new(0.0, 50.0, -20.0), Color::Rgb(0, 0, 0))] + #[case::over_lightness_clamps(Hsluv::new(0.0, 50.0, 150.0), Color::Rgb(255, 255, 255))] + #[case::under_saturation_lightness_clamps(Hsluv::new(0.0, -50.0, -20.0), Color::Rgb(0, 0, 0))] + #[case::over_saturation_lightness_clamps( + Hsluv::new(0.0, 150.0, 150.0), + Color::Rgb(255, 255, 255) + )] + fn test_hsluv_to_rgb(#[case] hsluv: palette::Hsluv, #[case] expected: Color) { + assert_eq!(Color::from_hsluv(hsluv), expected); } #[test]