Skip to content

Commit 57594db

Browse files
authored
Merge pull request #4603 from Textualize/hatch
hatch css
2 parents 7b0251e + 91dd066 commit 57594db

File tree

17 files changed

+666
-7
lines changed

17 files changed

+666
-7
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8-
## Unreleased
8+
## [0.65.0] - 2024-06-05
99

1010
### Added
1111

@@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1919

2020
- TabbedContent will automatically make tabs active when a widget in a pane is focused https://github.com/Textualize/textual/issues/4593
2121

22+
### Added
23+
24+
- Added hatch style https://github.com/Textualize/textual/pull/4603
25+
2226
## [0.64.0] - 2024-06-03
2327

2428
### Fixed
@@ -2061,6 +2065,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040
20612065
- New handler system for messages that doesn't require inheritance
20622066
- Improved traceback handling
20632067

2068+
[0.65.0]: https://github.com/Textualize/textual/compare/v0.64.0...v0.65.0
20642069
[0.64.0]: https://github.com/Textualize/textual/compare/v0.63.6...v0.64.0
20652070
[0.63.6]: https://github.com/Textualize/textual/compare/v0.63.5...v0.63.6
20662071
[0.63.5]: https://github.com/Textualize/textual/compare/v0.63.4...v0.63.5

docs/css_types/hatch.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# <hatch>
2+
3+
The `<hatch>` CSS type represents a character used in the [hatch](../styles/hatch.md) rule.
4+
5+
## Syntax
6+
7+
| Value | Description |
8+
| ------------ | ------------------------------ |
9+
| `cross` | A diagonal crossed line. |
10+
| `horizontal` | A horizontal line. |
11+
| `left` | A left leaning diagonal line. |
12+
| `right` | A right leaning diagonal line. |
13+
| `vertical` | A vertical line. |
14+
15+
16+
## Examples
17+
18+
### CSS
19+
20+
21+
```css
22+
.some-class {
23+
hatch: cross green;
24+
}
25+
```
26+
27+
### Python
28+
29+
```py
30+
widget.styles.hatch = ("cross", "red")
31+
```

docs/examples/styles/hatch.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from textual.app import App, ComposeResult
2+
from textual.containers import Horizontal, Vertical
3+
from textual.widgets import Static
4+
5+
HATCHES = ("cross", "horizontal", "custom", "left", "right")
6+
7+
8+
class HatchApp(App):
9+
CSS_PATH = "hatch.tcss"
10+
11+
def compose(self) -> ComposeResult:
12+
with Horizontal():
13+
for hatch in HATCHES:
14+
static = Static(classes=f"hatch {hatch}")
15+
static.border_title = hatch
16+
with Vertical():
17+
yield static
18+
19+
20+
if __name__ == "__main__":
21+
app = HatchApp()
22+
app.run()

docs/examples/styles/hatch.tcss

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.hatch {
2+
height: 1fr;
3+
border: solid $secondary;
4+
&.cross {
5+
hatch: cross $success;
6+
}
7+
&.horizontal {
8+
hatch: horizontal $success 80%;
9+
}
10+
&.custom {
11+
hatch: "T" $success 60%;
12+
}
13+
&.left {
14+
hatch: left $success 40%;
15+
}
16+
&.right {
17+
hatch: right $success 20%;
18+
}
19+
}

docs/styles/hatch.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Hatch
2+
3+
The `hatch` style fills a widget's background with a repeating character for a pleasing textured effect.
4+
5+
## Syntax
6+
7+
--8<-- "docs/snippets/syntax_block_start.md"
8+
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>]
9+
--8<-- "docs/snippets/syntax_block_end.md"
10+
11+
The hatch type can be specified with a constant, or a string. For example, `cross` for cross hatch, or `"T"` for a custom character.
12+
13+
The color can be any Textual color value.
14+
15+
An optional percentage can be used to set the opacity.
16+
17+
## Examples
18+
19+
20+
An app to show a few hatch effects.
21+
22+
=== "Output"
23+
24+
```{.textual path="docs/examples/styles/hatch.py"}
25+
```
26+
27+
=== "hatch.py"
28+
29+
```py
30+
--8<-- "docs/examples/styles/hatch.py"
31+
```
32+
33+
=== "hatch.tcss"
34+
35+
```css
36+
--8<-- "docs/examples/styles/hatch.tcss"
37+
```
38+
39+
40+
## CSS
41+
42+
```css
43+
/* Red cross hatch */
44+
hatch: cross red;
45+
/* Right diagonals, 50% transparent green. */
46+
hatch: right green 50%;
47+
/* T custom character in 80% blue. **/
48+
hatch: "T" blue 80%;
49+
```
50+
51+
52+
## Python
53+
54+
```py
55+
widget.styles.hatch = ("cross", "red")
56+
widget.styles.hatch = ("right", "rgba(0,255,0,128)")
57+
widget.styles.hatch = ("T", "blue")
58+
```

