From 6db16d67fc3cc97f1e5bd4b7df02ce9f00756a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orhun=20Parmaks=C4=B1z?= Date: Tue, 15 Oct 2024 05:15:05 +0300 Subject: [PATCH] refactor(color)!: use palette types for Hsl/Hsluv conversions (#1418) BREAKING-CHANGE: Previously `Color::from_hsl` accepted components as individual f64 parameters. It now accepts a single `palette::Hsl` value and is gated behind a `palette` feature flag. ```diff - Color::from_hsl(360.0, 100.0, 100.0) + Color::from_hsl(Hsl::new(360.0, 100.0, 100.0)) ``` Fixes: --------- Co-authored-by: Josh McKinney --- BREAKING-CHANGES.md | 13 ++++ src/lib.rs | 3 + src/style/color.rs | 141 +++++++++++--------------------------------- 3 files changed, 52 insertions(+), 105 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index d6318d1f..b5f868ca 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -15,6 +15,7 @@ This is a quick summary of the sections below: - `Line` now implements `From` - `Table::highlight_style` is now `Table::row_highlight_style` - `Tabs::select` now accepts `Into>` + - `Color::from_hsl` is now behind the `palette` feature - [v0.28.0](#v0280) - `Backend::size` returns `Size` instead of `Rect` - `Backend` trait migrates to `get/set_cursor_position` @@ -72,6 +73,18 @@ This is a quick summary of the sections below: ## Unreleased +### `Color::from_hsl` is now behind the `palette` feature and accepts `palette::Hsl` ([#1418]) + +[#1418]: https://github.com/ratatui/ratatui/pull/1418 + +Previously `Color::from_hsl` accepted components as individual f64 parameters. It now accepts a +single `palette::Hsl` value and is gated behind a `palette` feature flag. + +```diff +- Color::from_hsl(360.0, 100.0, 100.0) ++ Color::from_hsl(Hsl::new(360.0, 100.0, 100.0)) +``` + ### Removed public fields from `Rect` iterators ([#1358]) [#1358]: https://github.com/ratatui/ratatui/pull/1358 diff --git a/src/lib.rs b/src/lib.rs index 2d420aba..ac2121c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -295,6 +295,9 @@ /// re-export the `crossterm` crate so that users don't have to add it as a dependency #[cfg(feature = "crossterm")] pub use crossterm; +/// re-export the `palette` crate so that users don't have to add it as a dependency +#[cfg(feature = "palette")] +pub use palette; #[cfg(feature = "crossterm")] pub use terminal::{ init, init_with_options, restore, try_init, try_init_with_options, try_restore, DefaultTerminal, diff --git a/src/style/color.rs b/src/style/color.rs index 70e3d30b..ea567677 100644 --- a/src/style/color.rs +++ b/src/style/color.rs @@ -381,22 +381,26 @@ impl Color { /// # Examples /// /// ``` - /// use ratatui::style::Color; + /// use ratatui::{palette::Hsl, style::Color}; /// - /// let color: Color = Color::from_hsl(360.0, 100.0, 100.0); + /// let color: Color = Color::from_hsl(Hsl::new(360.0, 100.0, 100.0)); /// assert_eq!(color, Color::Rgb(255, 255, 255)); /// - /// let color: Color = Color::from_hsl(0.0, 0.0, 0.0); + /// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 0.0)); /// assert_eq!(color, Color::Rgb(0, 0, 0)); /// ``` - pub fn from_hsl(h: f64, s: f64, l: f64) -> Self { - // Clamp input values to valid ranges - let h = h.clamp(0.0, 360.0); - let s = s.clamp(0.0, 100.0); - let l = l.clamp(0.0, 100.0); + #[cfg(feature = "palette")] + pub fn from_hsl(hsl: palette::Hsl) -> Self { + use palette::{FromColor, Srgb}; - // Delegate to the function for normalized HSL to RGB conversion - normalized_hsl_to_rgb(h / 360.0, s / 100.0, l / 100.0) + let Srgb { + red, + green, + blue, + standard: _, + }: Srgb = Srgb::from_color(hsl).into(); + + Self::Rgb(red, green, blue) } /// Converts a `HSLuv` representation to a `Color::Rgb` instance. @@ -411,19 +415,18 @@ impl Color { /// # Examples /// /// ``` - /// use ratatui::prelude::*; + /// use ratatui::{palette::Hsluv, style::Color}; /// - /// let color = Color::from_hsluv(360.0, 50.0, 75.0); + /// 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(0.0, 0.0, 0.0); + /// let color: Color = Color::from_hsluv(Hsluv::new(0.0, 0.0, 0.0)); /// assert_eq!(color, Color::Rgb(0, 0, 0)); /// ``` #[cfg(feature = "palette")] - pub fn from_hsluv(h: f64, s: f64, l: f64) -> Self { - use palette::{Clamp, FromColor, Hsluv, Srgb}; + pub fn from_hsluv(hsluv: palette::Hsluv) -> Self { + use palette::{FromColor, Srgb}; - let hsluv = Hsluv::new(h, s, l).clamp(); let Srgb { red, green, @@ -435,83 +438,6 @@ impl Color { } } -/// Converts normalized HSL (Hue, Saturation, Lightness) values to RGB (Red, Green, Blue) color -/// representation. H, S, and L values should be in the range [0, 1]. -/// -/// Based on -fn normalized_hsl_to_rgb(hue: f64, saturation: f64, lightness: f64) -> Color { - // This function can be made into `const` in the future. - // This comment contains the relevant information for making it `const`. - // - // If it is `const` and made public, users can write the following: - // - // ```rust - // const SLATE_50: Color = normalized_hsl_to_rgb(0.210, 0.40, 0.98); - // ``` - // - // For it to be const now, we need `#![feature(const_fn_floating_point_arithmetic)]` - // Tracking issue: https://github.com/rust-lang/rust/issues/57241 - // - // We would also need to remove the use of `.round()` in this function, i.e.: - // - // ```rust - // Color::Rgb((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8) - // ``` - - // Initialize RGB components - let red: f64; - let green: f64; - let blue: f64; - - // Check if the color is achromatic (grayscale) - if saturation == 0.0 { - red = lightness; - green = lightness; - blue = lightness; - } else { - // Calculate RGB components for colored cases - let q = if lightness < 0.5 { - lightness * (1.0 + saturation) - } else { - lightness + saturation - lightness * saturation - }; - let p = 2.0 * lightness - q; - red = hue_to_rgb(p, q, hue + 1.0 / 3.0); - green = hue_to_rgb(p, q, hue); - blue = hue_to_rgb(p, q, hue - 1.0 / 3.0); - } - - // Scale RGB components to the range [0, 255] and create a Color::Rgb instance - Color::Rgb( - (red * 255.0).round() as u8, - (green * 255.0).round() as u8, - (blue * 255.0).round() as u8, - ) -} - -/// Helper function to calculate RGB component for a specific hue value. -fn hue_to_rgb(p: f64, q: f64, t: f64) -> f64 { - // Adjust the hue value to be within the valid range [0, 1] - let mut t = t; - if t < 0.0 { - t += 1.0; - } - if t > 1.0 { - t -= 1.0; - } - - // Calculate the RGB component based on the hue value - if t < 1.0 / 6.0 { - p + (q - p) * 6.0 * t - } else if t < 1.0 / 2.0 { - q - } else if t < 2.0 / 3.0 { - p + (q - p) * (2.0 / 3.0 - t) * 6.0 - } else { - p - } -} - #[cfg(test)] mod tests { use std::error::Error; @@ -521,58 +447,63 @@ mod tests { use super::*; + #[cfg(feature = "palette")] #[test] fn test_hsl_to_rgb() { + use palette::Hsl; + // Test with valid HSL values - let color = Color::from_hsl(120.0, 50.0, 75.0); + 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(360.0, 50.0, 75.0); + 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(400.0, 50.0, 75.0); + 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(240.0, 120.0, 150.0); + 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(-20.0, -50.0, -20.0); + 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(60.0, -20.0, -10.0); + let color = Color::from_hsl(Hsl::new(60.0, -20.0, -10.0)); assert_eq!(color, Color::Rgb(0, 0, 0)); } #[cfg(feature = "palette")] #[test] fn test_hsluv_to_rgb() { + use palette::Hsluv; + // Test with valid HSLuv values - let color = Color::from_hsluv(120.0, 50.0, 75.0); + 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(360.0, 50.0, 75.0); + 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(400.0, 50.0, 75.0); + 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(240.0, 120.0, 150.0); + 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(0.0, 0.0, 0.0); + 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(60.0, 0.0, 0.0); + let color = Color::from_hsluv(Hsluv::new(60.0, 0.0, 0.0)); assert_eq!(color, Color::Rgb(0, 0, 0)); }