Skip to content

Commit

Permalink
Merge pull request #4603 from Textualize/hatch
Browse files Browse the repository at this point in the history
hatch css
  • Loading branch information
willmcgugan authored Jun 5, 2024
2 parents 7b0251e + 91dd066 commit 57594db
Show file tree
Hide file tree
Showing 17 changed files with 666 additions and 7 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

### Added

Expand All @@ -19,6 +19,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
Expand Down Expand Up @@ -2061,6 +2065,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
Expand Down
31 changes: 31 additions & 0 deletions docs/css_types/hatch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# <hatch>

The `<hatch>` 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")
```
22 changes: 22 additions & 0 deletions docs/examples/styles/hatch.py
Original file line number Diff line number Diff line change
@@ -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()
19 changes: 19 additions & 0 deletions docs/examples/styles/hatch.tcss
Original file line number Diff line number Diff line change
@@ -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%;
}
}
58 changes: 58 additions & 0 deletions docs/styles/hatch.md
Original file line number Diff line number Diff line change
@@ -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: (<a href="../../css_types/hatch">&lt;hatch&gt;</a> | CHARACTER) <a href="../../css_types/color">&lt;color&gt;</a> [<a href="../../css_types/percentage">&lt;percentage&gt;</a>]
--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")
```
2 changes: 2 additions & 0 deletions mkdocs-nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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/"
Expand Down
34 changes: 34 additions & 0 deletions src/textual/_segment_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from __future__ import annotations

import re
from typing import Iterable

from rich.segment import Segment
Expand Down Expand Up @@ -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,
hatch_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), hatch_style)
else:
yield _Segment(token, style)
17 changes: 15 additions & 2 deletions src/textual/_styles_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -311,6 +311,17 @@ 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]:
"""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
)
return apply_hatch(segments, character, hatch_style)
return segments

def post(segments: Iterable[Segment]) -> Iterable[Segment]:
"""Post process segments to apply opacity and tint.
Expand All @@ -320,6 +331,7 @@ def post(segments: Iterable[Segment]) -> Iterable[Segment]:
Returns:
New list of segments
"""

try:
app = active_app.get()
ansi_theme = app.ansi_theme
Expand Down Expand Up @@ -421,6 +433,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
Expand All @@ -433,7 +446,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
Expand Down
2 changes: 2 additions & 0 deletions src/textual/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
28 changes: 26 additions & 2 deletions src/textual/css/_style_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
from typing_extensions import TypeAlias

from .._border import normalize_border_value
from ..color import Color, ColorParseError
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
from ._help_text import (
Expand All @@ -31,7 +32,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,
Expand Down Expand Up @@ -1139,3 +1140,26 @@ def __set__(
horizontal, vertical = value
setattr(obj, self.horizontal, horizontal)
setattr(obj, self.vertical, vertical)


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", (" ", TRANSPARENT)))

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 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)
obj.set_rule("hatch", hatch)
Loading

0 comments on commit 57594db

Please sign in to comment.