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: <https://github.com/ratatui/ratatui/issues/1414>

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
This commit is contained in:
Orhun Parmaksız 2024-10-15 05:15:05 +03:00 committed by GitHub
parent cc7497532a
commit 6db16d67fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 52 additions and 105 deletions

View file

@ -15,6 +15,7 @@ This is a quick summary of the sections below:
- `Line` now implements `From<Cow<str>` - `Line` now implements `From<Cow<str>`
- `Table::highlight_style` is now `Table::row_highlight_style` - `Table::highlight_style` is now `Table::row_highlight_style`
- `Tabs::select` now accepts `Into<Option<usize>>` - `Tabs::select` now accepts `Into<Option<usize>>`
- `Color::from_hsl` is now behind the `palette` feature
- [v0.28.0](#v0280) - [v0.28.0](#v0280)
- `Backend::size` returns `Size` instead of `Rect` - `Backend::size` returns `Size` instead of `Rect`
- `Backend` trait migrates to `get/set_cursor_position` - `Backend` trait migrates to `get/set_cursor_position`
@ -72,6 +73,18 @@ This is a quick summary of the sections below:
## Unreleased ## 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]) ### Removed public fields from `Rect` iterators ([#1358])
[#1358]: https://github.com/ratatui/ratatui/pull/1358 [#1358]: https://github.com/ratatui/ratatui/pull/1358

View file

@ -295,6 +295,9 @@
/// re-export the `crossterm` crate so that users don't have to add it as a dependency /// re-export the `crossterm` crate so that users don't have to add it as a dependency
#[cfg(feature = "crossterm")] #[cfg(feature = "crossterm")]
pub use 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")] #[cfg(feature = "crossterm")]
pub use terminal::{ pub use terminal::{
init, init_with_options, restore, try_init, try_init_with_options, try_restore, DefaultTerminal, init, init_with_options, restore, try_init, try_init_with_options, try_restore, DefaultTerminal,

View file

@ -381,22 +381,26 @@ impl Color {
/// # Examples /// # 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)); /// 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)); /// assert_eq!(color, Color::Rgb(0, 0, 0));
/// ``` /// ```
pub fn from_hsl(h: f64, s: f64, l: f64) -> Self { #[cfg(feature = "palette")]
// Clamp input values to valid ranges pub fn from_hsl(hsl: palette::Hsl) -> Self {
let h = h.clamp(0.0, 360.0); use palette::{FromColor, Srgb};
let s = s.clamp(0.0, 100.0);
let l = l.clamp(0.0, 100.0);
// Delegate to the function for normalized HSL to RGB conversion let Srgb {
normalized_hsl_to_rgb(h / 360.0, s / 100.0, l / 100.0) red,
green,
blue,
standard: _,
}: Srgb<u8> = Srgb::from_color(hsl).into();
Self::Rgb(red, green, blue)
} }
/// Converts a `HSLuv` representation to a `Color::Rgb` instance. /// Converts a `HSLuv` representation to a `Color::Rgb` instance.
@ -411,19 +415,18 @@ impl Color {
/// # Examples /// # 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)); /// 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)); /// assert_eq!(color, Color::Rgb(0, 0, 0));
/// ``` /// ```
#[cfg(feature = "palette")] #[cfg(feature = "palette")]
pub fn from_hsluv(h: f64, s: f64, l: f64) -> Self { pub fn from_hsluv(hsluv: palette::Hsluv) -> Self {
use palette::{Clamp, FromColor, Hsluv, Srgb}; use palette::{FromColor, Srgb};
let hsluv = Hsluv::new(h, s, l).clamp();
let Srgb { let Srgb {
red, red,
green, 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 <https://github.com/killercup/hsl-rs/blob/b8a30e11afd75f262e0550725333293805f4ead0/src/lib.rs>
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)] #[cfg(test)]
mod tests { mod tests {
use std::error::Error; use std::error::Error;
@ -521,58 +447,63 @@ mod tests {
use super::*; use super::*;
#[cfg(feature = "palette")]
#[test] #[test]
fn test_hsl_to_rgb() { fn test_hsl_to_rgb() {
use palette::Hsl;
// Test with valid HSL values // 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)); assert_eq!(color, Color::Rgb(159, 223, 159));
// Test with H value at upper bound // 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)); assert_eq!(color, Color::Rgb(223, 159, 159));
// Test with H value exceeding the upper bound // 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)); assert_eq!(color, Color::Rgb(223, 159, 159));
// Test with S and L values exceeding the upper bound // 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)); assert_eq!(color, Color::Rgb(255, 255, 255));
// Test with H, S, and L values below the lower bound // 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)); assert_eq!(color, Color::Rgb(0, 0, 0));
// Test with S and L values below the lower bound // 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)); assert_eq!(color, Color::Rgb(0, 0, 0));
} }
#[cfg(feature = "palette")] #[cfg(feature = "palette")]
#[test] #[test]
fn test_hsluv_to_rgb() { fn test_hsluv_to_rgb() {
use palette::Hsluv;
// Test with valid HSLuv values // 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)); assert_eq!(color, Color::Rgb(147, 198, 129));
// Test with H value at upper bound // 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)); assert_eq!(color, Color::Rgb(223, 171, 181));
// Test with H value exceeding the upper bound // 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)); assert_eq!(color, Color::Rgb(226, 174, 140));
// Test with S and L values exceeding the upper bound // 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)); assert_eq!(color, Color::Rgb(255, 255, 255));
// Test with H, S, and L values below the lower bound // 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)); assert_eq!(color, Color::Rgb(0, 0, 0));
// Test with S and L values below the lower bound // 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)); assert_eq!(color, Color::Rgb(0, 0, 0));
} }