added color reference and docs

This commit is contained in:
Will McGugan
2022-09-10 22:00:51 +01:00
parent b1fa3c0a2a
commit b6341ddb50
10 changed files with 161 additions and 126 deletions

View File

@@ -5,7 +5,7 @@ Textual provides a large number of *styles* you can use to customize how your ap
## Styles object ## Styles object
Every widget class in Textual provides a `styles` object which contains a number of writable attributes. Styles define the position and size of a widget, in addition to color, text style, borders, alignment and much more. Every widget class in Textual provides a `styles` object which contains a number of writable attributes. You can write to any of these attributes and Textual will update the screen accordingly.
Let's look at a simple example which sets the styles on the `screen` (a special widget that represents the screen). Let's look at a simple example which sets the styles on the `screen` (a special widget that represents the screen).
@@ -13,13 +13,21 @@ Let's look at a simple example which sets the styles on the `screen` (a special
--8<-- "docs/examples/guide/styles/screen.py" --8<-- "docs/examples/guide/styles/screen.py"
``` ```
The first line sets `screen.styles.background` to `"darkblue"` which will change the background color to dark blue. There are a few other ways of setting color which we will explore later. The first line sets [background](../styles/background.md) to `"darkblue"` which will change the background color to dark blue. There are a few other ways of setting color which we will explore later.
The second line sets `screen.styles.border` to a tuple of `("heavy", "white")` which tells Textual to draw a white border with a style of `"heavy"`. Running this code will show the following: The second line sets [border](../styles/border.md) to a tuple of `("heavy", "white")` which tells Textual to draw a white border with a style of `"heavy"`. Running this code will show the following:
```{.textual path="docs/examples/guide/styles/screen.py"} ```{.textual path="docs/examples/guide/styles/screen.py"}
``` ```
## Colors
The [color](../styles/color.md) property set the color of text on a widget. The [background](..styles/background/md) property sets the color of the background (under the text).
## Box Model ## Box Model
@@ -29,7 +37,6 @@ The second line sets `screen.styles.border` to a tuple of `("heavy", "white")` w
TODO: Styles docs TODO: Styles docs
- What are styles - What are styles

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -124,6 +124,9 @@ markdown_extensions:
- name: textual - name: textual
class: textual class: textual
format: !!python/name:textual._doc.format_svg format: !!python/name:textual._doc.format_svg
- name: rich
class: rich
format: !!python/name:textual._doc.rich
- pymdownx.inlinehilite - pymdownx.inlinehilite
- pymdownx.superfences - pymdownx.superfences
- pymdownx.snippets - pymdownx.snippets

28
sandbox/will/box.css Normal file
View File

@@ -0,0 +1,28 @@
Screen {
background: white;
color:black;
}
#box1 {
width: 10;
height: 5;
background: red 40%;
box-sizing: content-box;
}
#box2 {
width: 10;
height: 5;
padding: 1;
background:blue 40%;
box-sizing: content-box;
}
#box3 {
width: 10;
height: 5;
background:green 40%;
border: heavy;
box-sizing: content-box;
}

13
sandbox/will/box.py Normal file
View File

@@ -0,0 +1,13 @@
from textual.app import App
from textual.widgets import Static
class BoxApp(App):
def compose(self):
yield Static("0123456789", id="box1")
yield Static("0123456789", id="box2")
yield Static("0123456789", id="box3")
app = BoxApp(css_path="box.css")
app.run()

View File

@@ -50,3 +50,32 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str
import traceback import traceback
traceback.print_exception(error) traceback.print_exception(error)
def rich(source, language, css_class, options, md, attrs, **kwargs) -> str:
"""A superfences formatter to insert a SVG screenshot."""
from rich.console import Console
import io
title = attrs.get("title", "Rich")
console = Console(
file=io.StringIO(),
record=True,
force_terminal=True,
color_system="truecolor",
)
error_console = Console(stderr=True)
globals: dict = {}
try:
exec(source, globals)
except Exception:
error_console.print_exception()
console.bell()
if "output" in globals:
console.print(globals["output"])
output_svg = console.export_svg(title=title)
return output_svg

View File

