From 2f96a73f4f3845607af99686d70d705c660c2d91 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 5 Jun 2024 11:46:51 +0100 Subject: [PATCH 01/11] hatch css --- examples/five_by_five.tcss | 1 + src/textual/_segment_tools.py | 34 +++++++++++++++++++ src/textual/_styles_cache.py | 10 +++++- src/textual/color.py | 2 ++ src/textual/css/_style_properties.py | 8 +++++ src/textual/css/_styles_builder.py | 50 +++++++++++++++++++++++++++- src/textual/css/constants.py | 1 + src/textual/css/styles.py | 8 +++++ 8 files changed, 112 insertions(+), 2 deletions(-) diff --git a/examples/five_by_five.tcss b/examples/five_by_five.tcss index 5f435ecdd1..58168c48f9 100644 --- a/examples/five_by_five.tcss +++ b/examples/five_by_five.tcss @@ -44,6 +44,7 @@ GameCell { background: $surface; border: round $surface-darken-1; transition: background $animation-speed $animation-type, color $animation-speed $animation-type; + hatch: right $primary 50%; } GameCell:hover { diff --git a/src/textual/_segment_tools.py b/src/textual/_segment_tools.py index 219c6dcadf..13a9441564 100644 --- a/src/textual/_segment_tools.py +++ b/src/textual/_segment_tools.py @@ -4,6 +4,7 @@ from __future__ import annotations +import re from typing import Iterable from rich.segment import Segment @@ -258,3 +259,36 @@ def blank_lines(count: int) -> list[list[Segment]]: if bottom_blank_lines: yield from blank_lines(bottom_blank_lines) + + +_re_spaces = re.compile(r"(\s+|\S+)") + + +def apply_hatch( + segments: Iterable[Segment], + character: str, + hash_style: Style, + _split=_re_spaces.split, +) -> Iterable[Segment]: + """Replace run of spaces with another character + style. + + Args: + segments: Segments to process. + character: Character to replace spaces. + hatch_style: Style of replacement characters. + + Yields: + Segments. + """ + _Segment = Segment + for segment in segments: + if " " not in segment.text: + yield segment + else: + text, style, _ = segment + for token in _split(text): + if token: + if token.isspace(): + yield _Segment(character * len(token), hash_style) + else: + yield _Segment(token, style) diff --git a/src/textual/_styles_cache.py b/src/textual/_styles_cache.py index 674b2a52bf..3dd30218e7 100644 --- a/src/textual/_styles_cache.py +++ b/src/textual/_styles_cache.py @@ -14,7 +14,7 @@ from ._border import get_box, render_border_label, render_row from ._context import active_app from ._opacity import _apply_opacity -from ._segment_tools import line_pad, line_trim +from ._segment_tools import apply_hatch, line_pad, line_trim from .color import Color from .constants import DEBUG from .filter import LineFilter @@ -320,12 +320,20 @@ def post(segments: Iterable[Segment]) -> Iterable[Segment]: Returns: New list of segments """ + try: app = active_app.get() ansi_theme = app.ansi_theme except LookupError: ansi_theme = DEFAULT_TERMINAL_THEME + if styles.has_rule("hatch"): + character, color = styles.hatch + if character != " " and color.a > 0: + hatch_style = Style.from_color( + (background + color).rich_color, background.rich_color + ) + segments = list(apply_hatch(segments, character, hatch_style)) if styles.tint.a: segments = Tint.process_segments(segments, styles.tint, ansi_theme) if opacity != 1.0: diff --git a/src/textual/color.py b/src/textual/color.py index ab5996dda3..449843a2c2 100644 --- a/src/textual/color.py +++ b/src/textual/color.py @@ -598,6 +598,8 @@ def get_color(self, position: float) -> Color: """A constant for pure white.""" BLACK: Final = Color(0, 0, 0) """A constant for pure black.""" +TRANSPARENT: Final = Color.parse("transparent") +"""A constant for transparent.""" def rgb_to_lab(rgb: Color) -> Lab: diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index c66068d280..7fc351ce9f 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -1139,3 +1139,11 @@ def __set__( horizontal, vertical = value setattr(obj, self.horizontal, horizontal) setattr(obj, self.vertical, vertical) + + +class HatchProperty: + def __get__(self, obj: StylesBase, type: type[StylesBase]) -> tuple[str, Color]: + return cast("tuple[str, Color]", obj.get_rule("hatch")) + + def __set__(self, obj: StylesBase, value: tuple[str, Color]) -> None: + obj.set_rule("hatch", value) diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index 8b66fec39b..ef89ca9f52 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -8,7 +8,7 @@ from .._border import BorderValue, normalize_border_value from .._duration import _duration_as_seconds from .._easing import EASING -from ..color import Color, ColorParseError +from ..color import TRANSPARENT, Color, ColorParseError from ..geometry import Spacing, SpacingDimensions, clamp from ..suggestions import get_suggestion from ._error_tools import friendly_list @@ -43,6 +43,7 @@ VALID_CONSTRAIN, VALID_DISPLAY, VALID_EDGE, + VALID_HATCH, VALID_KEYLINE, VALID_OVERFLOW, VALID_OVERLAY, @@ -1054,6 +1055,53 @@ def process_constrain(self, name: str, tokens: list[Token]) -> None: else: self.styles._rules[name] = value # type: ignore + def process_hatch(self, name: str, tokens: list[Token]) -> None: + character = " " + color = TRANSPARENT + opacity = 1.0 + HATCHES = { + "left": "╲", + "right": "╱", + "cross": "╳", + "horizontal": "─", + "vertical": "│", + } + for token in tokens: + if token.name == "token": + if token.value not in VALID_HATCH: + self.error( + name, + tokens[0], + string_enum_help_text(name, VALID_HATCH, context="css"), + ) + character = HATCHES[token.value] + elif token.name == "string": + character = token.value[1:-1] + if len(character) != 1: + self.error( + name, + token, + f"Hatch requires a string of length 1; got {token.value!r}", + ) + elif token.name == "color": + try: + color = Color.parse(token.value) + except Exception as error: + self.error( + name, + token, + color_property_help_text(name, context="css", error=error), + ) + elif token.name == "scalar": + opacity_scalar = opacity = Scalar.parse(token.value) + if opacity_scalar.unit != Unit.PERCENT: + self.error( + name, token, "hatch alpha must be given as a percentage." + ) + opacity = clamp(opacity_scalar.value / 100.0, 0, 1.0) + + self.styles._rules[name] = (character, color.multiply_alpha(opacity)) + def _get_suggested_property_name_for_rule(self, rule_name: str) -> str | None: """ Returns a valid CSS property "Python" name, or None if no close matches could be found. diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py index 27c662dde9..083fb35207 100644 --- a/src/textual/css/constants.py +++ b/src/textual/css/constants.py @@ -74,3 +74,4 @@ VALID_OVERLAY: Final = {"none", "screen"} VALID_CONSTRAIN: Final = {"x", "y", "both", "inflect", "none"} VALID_KEYLINE: Final = {"none", "thin", "heavy", "double"} +VALID_HATCH: Final = {"left", "right", "cross", "vertical", "horizontal"} diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 733c84ba23..db37c070d7 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -22,6 +22,7 @@ ColorProperty, DockProperty, FractionalProperty, + HatchProperty, IntegerProperty, KeylineProperty, LayoutProperty, @@ -186,6 +187,8 @@ class RulesMap(TypedDict, total=False): border_subtitle_background: Color border_subtitle_style: Style + hatch: tuple[str, Color] + overlay: Overlay constrain: Constrain @@ -355,6 +358,8 @@ class StylesBase(ABC): border_subtitle_background = ColorProperty(Color(0, 0, 0, 0)) border_subtitle_style = StyleFlagsProperty() + hatch = HatchProperty() + overlay = StringEnumProperty( VALID_OVERLAY, "none", layout=True, refresh_parent=True ) @@ -1076,6 +1081,9 @@ def append_declaration(name: str, value: str) -> None: keyline_type, keyline_color = self.keyline if keyline_type != "none": append_declaration("keyline", f"{keyline_type}, {keyline_color.css}") + if "hatch" in rules: + hatch_character, hatch_color = self.hatch + append_declaration("hatch", f'"{hatch_character}" {hatch_color.css}') lines.sort() return lines From b180349629d0ca0142882aceb79acbc2f77e253e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 5 Jun 2024 11:48:53 +0100 Subject: [PATCH 02/11] docstring --- src/textual/css/_style_properties.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 7fc351ce9f..7e729a33d5 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -1142,6 +1142,8 @@ def __set__( class HatchProperty: + """Property to expose hatch style.""" + def __get__(self, obj: StylesBase, type: type[StylesBase]) -> tuple[str, Color]: return cast("tuple[str, Color]", obj.get_rule("hatch")) From 9466d3f4a587622214f53594dd5d3e5c4bcb5214 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 5 Jun 2024 11:55:24 +0100 Subject: [PATCH 03/11] more validation --- src/textual/css/_styles_builder.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index ef89ca9f52..b266cde0dc 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -6,6 +6,7 @@ import rich.repr from .._border import BorderValue, normalize_border_value +from .._cells import cell_len from .._duration import _duration_as_seconds from .._easing import EASING from ..color import TRANSPARENT, Color, ColorParseError @@ -1081,7 +1082,13 @@ def process_hatch(self, name: str, tokens: list[Token]) -> None: self.error( name, token, - f"Hatch requires a string of length 1; got {token.value!r}", + f"Hatch requires a string of length 1; got {token.value}", + ) + if cell_len(character) != 1: + self.error( + name, + token, + f"Hatch requires a string with a *cell length* of 1; got {token.value}", ) elif token.name == "color": try: From c74196f058dd332930cfe1fe0cbb8a720a2ed9e2 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 5 Jun 2024 12:01:05 +0100 Subject: [PATCH 04/11] fix 5x5 --- examples/five_by_five.tcss | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/five_by_five.tcss b/examples/five_by_five.tcss index 58168c48f9..5f435ecdd1 100644 --- a/examples/five_by_five.tcss +++ b/examples/five_by_five.tcss @@ -44,7 +44,6 @@ GameCell { background: $surface; border: round $surface-darken-1; transition: background $animation-speed $animation-type, color $animation-speed $animation-type; - hatch: right $primary 50%; } GameCell:hover { From 902eca180ee49e2b08c634efc152e4bea09aa3b2 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 5 Jun 2024 13:58:25 +0100 Subject: [PATCH 05/11] only hatch inside borders --- src/textual/_styles_cache.py | 20 +++++--- src/textual/css/_style_properties.py | 4 +- tests/snapshot_tests/snapshot_apps/hatch.py | 56 +++++++++++++++++++++ 3 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 tests/snapshot_tests/snapshot_apps/hatch.py diff --git a/src/textual/_styles_cache.py b/src/textual/_styles_cache.py index 3dd30218e7..78a87584b4 100644 --- a/src/textual/_styles_cache.py +++ b/src/textual/_styles_cache.py @@ -311,6 +311,16 @@ def render_line( inner = from_color(bgcolor=(base_background + background).rich_color) outer = from_color(bgcolor=base_background.rich_color) + def line_post(segments: Iterable[Segment]) -> Iterable[Segment]: + if styles.has_rule("hatch"): + character, color = styles.hatch + if character != " " and color.a > 0: + hatch_style = Style.from_color( + (background + color).rich_color, background.rich_color + ) + segments = apply_hatch(segments, character, hatch_style) + return segments + def post(segments: Iterable[Segment]) -> Iterable[Segment]: """Post process segments to apply opacity and tint. @@ -327,13 +337,6 @@ def post(segments: Iterable[Segment]) -> Iterable[Segment]: except LookupError: ansi_theme = DEFAULT_TERMINAL_THEME - if styles.has_rule("hatch"): - character, color = styles.hatch - if character != " " and color.a > 0: - hatch_style = Style.from_color( - (background + color).rich_color, background.rich_color - ) - segments = list(apply_hatch(segments, character, hatch_style)) if styles.tint.a: segments = Tint.process_segments(segments, styles.tint, ansi_theme) if opacity != 1.0: @@ -429,6 +432,7 @@ def post(segments: Iterable[Segment]) -> Iterable[Segment]: line = [make_blank(width - 1, background_style), right] else: line = [make_blank(width, background_style)] + line = line_post(line) else: # Content with border and padding (C) content_y = y - gutter.top @@ -441,7 +445,7 @@ def post(segments: Iterable[Segment]) -> Iterable[Segment]: line = Segment.apply_style(line, inner) if styles.text_opacity != 1.0: line = TextOpacity.process_segments(line, styles.text_opacity) - line = line_pad(line, pad_left, pad_right, inner) + line = line_post(line_pad(line, pad_left, pad_right, inner)) if border_left or border_right: # Add left / right border diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 7e729a33d5..95745b165e 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -17,7 +17,7 @@ from typing_extensions import TypeAlias from .._border import normalize_border_value -from ..color import Color, ColorParseError +from ..color import TRANSPARENT, Color, ColorParseError from ..geometry import NULL_SPACING, Spacing, SpacingDimensions, clamp from ._error_tools import friendly_list from ._help_text import ( @@ -1145,7 +1145,7 @@ class HatchProperty: """Property to expose hatch style.""" def __get__(self, obj: StylesBase, type: type[StylesBase]) -> tuple[str, Color]: - return cast("tuple[str, Color]", obj.get_rule("hatch")) + return cast("tuple[str, Color]", obj.get_rule("hatch", (" ", TRANSPARENT))) def __set__(self, obj: StylesBase, value: tuple[str, Color]) -> None: obj.set_rule("hatch", value) diff --git a/tests/snapshot_tests/snapshot_apps/hatch.py b/tests/snapshot_tests/snapshot_apps/hatch.py new file mode 100644 index 0000000000..1f9daac92a --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/hatch.py @@ -0,0 +1,56 @@ +from textual.app import App, ComposeResult +from textual.widgets import Label +from textual.containers import Container + + +class HatchApp(App): + CSS = """ + Screen { + hatch: right $primary; + } + #one { + hatch: left $success; + margin: 2 4; + } + + #two { + hatch: cross $warning; + margin: 2 4; + } + + #three { + hatch: horizontal $error; + margin: 2 4; + } + + #four { + hatch: vertical $primary; + margin: 2 4; + padding: 2 4; + align: center middle; + border: solid red; + } + + #five { + hatch: "┼" $success 50%; + margin: 2 4; + align: center middle; + text-style: bold; + color: magenta; + } + """ + + def compose(self) -> ComposeResult: + with Container(id="one"): + with Container(id="two"): + with Container(id="three"): + with Container(id="four"): + with Container(id="five"): + yield Label("Hatched") + + def on_mount(self) -> None: + self.query_one("#four").border_title = "Hello World" + + +if __name__ == "__main__": + HatchApp().run() From f4dcfc23a6989e64c764b6bc76af5b7e2bdc1ad7 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 5 Jun 2024 14:02:03 +0100 Subject: [PATCH 06/11] hatch snapshot --- src/textual/_styles_cache.py | 3 ++- tests/snapshot_tests/snapshot_apps/hatch.py | 2 +- tests/snapshot_tests/test_snapshots.py | 5 +++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/textual/_styles_cache.py b/src/textual/_styles_cache.py index 78a87584b4..7029b6b4f4 100644 --- a/src/textual/_styles_cache.py +++ b/src/textual/_styles_cache.py @@ -312,13 +312,14 @@ def render_line( outer = from_color(bgcolor=base_background.rich_color) def line_post(segments: Iterable[Segment]) -> Iterable[Segment]: + """Apply effects to segments inside the border.""" if styles.has_rule("hatch"): character, color = styles.hatch if character != " " and color.a > 0: hatch_style = Style.from_color( (background + color).rich_color, background.rich_color ) - segments = apply_hatch(segments, character, hatch_style) + return apply_hatch(segments, character, hatch_style) return segments def post(segments: Iterable[Segment]) -> Iterable[Segment]: diff --git a/tests/snapshot_tests/snapshot_apps/hatch.py b/tests/snapshot_tests/snapshot_apps/hatch.py index 1f9daac92a..7ec5aa7588 100644 --- a/tests/snapshot_tests/snapshot_apps/hatch.py +++ b/tests/snapshot_tests/snapshot_apps/hatch.py @@ -26,7 +26,7 @@ class HatchApp(App): #four { hatch: vertical $primary; margin: 2 4; - padding: 2 4; + padding: 1 2; align: center middle; border: solid red; } diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 78c1a70294..1e86134902 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -1278,3 +1278,8 @@ def test_data_table_in_tabs(snap_compare): def test_auto_tab_active(snap_compare): # https://github.com/Textualize/textual/issues/4593 assert snap_compare(SNAPSHOT_APPS_DIR / "auto_tab_active.py", press=["space"]) + + +def test_hatch(snap_compare): + """Test hatch styles.""" + assert snap_compare(SNAPSHOT_APPS_DIR / "hatch.py") From b38b170d998722371edf43b7da1998799930b779 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 5 Jun 2024 15:11:14 +0100 Subject: [PATCH 07/11] hatch docs --- docs/css_types/hatch.md | 40 +++++++++++++++++++ docs/examples/styles/hatch.py | 22 +++++++++++ docs/examples/styles/hatch.tcss | 19 +++++++++ docs/styles/hatch.md | 58 ++++++++++++++++++++++++++++ mkdocs-nav.yml | 2 + src/textual/css/_style_properties.py | 17 ++++++-- src/textual/css/_styles_builder.py | 9 +---- src/textual/css/constants.py | 7 ++++ 8 files changed, 164 insertions(+), 10 deletions(-) create mode 100644 docs/css_types/hatch.md create mode 100644 docs/examples/styles/hatch.py create mode 100644 docs/examples/styles/hatch.tcss create mode 100644 docs/styles/hatch.md diff --git a/docs/css_types/hatch.md b/docs/css_types/hatch.md new file mode 100644 index 0000000000..0e41a50650 --- /dev/null +++ b/docs/css_types/hatch.md @@ -0,0 +1,40 @@ +# <hatch> + +The `` CSS type represents a character used in the [hatch](../styles/hatch.md) rule. + +## Syntax + +| Value | Description | +| ------------ | ------------------------------ | +| `cross` | A diagonal crossed line. | +| `horizontal` | A horizontal line. | +| `left` | A left leaning diagonal line. | +| `right` | A right leaning diagonal line. | +| `vertical` | A vertical line. | + + + + +## Examples + +### CSS + + + +```css +.some-class { + hatch: cross green; +} +``` + +### Python + + + +```py +widget.styles.hatch = ("cross", "red") +``` diff --git a/docs/examples/styles/hatch.py b/docs/examples/styles/hatch.py new file mode 100644 index 0000000000..bbdce366ff --- /dev/null +++ b/docs/examples/styles/hatch.py @@ -0,0 +1,22 @@ +from textual.app import App, ComposeResult +from textual.containers import Horizontal, Vertical +from textual.widgets import Static + +HATCHES = ("cross", "horizontal", "custom", "left", "right") + + +class HatchApp(App): + CSS_PATH = "hatch.tcss" + + def compose(self) -> ComposeResult: + with Horizontal(): + for hatch in HATCHES: + static = Static(classes=f"hatch {hatch}") + static.border_title = hatch + with Vertical(): + yield static + + +if __name__ == "__main__": + app = HatchApp() + app.run() diff --git a/docs/examples/styles/hatch.tcss b/docs/examples/styles/hatch.tcss new file mode 100644 index 0000000000..989d35efaa --- /dev/null +++ b/docs/examples/styles/hatch.tcss @@ -0,0 +1,19 @@ +.hatch { + height: 1fr; + border: solid $secondary; + &.cross { + hatch: cross $success; + } + &.horizontal { + hatch: horizontal $success 80%; + } + &.custom { + hatch: "T" $success 60%; + } + &.left { + hatch: left $success 40%; + } + &.right { + hatch: right $success 20%; + } +} diff --git a/docs/styles/hatch.md b/docs/styles/hatch.md new file mode 100644 index 0000000000..41cac23edb --- /dev/null +++ b/docs/styles/hatch.md @@ -0,0 +1,58 @@ +# Hatch + +The `hatch` style fills a widget's background with a repeating character for a pleasing textured effect. + +## Syntax + +--8<-- "docs/snippets/syntax_block_start.md" +hatch: (<hatch> | CHARACTER) <color> [<percentage>] +--8<-- "docs/snippets/syntax_block_end.md" + +The hatch type can be specified with a constant, or a string. For example, `cross` for cross hatch, or `"T"` for a custom character. + +The color can be any Textual color value. + +An optional percentage can be used to set the opacity. + +## Examples + + +An app to show a few hatch effects. + +=== "Output" + + ```{.textual path="docs/examples/styles/hatch.py"} + ``` + +=== "hatch.py" + + ```py + --8<-- "docs/examples/styles/hatch.py" + ``` + +=== "hatch.tcss" + + ```css + --8<-- "docs/examples/styles/hatch.tcss" + ``` + + +## CSS + +```css +/* Red cross hatch */ +hatch: cross red; +/* Right diagonals, 50% transparent green. */ +hatch: right green 50%; +/* T custom character in 80% blue. **/ +hatch: "T" blue 80%; +``` + + +## Python + +```py +widget.styles.hatch = ("cross", "red") +widget.styles.hatch = ("right", "rgba(0,255,0,128)") +widget.styles.hatch = ("T", "blue") +``` diff --git a/mkdocs-nav.yml b/mkdocs-nav.yml index 9c1e4c0aa4..169f3ef0d0 100644 --- a/mkdocs-nav.yml +++ b/mkdocs-nav.yml @@ -30,6 +30,7 @@ nav: - "css_types/index.md" - "css_types/border.md" - "css_types/color.md" + - "css_types/hatch.md" - "css_types/horizontal.md" - "css_types/integer.md" - "css_types/keyline.md" @@ -96,6 +97,7 @@ nav: - "styles/grid/grid_rows.md" - "styles/grid/grid_size.md" - "styles/grid/row_span.md" + - "styles/hatch.md" - "styles/height.md" - "styles/keyline.md" - "styles/layer.md" diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 95745b165e..06db0b26cb 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -31,7 +31,7 @@ string_enum_help_text, style_flags_property_help_text, ) -from .constants import VALID_STYLE_FLAGS +from .constants import HATCHES, VALID_STYLE_FLAGS from .errors import StyleTypeError, StyleValueError from .scalar import ( NULL_SCALAR, @@ -1147,5 +1147,16 @@ class HatchProperty: def __get__(self, obj: StylesBase, type: type[StylesBase]) -> tuple[str, Color]: return cast("tuple[str, Color]", obj.get_rule("hatch", (" ", TRANSPARENT))) - def __set__(self, obj: StylesBase, value: tuple[str, Color]) -> None: - obj.set_rule("hatch", value) + def __set__(self, obj: StylesBase, value: tuple[str, Color | str]) -> None: + character, color = value + if len(character) != 1: + try: + character = HATCHES[character] + except KeyError: + raise ValueError( + f"Expected a character or hatch value here; found {character!r}" + ) from None + if isinstance(color, str): + color = Color.parse(color) + hatch = (character, color) + obj.set_rule("hatch", hatch) diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index b266cde0dc..760024cd52 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -37,6 +37,7 @@ text_align_help_text, ) from .constants import ( + HATCHES, VALID_ALIGN_HORIZONTAL, VALID_ALIGN_VERTICAL, VALID_BORDER, @@ -1060,13 +1061,7 @@ def process_hatch(self, name: str, tokens: list[Token]) -> None: character = " " color = TRANSPARENT opacity = 1.0 - HATCHES = { - "left": "╲", - "right": "╱", - "cross": "╳", - "horizontal": "─", - "vertical": "│", - } + for token in tokens: if token.name == "token": if token.value not in VALID_HATCH: diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py index 083fb35207..07e2523989 100644 --- a/src/textual/css/constants.py +++ b/src/textual/css/constants.py @@ -75,3 +75,10 @@ VALID_CONSTRAIN: Final = {"x", "y", "both", "inflect", "none"} VALID_KEYLINE: Final = {"none", "thin", "heavy", "double"} VALID_HATCH: Final = {"left", "right", "cross", "vertical", "horizontal"} +HATCHES: Final = { + "left": "╲", + "right": "╱", + "cross": "╳", + "horizontal": "─", + "vertical": "│", +} From d9da345fcfcef22057721c1f9f58297e8727e4e4 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 5 Jun 2024 15:16:56 +0100 Subject: [PATCH 08/11] docs --- CHANGELOG.md | 4 ++++ docs/css_types/hatch.md | 9 --------- src/textual/css/_style_properties.py | 3 +++ tests/snapshot_tests/snapshot_apps/hatch.py | 4 ++-- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75f87b8994..3107f0dbe4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - TabbedContent will automatically make tabs active when a widget in a pane is focused https://github.com/Textualize/textual/issues/4593 +### Added + +- Added hatch style https://github.com/Textualize/textual/pull/4603 + ## [0.64.0] - 2024-06-03 ### Fixed diff --git a/docs/css_types/hatch.md b/docs/css_types/hatch.md index 0e41a50650..b489bd3d3c 100644 --- a/docs/css_types/hatch.md +++ b/docs/css_types/hatch.md @@ -13,17 +13,10 @@ The `` CSS type represents a character used in the [hatch](../styles/hatc | `vertical` | A vertical line. | - - ## Examples ### CSS - ```css .some-class { @@ -33,8 +26,6 @@ Add comments when needed/if helpful. ### Python - - ```py widget.styles.hatch = ("cross", "red") ``` diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 06db0b26cb..c21c3b9e14 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -17,6 +17,7 @@ from typing_extensions import TypeAlias from .._border import normalize_border_value +from .._cells import cell_len from ..color import TRANSPARENT, Color, ColorParseError from ..geometry import NULL_SPACING, Spacing, SpacingDimensions, clamp from ._error_tools import friendly_list @@ -1156,6 +1157,8 @@ def __set__(self, obj: StylesBase, value: tuple[str, Color | str]) -> None: raise ValueError( f"Expected a character or hatch value here; found {character!r}" ) from None + if cell_len(character) != 1: + raise ValueError("Hatch character must have a cell length of 1") if isinstance(color, str): color = Color.parse(color) hatch = (character, color) diff --git a/tests/snapshot_tests/snapshot_apps/hatch.py b/tests/snapshot_tests/snapshot_apps/hatch.py index 7ec5aa7588..12c1c3a7b2 100644 --- a/tests/snapshot_tests/snapshot_apps/hatch.py +++ b/tests/snapshot_tests/snapshot_apps/hatch.py @@ -25,7 +25,7 @@ class HatchApp(App): #four { hatch: vertical $primary; - margin: 2 4; + margin: 1 2; padding: 1 2; align: center middle; border: solid red; @@ -33,7 +33,7 @@ class HatchApp(App): #five { hatch: "┼" $success 50%; - margin: 2 4; + margin: 1 2; align: center middle; text-style: bold; color: magenta; From f824bb45a488a84ff766447e1c3045b0df5758f5 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 5 Jun 2024 15:21:04 +0100 Subject: [PATCH 09/11] snapshot --- .../__snapshots__/test_snapshots.ambr | 322 ++++++++++++++++++ 1 file changed, 322 insertions(+) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 228303dd56..9499dc7aab 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -8972,6 +8972,167 @@ ''' # --- +# name: test_css_property[hatch.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + HatchApp + + + + + + + + + +  cross ────── horizontal  custom ───── left ─────── right ────── + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳──────────────TTTTTTTTTTTTTT╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ────────────────────────────────────────────────────────────────────── + + + + + ''' +# --- # name: test_css_property[height.py] ''' @@ -22098,6 +22259,167 @@ ''' # --- +# name: test_hatch + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + HatchApp + + + + + + + + + + ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╱╱╱╱╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱ + ╱╱╱╱╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱ + ╱╱╱╱╲╲╲╲╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╲╲╲╲╱╱╱╱ + ╱╱╱╱╲╲╲╲╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╲╲╲╲╱╱╱╱ + ╱╱╱╱╲╲╲╲╳╳╳╳────────────────────────────────────────────────────────╳╳╳╳╲╲╲╲╱╱╱╱ + ╱╱╱╱╲╲╲╲╳╳╳╳── Hello World ──────────────────────────────────────╳╳╳╳╲╲╲╲╱╱╱╱ + ╱╱╱╱╲╲╲╲╳╳╳╳──││││││││││││││││││││││││││││││││││││││││││││││││││──╳╳╳╳╲╲╲╲╱╱╱╱ + ╱╱╱╱╲╲╲╲╳╳╳╳──││││││││││││││││││││││││││││││││││││││││││││││││││──╳╳╳╳╲╲╲╲╱╱╱╱ + ╱╱╱╱╲╲╲╲╳╳╳╳──││││┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼││││──╳╳╳╳╲╲╲╲╱╱╱╱ + ╱╱╱╱╲╲╲╲╳╳╳╳──││││┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼Hatched┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼││││──╳╳╳╳╲╲╲╲╱╱╱╱ + ╱╱╱╱╲╲╲╲╳╳╳╳──││││┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼││││──╳╳╳╳╲╲╲╲╱╱╱╱ + ╱╱╱╱╲╲╲╲╳╳╳╳──││││┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼││││──╳╳╳╳╲╲╲╲╱╱╱╱ + ╱╱╱╱╲╲╲╲╳╳╳╳──││││││││││││││││││││││││││││││││││││││││││││││││││──╳╳╳╳╲╲╲╲╱╱╱╱ + ╱╱╱╱╲╲╲╲╳╳╳╳──││││││││││││││││││││││││││││││││││││││││││││││││││──╳╳╳╳╲╲╲╲╱╱╱╱ + ╱╱╱╱╲╲╲╲╳╳╳╳──────────────────────────────────────────────────────╳╳╳╳╲╲╲╲╱╱╱╱ + ╱╱╱╱╲╲╲╲╳╳╳╳────────────────────────────────────────────────────────╳╳╳╳╲╲╲╲╱╱╱╱ + ╱╱╱╱╲╲╲╲╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╲╲╲╲╱╱╱╱ + ╱╱╱╱╲╲╲╲╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╲╲╲╲╱╱╱╱ + ╱╱╱╱╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱ + ╱╱╱╱╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╲╱╱╱╱ + ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + + + + + ''' +# --- # name: test_header_render ''' From c02989aa209e53268f690c898e536603c78a7b79 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 5 Jun 2024 15:32:07 +0100 Subject: [PATCH 10/11] fix typo --- src/textual/_segment_tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/_segment_tools.py b/src/textual/_segment_tools.py index 13a9441564..5db0f4d70e 100644 --- a/src/textual/_segment_tools.py +++ b/src/textual/_segment_tools.py @@ -267,7 +267,7 @@ def blank_lines(count: int) -> list[list[Segment]]: def apply_hatch( segments: Iterable[Segment], character: str, - hash_style: Style, + hatch_style: Style, _split=_re_spaces.split, ) -> Iterable[Segment]: """Replace run of spaces with another character + style. @@ -289,6 +289,6 @@ def apply_hatch( for token in _split(text): if token: if token.isspace(): - yield _Segment(character * len(token), hash_style) + yield _Segment(character * len(token), hatch_style) else: yield _Segment(token, style) From 91dd066bc506902f1a4f82c00c954f8b79c8476e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 5 Jun 2024 15:38:42 +0100 Subject: [PATCH 11/11] bump --- CHANGELOG.md | 3 ++- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3107f0dbe4..fd6b8adfd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## Unreleased +## [0.65.0] - 2024-06-05 ### Fixed @@ -2061,6 +2061,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040 - New handler system for messages that doesn't require inheritance - Improved traceback handling +[0.65.0]: https://github.com/Textualize/textual/compare/v0.64.0...v0.65.0 [0.64.0]: https://github.com/Textualize/textual/compare/v0.63.6...v0.64.0 [0.63.6]: https://github.com/Textualize/textual/compare/v0.63.5...v0.63.6 [0.63.5]: https://github.com/Textualize/textual/compare/v0.63.4...v0.63.5 diff --git a/pyproject.toml b/pyproject.toml index c6506a5b63..6f66f4609d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "0.64.0" +version = "0.65.0" homepage = "https://github.com/Textualize/textual" repository = "https://github.com/Textualize/textual" documentation = "https://textual.textualize.io/"