From a97a2c6bfd0dd358668d3393074d0f68f74e7696 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 7 Jun 2022 15:23:24 +0100 Subject: [PATCH] Add support for HSL and HSLA --- sandbox/hsl.py | 11 ++++++++++ sandbox/hsl.scss | 5 +++++ src/textual/color.py | 35 ++++++++++++++++++++++++------ src/textual/css/_styles_builder.py | 14 +++++++++--- src/textual/css/scalar.py | 13 ++++++++++- src/textual/css/tokenize.py | 14 ++++++++++-- 6 files changed, 79 insertions(+), 13 deletions(-) create mode 100644 sandbox/hsl.py create mode 100644 sandbox/hsl.scss diff --git a/sandbox/hsl.py b/sandbox/hsl.py new file mode 100644 index 000000000..17c0d4f49 --- /dev/null +++ b/sandbox/hsl.py @@ -0,0 +1,11 @@ +from textual.app import App, ComposeResult +from textual.widgets import Static + + +class HSLApp(App): + def compose(self) -> ComposeResult: + yield Static(classes="box") + + +app = HSLApp(css_path="hsl.scss", watch_css=True) +app.run() diff --git a/sandbox/hsl.scss b/sandbox/hsl.scss new file mode 100644 index 000000000..78c0a495f --- /dev/null +++ b/sandbox/hsl.scss @@ -0,0 +1,5 @@ +.box { + height: 1fr; + /*background: rgb(180,50, 50);*/ + background: hsl(180,50%, 50%); +} diff --git a/src/textual/color.py b/src/textual/color.py index 3977736a7..84cfc3502 100644 --- a/src/textual/color.py +++ b/src/textual/color.py @@ -23,6 +23,8 @@ from rich.color import Color as RichColor from rich.style import Style from rich.text import Text +from textual.css.scalar import percentage_string_to_float +from textual.css.tokenize import COMMA, OPEN_BRACE, CLOSE_BRACE, DECIMAL, PERCENT from textual.suggestions import get_suggestion from ._color_constants import COLOR_NAME_TO_RGB from .geometry import clamp @@ -53,13 +55,15 @@ class Lab(NamedTuple): RE_COLOR = re.compile( - r"""^ -\#([0-9a-fA-F]{3})$| -\#([0-9a-fA-F]{4})$| -\#([0-9a-fA-F]{6})$| -\#([0-9a-fA-F]{8})$| -rgb\((\-?\d+\.?\d*,\-?\d+\.?\d*,\-?\d+\.?\d*)\)$| -rgba\((\-?\d+\.?\d*,\-?\d+\.?\d*,\-?\d+\.?\d*,\-?\d+\.?\d*)\)$ + rf"""^ +\#([0-9a-fA-F]{{3}})$| +\#([0-9a-fA-F]{{4}})$| +\#([0-9a-fA-F]{{6}})$| +\#([0-9a-fA-F]{{8}})$| +rgb{OPEN_BRACE}({DECIMAL}{COMMA}{DECIMAL}{COMMA}{DECIMAL}){CLOSE_BRACE}$| +rgba{OPEN_BRACE}({DECIMAL}{COMMA}{DECIMAL}{COMMA}{DECIMAL}{COMMA}{DECIMAL}){CLOSE_BRACE}$| +hsl{OPEN_BRACE}({DECIMAL}{COMMA}{PERCENT}{COMMA}{PERCENT}){CLOSE_BRACE}$| +hsla{OPEN_BRACE}({DECIMAL}{COMMA}{PERCENT}{COMMA}{PERCENT}{COMMA}{DECIMAL}){CLOSE_BRACE}$| """, re.VERBOSE, ) @@ -123,7 +127,9 @@ class Color(NamedTuple): Returns: Color: A new color. """ + print("A") r, g, b = hls_to_rgb(h, l, s) + print("B") return cls(int(r * 255 + 0.5), int(g * 255 + 0.5), int(b * 255 + 0.5)) def __rich__(self) -> Text: @@ -296,6 +302,8 @@ class Color(NamedTuple): rgba_hex, rgb, rgba, + hsl, + hsla, ) = color_match.groups() if rgb_hex_triple is not None: @@ -328,6 +336,19 @@ class Color(NamedTuple): clamp(int(float_b), 0, 255), clamp(float_a, 0.0, 1.0), ) + elif hsl is not None: + h, s, l = [value.strip() for value in hsl.split(",")] + h = clamp(int(h), 0, 360) / 360 + s = percentage_string_to_float(s) + l = percentage_string_to_float(l) + color = Color.from_hls(h, l, s) + elif hsla is not None: + h, s, l, a = [value.strip() for value in hsl.split(",")] + h = clamp(h, 0, 360) + s = percentage_string_to_float(s) + l = percentage_string_to_float(l) + a = clamp(a, 0.0, 1.0) + color = Color.from_hls(h, l, s).with_alpha(a) else: raise AssertionError("Can't get here if RE_COLOR matches") return color diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index c9d4e40cb..5f91c9fdc 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -1,5 +1,7 @@ from __future__ import annotations +import re +import sys from functools import lru_cache from typing import cast, Iterable, NoReturn, Sequence @@ -40,7 +42,14 @@ from .constants import ( ) from .errors import DeclarationError, StyleValueError from .model import Declaration -from .scalar import Scalar, ScalarOffset, Unit, ScalarError, ScalarParseError +from .scalar import ( + Scalar, + ScalarOffset, + Unit, + ScalarError, + ScalarParseError, + percentage_string_to_float, +) from .styles import DockGroup, Styles from .tokenize import Token from .transition import Transition @@ -333,9 +342,8 @@ class StylesBuilder: token_name = token.name value = token.value if token_name == "scalar" and value.endswith("%"): - percentage = value[:-1] try: - opacity = clamp(float(percentage) / 100, 0, 1) + opacity = percentage_string_to_float(value) self.styles.set_rule(name, opacity) except ValueError: error = True diff --git a/src/textual/css/scalar.py b/src/textual/css/scalar.py index f0a4083d9..6e98df182 100644 --- a/src/textual/css/scalar.py +++ b/src/textual/css/scalar.py @@ -8,7 +8,7 @@ from typing import Iterable, NamedTuple import rich.repr -from ..geometry import Offset, Size +from ..geometry import Offset, Size, clamp class ScalarError(Exception): @@ -334,6 +334,17 @@ class ScalarOffset(NamedTuple): NULL_SCALAR = ScalarOffset(Scalar.from_number(0), Scalar.from_number(0)) + +def percentage_string_to_float(string: str) -> float: + string = string.strip() + if string.endswith("%"): + percentage = string[:-1] + float_percentage = clamp(float(percentage) / 100, 0, 1) + else: + float_percentage = float(string) + return float_percentage + + if __name__ == "__main__": print(Scalar.parse("3.14fr")) s = Scalar.parse("23") diff --git a/src/textual/css/tokenize.py b/src/textual/css/tokenize.py index dff909c4b..879a66e80 100644 --- a/src/textual/css/tokenize.py +++ b/src/textual/css/tokenize.py @@ -6,11 +6,21 @@ from typing import Iterable from textual.css.tokenizer import Expect, Tokenizer, Token +PERCENT = r"-?\d+\.?\d*%" +DECIMAL = r"-?\d+\.?\d*" +COMMA = r",\s*" +OPEN_BRACE = r"\(\s*" +CLOSE_BRACE = r"\s*\)" + +HEX_COLOR = r"\#[0-9a-fA-F]{8}|\#[0-9a-fA-F]{6}|\#[0-9a-fA-F]{4}|\#[0-9a-fA-F]{3}" +RGB_COLOR = rf"rgb{OPEN_BRACE}{DECIMAL}{COMMA}{DECIMAL}{COMMA}{DECIMAL}{CLOSE_BRACE}|rgba{OPEN_BRACE}{DECIMAL}{COMMA}{DECIMAL}{COMMA}{DECIMAL}{COMMA}{DECIMAL}{CLOSE_BRACE}" +HSL_COLOR = rf"hsl{OPEN_BRACE}{DECIMAL}{COMMA}{PERCENT}{COMMA}{PERCENT}{CLOSE_BRACE}|hsla{OPEN_BRACE}{DECIMAL}{COMMA}{PERCENT}{COMMA}{PERCENT}{COMMA}{DECIMAL}{CLOSE_BRACE}" + COMMENT_START = r"\/\*" -SCALAR = r"\-?\d+\.?\d*(?:fr|%|w|h|vw|vh)" +SCALAR = rf"{DECIMAL}(?:fr|%|w|h|vw|vh)" DURATION = r"\d+\.?\d*(?:ms|s)" NUMBER = r"\-?\d+\.?\d*" -COLOR = r"\#[0-9a-fA-F]{8}|\#[0-9a-fA-F]{6}|\#[0-9a-fA-F]{4}|\#[0-9a-fA-F]{3}|rgb\(\-?\d+\.?\d*,\-?\d+\.?\d*,\-?\d+\.?\d*\)|rgba\(\-?\d+\.?\d*,\-?\d+\.?\d*,\-?\d+\.?\d*,\-?\d+\.?\d*\)" +COLOR = f"{HEX_COLOR}|{RGB_COLOR}|{HSL_COLOR}" KEY_VALUE = r"[a-zA-Z_-][a-zA-Z0-9_-]*=[0-9a-zA-Z_\-\/]+" TOKEN = "[a-zA-Z][a-zA-Z0-9_-]*" STRING = r"\".*?\""