Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 23 additions & 23 deletions src/textual/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from textual.selection import Selection
from textual.strip import Strip
from textual.style import Style
from textual.visual import RulesMap, Visual
from textual.visual import RenderOptions, RulesMap, Visual

__all__ = ["ContentType", "Content", "Span"]

Expand Down Expand Up @@ -502,6 +502,7 @@ def _wrap_and_format(
selection: Selection | None = None,
selection_style: Style | None = None,
post_style: Style | None = None,
get_style: Callable[[str | Style], Style] = Style.parse,
) -> list[_FormattedLine]:
"""Wraps the text and applies formatting.

Expand Down Expand Up @@ -543,15 +544,17 @@ def get_span(y: int) -> tuple[int, int] | None:
if overflow == "fold":
cuts = list(range(0, line.cell_length, width))[1:]
new_lines = [
_FormattedLine(line, width, y=y, align=align)
_FormattedLine(get_style, line, width, y=y, align=align)
for line in line.divide(cuts)
]
else:
line = line.truncate(width, ellipsis=overflow == "ellipsis")
content_line = _FormattedLine(line, width, y=y, align=align)
content_line = _FormattedLine(
get_style, line, width, y=y, align=align
)
new_lines = [content_line]
else:
content_line = _FormattedLine(line, width, y=y, align=align)
content_line = _FormattedLine(get_style, line, width, y=y, align=align)
offsets = divide_line(
line.plain, width - line_pad * 2, fold=overflow == "fold"
)
Expand All @@ -568,6 +571,7 @@ def get_span(y: int) -> tuple[int, int] | None:

new_lines = [
_FormattedLine(
get_style,
content.rstrip_end(width).pad(line_pad, line_pad),
width,
offset,
Expand All @@ -583,25 +587,15 @@ def get_span(y: int) -> tuple[int, int] | None:
return output_lines

def render_strips(
self,
rules: RulesMap,
width: int,
height: int | None,
style: Style,
selection: Selection | None = None,
selection_style: Style | None = None,
post_style: Style | None = None,
self, width: int, height: int | None, style: Style, options: RenderOptions
) -> list[Strip]:
"""Render the visual into an iterable of strips. Part of the Visual protocol.
"""Render the Visual into an iterable of strips. Part of the Visual protocol.

Args:
rules: A mapping of style rules, such as the Widgets `styles` object.
width: Width of desired render.
height: Height of desired render or `None` for any height.
style: The base style to render on top of.
selection: Selection information, if applicable, otherwise `None`.
selection_style: Selection style if `selection` is not `None`.
post_style: Style | None = None,
options: Additional render options.

Returns:
An list of Strips.
Expand All @@ -610,17 +604,18 @@ def render_strips(
if not width:
return []

get_rule = rules.get
get_rule = options.rules.get
lines = self._wrap_and_format(
width,
align=get_rule("text_align", "left"),
overflow=get_rule("text_overflow", "fold"),
no_wrap=get_rule("text_wrap", "wrap") == "nowrap",
line_pad=get_rule("line_pad", 0),
tab_size=8,
selection=selection,
selection_style=selection_style,
post_style=post_style,
selection=options.selection,
selection_style=options.selection_style,
post_style=options.post_style,
get_style=options.get_style,
)

if height is not None:
Expand Down Expand Up @@ -1478,6 +1473,7 @@ class _FormattedLine:

def __init__(
self,
get_style: Callable[[str | Style], Style],
content: Content,
width: int,
x: int = 0,
Expand All @@ -1486,6 +1482,7 @@ def __init__(
line_end: bool = False,
link_style: Style | None = None,
) -> None:
self.get_style = get_style
self.content = content
self.width = width
self.x = x
Expand All @@ -1506,6 +1503,7 @@ def to_strip(self, style: Style) -> tuple[list[Segment], int]:
content = self.content
x = self.x
y = self.y
get_style = self.get_style

if align in ("start", "left") or (align == "justify" and self.line_end):
pass
Expand Down Expand Up @@ -1534,7 +1532,9 @@ def to_strip(self, style: Style) -> tuple[list[Segment], int]:
add_segment = segments.append
x = self.x
for index, word in enumerate(words):
for text, text_style in word.render(style, end=""):
for text, text_style in word.render(
style, end="", parse_style=get_style
):
add_segment(
_Segment(
text, (style + text_style).rich_style_with_offset(x, y)
Expand All @@ -1552,7 +1552,7 @@ def to_strip(self, style: Style) -> tuple[list[Segment], int]:
else []
)
add_segment = segments.append
for text, text_style in content.render(style, end=""):
for text, text_style in content.render(style, end="", parse_style=get_style):
add_segment(
_Segment(text, (style + text_style).rich_style_with_offset(x, y))
)
Expand Down
23 changes: 13 additions & 10 deletions src/textual/renderables/blank.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
from textual.color import Color
from textual.content import Style
from textual.css.styles import RulesMap
from textual.selection import Selection
from textual.strip import Strip
from textual.visual import Visual
from textual.visual import RenderOptions, Visual


class Blank(Visual):
Expand All @@ -26,14 +25,18 @@ def get_height(self, rules: RulesMap, width: int) -> int:
return 1

def render_strips(
self,
rules: RulesMap,
width: int,
height: int | None,
style: Style,
selection: Selection | None = None,
selection_style: Style | None = None,
post_style: Style | None = None,
self, width: int, height: int | None, style: Style, options: RenderOptions
) -> list[Strip]:
"""Render the Visual into an iterable of strips. Part of the Visual protocol.