mkdocs-nav.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ nav:
3030
- "css_types/index.md"
3131
- "css_types/border.md"
3232
- "css_types/color.md"
33+
- "css_types/hatch.md"
3334
- "css_types/horizontal.md"
3435
- "css_types/integer.md"
3536
- "css_types/keyline.md"
@@ -96,6 +97,7 @@ nav:
9697
- "styles/grid/grid_rows.md"
9798
- "styles/grid/grid_size.md"
9899
- "styles/grid/row_span.md"
100+
- "styles/hatch.md"
99101
- "styles/height.md"
100102
- "styles/keyline.md"
101103
- "styles/layer.md"

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "textual"
3-
version = "0.64.0"
3+
version = "0.65.0"
44
homepage = "https://github.com/Textualize/textual"
55
repository = "https://github.com/Textualize/textual"
66
documentation = "https://textual.textualize.io/"

src/textual/_segment_tools.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from __future__ import annotations
66

7+
import re
78
from typing import Iterable
89

910
from rich.segment import Segment
@@ -258,3 +259,36 @@ def blank_lines(count: int) -> list[list[Segment]]:
258259

259260
if bottom_blank_lines:
260261
yield from blank_lines(bottom_blank_lines)
262+
263+
264+
_re_spaces = re.compile(r"(\s+|\S+)")
265+
266+
267+
def apply_hatch(
268+
segments: Iterable[Segment],
269+
character: str,
270+
hatch_style: Style,
271+
_split=_re_spaces.split,
272+
) -> Iterable[Segment]:
273+
"""Replace run of spaces with another character + style.
274+
275+
Args:
276+
segments: Segments to process.
277+
character: Character to replace spaces.
278+
hatch_style: Style of replacement characters.
279+
280+
Yields:
281+
Segments.
282+
"""
283+
_Segment = Segment
284+
for segment in segments:
285+
if " " not in segment.text:
286+
yield segment
287+
else:
288+
text, style, _ = segment
289+
for token in _split(text):
290+
if token:
291+
if token.isspace():
292+
yield _Segment(character * len(token), hatch_style)
293+
else:
294+
yield _Segment(token, style)

src/textual/_styles_cache.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from ._border import get_box, render_border_label, render_row
1515
from ._context import active_app
1616
from ._opacity import _apply_opacity
17-
from ._segment_tools import line_pad, line_trim
17+
from ._segment_tools import apply_hatch, line_pad, line_trim
1818
from .color import Color
1919
from .constants import DEBUG
2020
from .filter import LineFilter
@@ -311,6 +311,17 @@ def render_line(
311311
inner = from_color(bgcolor=(base_background + background).rich_color)
312312
outer = from_color(bgcolor=base_background.rich_color)
313313

314+
def line_post(segments: Iterable[Segment]) -> Iterable[Segment]:
315+
"""Apply effects to segments inside the border."""
316+
if styles.has_rule("hatch"):
317+
character, color = styles.hatch
318+
if character != " " and color.a > 0:
319+
hatch_style = Style.from_color(
320+
(background + color).rich_color, background.rich_color
321+
)
322+
return apply_hatch(segments, character, hatch_style)
323+
return segments
324+
314325
def post(segments: Iterable[Segment]) -> Iterable[Segment]:
315326
"""Post process segments to apply opacity and tint.
316327
@@ -320,6 +331,7 @@ def post(segments: Iterable[Segment]) -> Iterable[Segment]:
320331
Returns:
321332
New list of segments
322333
"""
334+
323335
try:
324336
app = active_app.get()
325337
ansi_theme = app.ansi_theme
@@ -421,6 +433,7 @@ def post(segments: Iterable[Segment]) -> Iterable[Segment]:
421433
line = [make_blank(width - 1, background_style), right]
422434
else:
423435
line = [make_blank(width, background_style)]
436+
line = line_post(line)
424437
else:
425438
# Content with border and padding (C)
426439
content_y = y - gutter.top
@@ -433,7 +446,7 @@ def post(segments: Iterable[Segment]) -> Iterable[Segment]:
433446
line = Segment.apply_style(line, inner)
434447
if styles.text_opacity != 1.0:
435448
line = TextOpacity.process_segments(line, styles.text_opacity)
436-
line = line_pad(line, pad_left, pad_right, inner)
449+
line = line_post(line_pad(line, pad_left, pad_right, inner))
437450

438451
if border_left or border_right:
439452
# Add left / right border

src/textual/color.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,8 @@ def get_color(self, position: float) -> Color:
598598
"""A constant for pure white."""
599599
BLACK: Final = Color(0, 0, 0)
600600
"""A constant for pure black."""
601+
TRANSPARENT: Final = Color.parse("transparent")
602+
"""A constant for transparent."""
601603

602604

603605
def rgb_to_lab(rgb: Color) -> Lab:

0 commit comments

Comments
 (0)