Skip to content

feat(color): add HSV support to Color class #5803

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fixed `VERTICAL_BREAKPOINTS` doesn't work https://github.com/Textualize/textual/pull/5785
- Fixed `Button` allowing text selection https://github.com/Textualize/textual/pull/5770

### Added

- Added `Color.hsv` property and `Color.from_hsv` class method https://github.com/Textualize/textual/pull/5803

## [3.2.0] - 2025-05-02

### Fixed
Expand Down
32 changes: 30 additions & 2 deletions src/textual/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from __future__ import annotations

import re
from colorsys import hls_to_rgb, rgb_to_hls
from colorsys import hls_to_rgb, hsv_to_rgb, rgb_to_hls, rgb_to_hsv
from functools import lru_cache
from operator import itemgetter
from typing import Callable, NamedTuple
Expand Down Expand Up @@ -82,7 +82,7 @@ class HSV(NamedTuple):
s: float
"""Saturation in range 0 to 1."""
v: float
"""Value un range 0 to 1."""
"""Value in range 0 to 1."""


class Lab(NamedTuple):
Expand Down Expand Up @@ -212,6 +212,21 @@ def from_hsl(cls, h: float, s: float, l: float) -> Color:
r, g, b = hls_to_rgb(h, l, s)
return cls(int(r * 255 + 0.5), int(g * 255 + 0.5), int(b * 255 + 0.5))

@classmethod
def from_hsv(cls, h: float, s: float, v: float) -> Color:
"""Create a color from HSV components.

Args:
h: Hue.
s: Saturation.
v: Value.

Returns:
A new color.
"""
r, g, b = hsv_to_rgb(h, s, v)
return cls(int(r * 255 + 0.5), int(g * 255 + 0.5), int(b * 255 + 0.5))

@property
def inverse(self) -> Color:
"""The inverse of this color.
Expand Down Expand Up @@ -286,6 +301,19 @@ def hsl(self) -> HSL:
h, l, s = rgb_to_hls(r, g, b)
return HSL(h, s, l)

@property
def hsv(self) -> HSV:
"""This color in HSV format.

HSV color is an alternative way of representing a color, which can be used in certain color calculations.

Returns:
Color encoded in HSV format.
"""
r, g, b = self.normalized
h, s, v = rgb_to_hsv(r, g, b)
return HSV(h, s, v)

@property
def brightness(self) -> float:
"""The human perceptual brightness.
Expand Down
11 changes: 11 additions & 0 deletions tests/test_color.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ def test_hsl():
assert red.hsl.css == "hsl(356,81.8%,43.1%)"


def test_hsv():
red = Color(200, 20, 32)
print(red.hsv)
assert red.hsv == pytest.approx(
(0.9888888888888889, 0.8999999999999999, 0.7843137254901961)
)
assert Color.from_hsv(
0.9888888888888889, 0.8999999999999999, 0.7843137254901961
).normalized == pytest.approx(red.normalized, rel=1e-5)


def test_color_brightness():
assert Color(255, 255, 255).brightness == 1
assert Color(0, 0, 0).brightness == 0
Expand Down
Loading