Args:
width: Width of desired render.
height: Height of desired render or `None` for any height.
style: The base style to render on top of.
options: Additional render options.

Returns:
An list of Strips.
"""
line_count = 1 if height is None else height
return [Strip.blank(width, self._rich_style)] * line_count
90 changes: 53 additions & 37 deletions src/textual/visual.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass
from itertools import islice
from typing import TYPE_CHECKING, Protocol
from typing import TYPE_CHECKING, Callable, Protocol

import rich.repr
from rich.console import Console, ConsoleOptions, RenderableType
Expand Down Expand Up @@ -31,6 +32,22 @@ def is_visual(obj: object) -> bool:
return isinstance(obj, Visual) or hasattr(obj, "textualize")


@dataclass(frozen=True)
class RenderOptions:
"""Additional options passed to `Visual.render_strips`."""

get_style: Callable[[str | Style], Style]
"""Callable to get a style."""
rules: RulesMap
"""Mapping of style rules."""
selection: Selection | None = None
"""Text selection information."""
selection_style: Style | None = None
"""Style of text selection."""
post_style: Style | None = None
"""Optional style to apply post render."""


# Note: not runtime checkable currently, as I've found that to be slow
class SupportsVisual(Protocol):
"""An object that supports the textualize protocol."""
Expand Down Expand Up @@ -112,25 +129,15 @@ class Visual(ABC):

@abstractmethod
def render_strips(
self,
rules: RulesMap,
width: int,
height: int | None,
style: Style,
selection: Selection | None = None,
selection_style: Style | None = None,
post_style: Style | None = None,
self, width: int, height: int | None, style: Style, options: RenderOptions
) -> list[Strip]:
"""Render the Visual into an iterable of strips.

Args:
rules: A mapping of style rules, such as the Widgets `styles` object.
width: Width of desired render.
height: Height of desired render or `None` for any height.
style: The base style to render on top of.
selection: Selection information, if applicable, otherwise `None`.
selection_style: Selection style if `selection` is not `None`.
post_style: Optional style to apply post render.
options: Additional render options.

