diff --git a/docs/guide/styles.md b/docs/guide/styles.md
index 7346ced7f..2c24eb9b0 100644
--- a/docs/guide/styles.md
+++ b/docs/guide/styles.md
@@ -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
diff --git a/docs/images/styles/border_box.excalidraw.svg b/docs/images/styles/border_box.excalidraw.svg
new file mode 100644
index 000000000..0288da325
--- /dev/null
+++ b/docs/images/styles/border_box.excalidraw.svg
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/docs/images/styles/box.excalidraw.svg b/docs/images/styles/box.excalidraw.svg
index 67a960567..462f21bf5 100644
--- a/docs/images/styles/box.excalidraw.svg
+++ b/docs/images/styles/box.excalidraw.svg
@@ -1,6 +1,6 @@
-
\ No newline at end of file
diff --git a/mkdocs.yml b/mkdocs.yml
index c13b73b6b..e15f9e5d9 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -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
diff --git a/sandbox/will/box.css b/sandbox/will/box.css
new file mode 100644
index 000000000..fcc4c3d4a
--- /dev/null
+++ b/sandbox/will/box.css
@@ -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;
+}
diff --git a/sandbox/will/box.py b/sandbox/will/box.py
new file mode 100644
index 000000000..ab99d4ee6
--- /dev/null
+++ b/sandbox/will/box.py
@@ -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()
diff --git a/src/textual/_doc.py b/src/textual/_doc.py
index febe3e576..b70ce67fc 100644
--- a/src/textual/_doc.py
+++ b/src/textual/_doc.py
@@ -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
diff --git a/src/textual/color.py b/src/textual/color.py
index fd420e17c..edb3c076b 100644
--- a/src/textual/color.py
+++ b/src/textual/color.py
@@ -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"))
diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py
index 175504a85..28fc34db8 100644
--- a/src/textual/css/_style_properties.py
+++ b/src/textual/css/_style_properties.py
@@ -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)."""
diff --git a/tests/test_color.py b/tests/test_color.py
index 0e1e51ebb..0969b5678 100644
--- a/tests/test_color.py
+++ b/tests/test_color.py
@@ -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"