@@ -1,9 +1,31 @@
""" """
Manages Color in Textual. This module contains a powerful Color class which Textual uses to expose colors.
The only exception would be for Rich renderables, which require a rich.color.Color instance.
You can convert from a Textual color to a Rich color with the [rich_color][textual.color.Color.rich_color] property.
## Named colors
The following named colors are used by the [parse][textual.color.Color.parse] method.
```{.rich title="colors"}
from textual._color_constants import COLOR_NAME_TO_RGB
from rich.table import Table
from rich.text import Text
table = Table("Name", "RGB", "Color", expand=True, highlight=True)
for name, triplet in sorted(COLOR_NAME_TO_RGB.items()):
if len(triplet) != 3:
continue
r, g, b = triplet
table.add_row(
f'"{name}"',
f"rgb({r}, {g}, {b})",
Text(" ", style=f"on rgb({r},{g},{b})")
)
output = table
```
All instances where the developer is presented with a color will use this class. The only
exception should be when passing things to a Rich renderable, which will need to use the
`rich_color` attribute to perform a conversion.
""" """
@@ -87,23 +109,21 @@ split_pairs4: Callable[[str], tuple[str, str, str, str]] = itemgetter(
class ColorParseError(Exception): class ColorParseError(Exception):
"""A color failed to parse""" """A color failed to parse.
Args:
message (str): the error message
suggested_color (str | None): a close color we can suggest. Defaults to None.
"""
def __init__(self, message: str, suggested_color: str | None = None): def __init__(self, message: str, suggested_color: str | None = None):
"""
Creates a new ColorParseError
Args:
message (str): the error message
suggested_color (str | None): a close color we can suggest. Defaults to None.
"""
super().__init__(message) super().__init__(message)
self.suggested_color = suggested_color self.suggested_color = suggested_color
@rich.repr.auto @rich.repr.auto
class Color(NamedTuple): class Color(NamedTuple):
"""A class to represent a single RGB color with alpha.""" """A class to represent a RGB color with an alpha component."""
r: int r: int
"""Red component (0-255)""" """Red component (0-255)"""
@@ -310,7 +330,29 @@ class Color(NamedTuple):
@classmethod @classmethod
@lru_cache(maxsize=1024 * 4) @lru_cache(maxsize=1024 * 4)
def parse(cls, color_text: str | Color) -> Color: def parse(cls, color_text: str | Color) -> Color:
"""Parse a string containing a CSS-style color. """Parse a string containing a named color or CSS-style color.
Colors may be parsed from the following formats:
Text beginning with a `#` is parsed as hex:
R, G, and B must be hex digits (0-9A-F)
- `#RGB`
- `#RRGGBB`
- `#RRGGBBAA`
Text in the following formats is parsed as decimal values:
RED, GREEN, and BLUE must be numbers between 0 and 255.
ALPHA should ba a value between 0 and 1.
- `rgb(RED,GREEN,BLUE)`
- `rgba(RED,GREEN,BLUE,ALPHA)`
- `hsl(RED,GREEN,BLUE)`
- `hsl(RED,GREEN,BLUE,ALPHA)`
All other text will raise a `ColorParseError`.
Args: Args:
color_text (str | Color): Text with a valid color format. Color objects will color_text (str | Color): Text with a valid color format. Color objects will
@@ -445,43 +487,6 @@ BLACK = Color(0, 0, 0)
TRANSPARENT = Color(0, 0, 0, 0) TRANSPARENT = Color(0, 0, 0, 0)
class ColorPair(NamedTuple):
"""A pair of colors for foreground and background."""
foreground: Color
background: Color
def __rich_repr__(self) -> rich.repr.Result:
yield "foreground", self.foreground
yield "background", self.background
@property
def style(self) -> Style:
"""A Rich style with foreground and background."""
return self._get_style()
@lru_cache(maxsize=1024 * 4)
def _get_style(self) -> Style:
"""Get a Rich style, foreground adjusted for transparency."""
r, g, b, a = self.foreground
if a == 0:
return Style.from_color(
self.background.rich_color, self.background.rich_color
)
elif a == 1:
return Style.from_color(
self.foreground.rich_color, self.background.rich_color
)
else:
r2, g2, b2, _ = self.background
return Style.from_color(
RichColor.from_rgb(
r + (r2 - r) * a, g + (g2 - g) * a, b + (b2 - b) * a
),
self.background.rich_color,
)
def rgb_to_lab(rgb: Color) -> Lab: def rgb_to_lab(rgb: Color) -> Lab:
"""Convert an RGB color to the CIE-L*ab format. """Convert an RGB color to the CIE-L*ab format.
@@ -534,27 +539,3 @@ def lab_to_rgb(lab: Lab) -> Color:
b = 1.055 * pow(b, 1 / 2.4) - 0.055 if b > 0.0031308 else 12.92 * b b = 1.055 * pow(b, 1 / 2.4) - 0.055 if b > 0.0031308 else 12.92 * b
return Color(int(r * 255), int(g * 255), int(b * 255)) return Color(int(r * 255), int(g * 255), int(b * 255))
if __name__ == "__main__":
from rich import print
c1 = Color.parse("#112233")
print(c1, c1.hex, c1.css)
c2 = Color.parse("#11223344")
print(c2)
c3 = Color.parse("rgb(10,20,30)")
print(c3)
c4 = Color.parse("rgba(10,20,30,0.5)")
print(c4, c4.hex, c4.css)
p1 = ColorPair(c4, c1)
print(p1)
print(p1.style)
print(Color.parse("dark_blue"))

View File

@@ -28,7 +28,7 @@ from ._help_text import (
color_property_help_text, color_property_help_text,
) )
from .._border import INVISIBLE_EDGE_TYPES, normalize_border_value from .._border import INVISIBLE_EDGE_TYPES, normalize_border_value
from ..color import Color, ColorPair, ColorParseError from ..color import Color, ColorParseError
from ._error_tools import friendly_list from ._error_tools import friendly_list
from .constants import NULL_SPACING, VALID_STYLE_FLAGS from .constants import NULL_SPACING, VALID_STYLE_FLAGS
from .errors import StyleTypeError, StyleValueError from .errors import StyleTypeError, StyleValueError
@@ -452,27 +452,6 @@ class BorderProperty:
obj.refresh(layout=self._layout) obj.refresh(layout=self._layout)
class StyleProperty:
"""Descriptor for getting the Rich style."""
DEFAULT_STYLE = Style()
def __get__(
self, obj: StylesBase, objtype: type[StylesBase] | None = None
) -> Style:
"""Get the Style
Args:
obj (Styles): The ``Styles`` object
objtype (type[Styles]): The ``Styles`` class
Returns:
A ``Style`` object.
"""
style = ColorPair(obj.color, obj.background).style + obj.text_style
return style
class SpacingProperty: class SpacingProperty:
"""Descriptor for getting and setting spacing properties (e.g. padding and margin).""" """Descriptor for getting and setting spacing properties (e.g. padding and margin)."""

