Skip to content

Commit aa159dd

Browse files
committed
Visual protocol
1 parent 04dbf88 commit aa159dd

File tree

9 files changed

+129
-60
lines changed

9 files changed

+129
-60
lines changed

src/textual/_border.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,9 +271,6 @@ def get_box(
271271
Returns:
272272
A tuple of 3 Segment triplets.
273273
"""
274-
assert isinstance(inner_style, Style)
275-
assert isinstance(outer_style, Style)
276-
assert isinstance(style, Style)
277274
_Segment = Segment
278275
(
279276
(top1, top2, top3),

src/textual/content.py

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from rich.cells import set_cell_size
2020
from rich.console import OverflowMethod
2121
from rich.segment import Segment, Segments
22+
from rich.style import Style as RichStyle
2223
from rich.terminal_theme import TerminalTheme
2324
from rich.text import Text
2425
from typing_extensions import Final, TypeAlias
@@ -27,14 +28,15 @@
2728
from textual._context import active_app
2829
from textual._loop import loop_last
2930
from textual.color import Color
31+
from textual.css.styles import RulesMap
3032
from textual.css.types import TextAlign
3133
from textual.selection import Selection
3234
from textual.strip import Strip
3335
from textual.style import Style
3436
from textual.visual import Visual
3537

3638
if TYPE_CHECKING:
37-
from textual.widget import Widget
39+
pass
3840

3941

4042
ContentType: TypeAlias = Union["Content", str]
@@ -275,7 +277,11 @@ def __lt__(self, other: object) -> bool:
275277
return self.plain < other.plain
276278
return NotImplemented
277279

278-
def get_optimal_width(self, widget: Widget, container_width: int) -> int:
280+
def get_optimal_width(
281+
self,
282+
rules: RulesMap,
283+
container_width: int,
284+
) -> int:
279285
"""Get optimal width of the visual to display its content. Part of the Textual Visual protocol.
280286
281287
Args:
@@ -289,7 +295,7 @@ def get_optimal_width(self, widget: Widget, container_width: int) -> int:
289295
lines = self.without_spans.split("\n")
290296
return max(line.cell_length for line in lines)
291297

292-
def get_height(self, widget: Widget, width: int) -> int:
298+
def get_height(self, rules: RulesMap, width: int) -> int:
293299
"""Get the height of the visual if rendered with the given width. Part of the Textual Visual protocol.
294300
295301
Args:
@@ -365,37 +371,30 @@ def get_span(y: int) -> tuple[int, int] | None:
365371

366372
def render_strips(
367373
self,
368-
widget: Widget,
374+
rules: RulesMap,
369375
width: int,
370376
height: int | None,
371377
style: Style,
378+
selection: Selection | None = None,
379+
selection_style: Style | None = None,
372380
) -> list[Strip]:
373381
if not width:
374382
return []
375383

376-
selection = widget.selection
377-
if selection is not None:
378-
selection_style = Style.from_rich_style(
379-
widget.screen.get_component_rich_style("screen--selection")
380-
)
381-
382-
else:
383-
selection_style = None
384-
385384
lines = self._wrap_and_format(
386385
width,
387-
align=widget.styles.text_align,
386+
align=rules.get("text_align", "left"),
388387
overflow="fold",
389388
no_wrap=False,
390389
tab_size=8,
391-
selection=widget.selection,
390+
selection=selection,
392391
selection_style=selection_style,
393392
)
394393

395394
if height is not None:
396395
lines = lines[:height]
397396

398-
strip_lines = [line.to_strip(widget, style) for line in lines]
397+
strip_lines = [Strip(*line.to_strip(style)) for line in lines]
399398
return strip_lines
400399

401400
def __len__(self) -> int:
@@ -1128,7 +1127,7 @@ def __init__(
11281127
def plain(self) -> str:
11291128
return self.content.plain
11301129

1131-
def to_strip(self, widget: Widget, style: Style) -> Strip:
1130+
def to_strip(self, style: Style) -> tuple[list[Segment], int]:
11321131
_Segment = Segment
11331132
align = self.align
11341133
width = self.width
@@ -1137,8 +1136,6 @@ def to_strip(self, widget: Widget, style: Style) -> Strip:
11371136
x = self.x
11381137
y = self.y
11391138

1140-
parse_style = widget.app.stylesheet.parse_style
1141-
11421139
if align in ("start", "left") or (align == "justify" and self.line_end):
11431140
pass
11441141

@@ -1166,9 +1163,7 @@ def to_strip(self, widget: Widget, style: Style) -> Strip:
11661163
add_segment = segments.append
11671164
x = self.x
11681165
for index, word in enumerate(words):
1169-
for text, text_style in word.render(
1170-
style, end="", parse_style=parse_style
1171-
):
1166+
for text, text_style in word.render(style, end=""):
11721167
add_segment(
11731168
_Segment(
11741169
text, (style + text_style).rich_style_with_offset(x, y)
@@ -1178,16 +1173,15 @@ def to_strip(self, widget: Widget, style: Style) -> Strip:
11781173
if index < len(spaces) and (pad := spaces[index]):
11791174
add_segment(_Segment(" " * pad, (style + text_style).rich_style))
11801175

1181-
strip = Strip(self._apply_link_style(widget, segments), width)
1182-
return strip
1176+
return segments, width
11831177

11841178
segments = (
11851179
[Segment(" " * pad_left, style.background_style.rich_style)]
11861180
if pad_left
11871181
else []
11881182
)
11891183
add_segment = segments.append
1190-
for text, text_style in content.render(style, end="", parse_style=parse_style):
1184+
for text, text_style in content.render(style, end=""):
11911185
add_segment(
11921186
_Segment(text, (style + text_style).rich_style_with_offset(x, y))
11931187
)
@@ -1197,16 +1191,13 @@ def to_strip(self, widget: Widget, style: Style) -> Strip:
11971191
segments.append(
11981192
_Segment(" " * pad_right, style.background_style.rich_style)
11991193
)
1200-
strip = Strip(
1201-
self._apply_link_style(widget, segments),
1202-
content.cell_length + pad_left + pad_right,
1203-
)
1204-
return strip
1194+
1195+
return (segments, content.cell_length + pad_left + pad_right)
12051196

12061197
def _apply_link_style(
1207-
self, widget: Widget, segments: list[Segment]
1198+
self, link_style: RichStyle, segments: list[Segment]
12081199
) -> list[Segment]:
1209-
link_style = widget.link_style
1200+
12101201
_Segment = Segment
12111202
segments = [
12121203
_Segment(

src/textual/css/styles.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from __future__ import annotations
22

3+
from collections import abc
34
from dataclasses import dataclass, field
45
from functools import partial
56
from operator import attrgetter
6-
from typing import TYPE_CHECKING, Any, Callable, Iterable, Literal, cast
7+
from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, Literal, cast
78

89
import rich.repr
910
from rich.style import Style
@@ -1245,7 +1246,7 @@ def css(self) -> str:
12451246

12461247

12471248
@rich.repr.auto
1248-
class RenderStyles(StylesBase):
1249+
class RenderStyles(StylesBase, abc.Mapping):
12491250
"""Presents a combined view of two Styles object: a base Styles and inline Styles."""
12501251

12511252
def __init__(self, node: DOMNode, base: Styles, inline_styles: Styles) -> None:
@@ -1265,6 +1266,35 @@ def __eq__(self, other: object) -> bool:
12651266
)
12661267
return NotImplemented
12671268

1269+
def __getitem__(self, key: str) -> object:
1270+
if key not in RULE_NAMES_SET:
1271+
raise KeyError(key)
1272+
return getattr(self, key)
1273+
1274+
def get(self, key: str, default: object | None = None) -> object:
1275+
return getattr(self, key) if key in RULE_NAMES_SET else default
1276+
1277+
def __len__(self) -> int:
1278+
return len(RULE_NAMES)
1279+
1280+
def __iter__(self) -> Iterator[str]:
1281+
return iter(RULE_NAMES)
1282+
1283+
def __contains__(self, key: object) -> bool:
1284+
return key in RULE_NAMES_SET
1285+
1286+
def keys(self) -> Iterable[str]:
1287+
return RULE_NAMES
1288+
1289+
def values(self) -> Iterable[object]:
1290+
for key in RULE_NAMES:
1291+
yield getattr(self, key)
1292+
1293+
def items(self) -> Iterable[tuple[str, object]]:
1294+
get_rule = self.get_rule
1295+
for key in RULE_NAMES:
1296+
yield (key, getattr(self, key))
1297+
12681298
@property
12691299
def _cache_key(self) -> int:
12701300
"""A cache key, that changes when any style is changed.

src/textual/css/tokenizer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,12 @@ def __init__(self, description: str, **tokens: str) -> None:
130130
self._expect_semicolon = True
131131

132132
def expect_eof(self, eof: bool) -> Expect:
133+
"""Expect an end of file."""
133134
self._expect_eof = eof
134135
return self
135136

136137
def expect_semicolon(self, semicolon: bool) -> Expect:
138+
"""Tokenizer expects text to be terminated with a semi-colon."""
137139
self._expect_semicolon = semicolon
138140
return self
139141

src/textual/strip.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,27 @@ def apply_style(self, style: Style) -> Strip:
599599
self._style_cache[style] = styled_strip
600600
return styled_strip
601601

602+
def _apply_link_style(self, link_style: Style) -> Strip:
603+
segments = self._segments
604+
_Segment = Segment
605+
segments = [
606+
(
607+
_Segment(
608+
text,
609+
(
610+
style
611+
if style._meta is None
612+
else (style + link_style if "@click" in style.meta else style)
613+
),
614+
control,
615+
)
616+
if style
617+
else _Segment(text)
618+
)
619+
for text, style, control in segments
620+
]
621+
return Strip(segments, self._cell_length)
622+
602623
def render(self, console: Console) -> str:
603624
"""Render the strip into terminal sequences.
604625

0 commit comments

Comments
 (0)