mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
added color reference and docs
This commit is contained in:
@@ -5,7 +5,7 @@ Textual provides a large number of *styles* you can use to customize how your ap
|
||||
|
||||
## 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).
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
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"}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -29,7 +37,6 @@ The second line sets `screen.styles.border` to a tuple of `("heavy", "white")` w
|
||||
|
||||
|
||||
|
||||
|
||||
TODO: Styles docs
|
||||
|
||||
- What are styles
|
||||
|
||||
16
docs/images/styles/border_box.excalidraw.svg
Normal file
16
docs/images/styles/border_box.excalidraw.svg
Normal file
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 |
@@ -124,6 +124,9 @@ markdown_extensions:
|
||||
- name: textual
|
||||
class: textual
|
||||
format: !!python/name:textual._doc.format_svg
|
||||
- name: rich
|
||||
class: rich
|
||||
format: !!python/name:textual._doc.rich
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.superfences
|
||||
- pymdownx.snippets
|
||||
|
||||
28
sandbox/will/box.css
Normal file
28
sandbox/will/box.css
Normal 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
13
sandbox/will/box.py
Normal 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()
|
||||
@@ -50,3 +50,32 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str
|
||||
import traceback
|
||||
|
||||
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
|
||||
|
||||
@@ -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):
|
||||
"""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):
|
||||
"""
|
||||
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)
|
||||
self.suggested_color = suggested_color
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
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
|
||||
"""Red component (0-255)"""
|
||||
@@ -310,7 +330,29 @@ class Color(NamedTuple):
|
||||
@classmethod
|
||||
@lru_cache(maxsize=1024 * 4)
|
||||
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:
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
"""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
|
||||
|
||||
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"))
|
||||
|
||||
@@ -28,7 +28,7 @@ from ._help_text import (
|
||||
color_property_help_text,
|
||||
)
|
||||
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 .constants import NULL_SPACING, VALID_STYLE_FLAGS
|
||||
from .errors import StyleTypeError, StyleValueError
|
||||
@@ -452,27 +452,6 @@ class BorderProperty:
|
||||
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:
|
||||
"""Descriptor for getting and setting spacing properties (e.g. padding and margin)."""
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import pytest
|
||||
from rich.color import Color as RichColor
|
||||
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():
|
||||
@@ -31,22 +31,6 @@ def test_css():
|
||||
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():
|
||||
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_.g == pytest.approx(g, 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"
|
||||
|
||||
Reference in New Issue
Block a user