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
|
## 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
|
||||||
|
|||||||
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
|
- 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
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
|
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
|
||||||
|
|||||||
@@ -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"))
|
|
||||||
|
|||||||
@@ -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)."""
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user