Returns:
An list of Strips.
Expand Down Expand Up @@ -216,12 +223,15 @@ def to_strips(
selection_style = None

strips = visual.render_strips(
widget.styles,
width,
height,
style,
selection,
selection_style,
RenderOptions(
widget._get_style,
widget.styles,
selection,
selection_style,
),
)
strips = [strip._apply_link_style(widget.link_style) for strip in strips]

Expand Down Expand Up @@ -303,24 +313,28 @@ def get_height(self, rules: RulesMap, width: int) -> int:
return height

def render_strips(
self,
rules: RulesMap,
width: int,
height: int | None,
style: Style,
selection: Selection | None = None,
selection_style: Style | None = None,
post_style: Style | None = None,
self, width: int, height: int | None, style: Style, options: RenderOptions
) -> list[Strip]:
"""Render the Visual into an iterable of strips. Part of the Visual protocol.

Args:
width: Width of desired render.
height: Height of desired render or `None` for any height.
style: The base style to render on top of.
options: Additional render options.

Returns:
An list of Strips.
"""
console = active_app.get().console
options = console.options.update(
console_options = console.options.update(
highlight=False,
width=width,
height=height,
)
rich_style = style.rich_style
renderable = self._widget.post_render(self._renderable, rich_style)
segments = console.render(renderable, options.update_width(width))
segments = console.render(renderable, console_options.update_width(width))
strips = [
Strip(line)
for line in islice(
Expand Down Expand Up @@ -365,28 +379,30 @@ def get_height(self, rules: RulesMap, width: int) -> int:
)

def render_strips(
self,
rules: RulesMap,
width: int,
height: int | None,
style: Style,
selection: Selection | None = None,
selection_style: Style | None = None,
post_style: Style | None = None,
self, width: int, height: int | None, style: Style, options: RenderOptions
) -> list[Strip]:
"""Render the Visual into an iterable of strips. Part of the Visual protocol.

Args:
width: Width of desired render.
height: Height of desired render or `None` for any height.
style: The base style to render on top of.
options: Additional render options.

Returns:
An list of Strips.
"""
padding = self._spacing
top, right, bottom, left = self._spacing
render_width = width - (left + right)
if render_width <= 0:
return []

strips = self._visual.render_strips(
rules,
render_width,
None if height is None else height - padding.height,
style,
selection,
selection_style,
options,
)

if padding:
Expand Down
34 changes: 28 additions & 6 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -1140,9 +1140,15 @@ def iter_styles() -> Iterable[StylesBase]:
text_background = background + styles.background.tint(
styles.background_tint
)
background += (
styles.background.tint(styles.background_tint)
).multiply_alpha(opacity)
if partial:
background_tint = styles.background.tint(styles.background_tint)
background = background.blend(
background_tint, 1 - background_tint.a
).multiply_alpha(opacity)
else:
background += (
styles.background.tint(styles.background_tint)
).multiply_alpha(opacity)
else:
text_background = background
if has_rule("color"):
Expand All @@ -1164,7 +1170,7 @@ def iter_styles() -> Iterable[StylesBase]:

return visual_style

def _get_style(self, style: str) -> VisualStyle | None:
def _get_style(self, style: VisualStyle | str) -> VisualStyle:
"""A get_style method for use in Content.

Args:
Expand All @@ -1173,9 +1179,25 @@ def _get_style(self, style: str) -> VisualStyle | None:
Returns:
A visual style if one is fund, otherwise `None`.
"""
if isinstance(style, VisualStyle):
return style
if style.startswith("."):
return self.get_visual_style(style[1:])
return None
for node in self.ancestors_with_self:
if not isinstance(node, Widget):
break
try:
visual_style = node.get_visual_style(style[1:], partial=True)
break
except KeyError:
continue
else:
raise KeyError(f"No matching component class found for '{style}'")
return visual_style
try:
visual_style = VisualStyle.parse(style)
except Exception:
visual_style = VisualStyle.null()
return visual_style

@overload
def render_str(self, text_content: str) -> Content: ...
Expand Down
Loading
Loading