View File

@@ -2,7 +2,7 @@ import pytest
from rich.color import Color as RichColor from rich.color import Color as RichColor
from rich.text import Text from rich.text import Text
from textual.color import Color, ColorPair, Lab, lab_to_rgb, rgb_to_lab from textual.color import Color, Lab, lab_to_rgb, rgb_to_lab
def test_rich_color(): def test_rich_color():
@@ -31,22 +31,6 @@ def test_css():
assert Color(10, 20, 30, 0.5).css == "rgba(10,20,30,0.5)" assert Color(10, 20, 30, 0.5).css == "rgba(10,20,30,0.5)"
def test_colorpair_style():
"""Test conversion of colorpair to style."""
# Black on white
assert (
str(ColorPair(Color.parse("#000000"), Color.parse("#ffffff")).style)
== "#000000 on #ffffff"
)
# 50% black on white
assert (
str(ColorPair(Color.parse("rgba(0,0,0,0.5)"), Color.parse("#ffffff")).style)
== "#7f7f7f on #ffffff"
)
def test_rgb(): def test_rgb():
assert Color(10, 20, 30, 0.55).rgb == (10, 20, 30) assert Color(10, 20, 30, 0.55).rgb == (10, 20, 30)
@@ -221,8 +205,3 @@ def test_rgb_lab_rgb_roundtrip():
assert c_.r == pytest.approx(r, abs=1) assert c_.r == pytest.approx(r, abs=1)
assert c_.g == pytest.approx(g, abs=1) assert c_.g == pytest.approx(g, abs=1)
assert c_.b == pytest.approx(b, abs=1) assert c_.b == pytest.approx(b, abs=1)
def test_color_pair_style():
pair = ColorPair(Color(220, 220, 220), Color(10, 20, 30))
assert str(pair.style) == "#dcdcdc on #0a141e"