Skip to content

Commit

Permalink
optimize style parse
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed Jan 23, 2025
1 parent ed79ffd commit 2197fd0
Show file tree
Hide file tree
Showing 8 changed files with 45 additions and 37 deletions.
18 changes: 10 additions & 8 deletions src/textual/_compositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -862,13 +862,14 @@ def get_style_at(self, x: int, y: int) -> Style:

# TODO: This prompts a render, can we avoid that?
visible_screen_stack.set(widget.app._background_screens)
lines = widget.render_lines(Region(0, y, region.width, 1))
line = widget.render_line(y)
# lines = widget.render_lines(Region(0, y, region.width, 1))

if not lines:
return Style.null()
# if not lines:
# return Style.null()
end = 0

for segment in lines[0]:
for segment in line:
end += segment.cell_length
if x < end:
return segment.style or Style.null()
Expand Down Expand Up @@ -901,18 +902,19 @@ def get_widget_and_offset_at(
y -= region.y

visible_screen_stack.set(widget.app._background_screens)
lines = widget.render_lines(Region(0, y, region.width, 1))
# lines = widget.render_lines(Region(0, y, region.width, 1))
line = widget.render_line(y)

if not lines:
return widget, None
# if not lines:
# return widget, None
end = 0
start = 0

offset_y: int | None = None
offset_x = 0
offset_x2 = 0

for segment in lines[0]:
for segment in line:
end += len(segment.text)
style = segment.style
if style is not None and style._meta is not None:
Expand Down
26 changes: 19 additions & 7 deletions src/textual/_style_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,20 @@
"s": "strike",
}

from textual._profile import timer


@timer("style parse")
def style_parse(style_text: str, variables: dict[str, str] | None) -> Style:
"""Parse a Textual style definition.
Note that variables should be `None` when called within a running app. This is so that
this method can use a style cache from the app. Supply a variables dict only for testing.
Args:
style_text: String containing style definition.
variables: Variables to use, or `None` for variables from active app.
Returns:
Style instance.
"""
styles: dict[str, bool | None] = {style: None for style in STYLES}

color: Color | None = None
Expand All @@ -33,16 +42,19 @@ def style_parse(style_text: str, variables: dict[str, str] | None) -> Style:
try:
app = active_app.get()
except LookupError:
pass
reference_tokens = {}
else:
reference_tokens = app.stylesheet._variable_tokens
else:
reference_tokens = tokenize_values(variables)

for token in substitute_references(
tokenize_style(style_text, read_from=("inline style", "")), reference_tokens
tokenize_style(
style_text,
read_from=("inline style", ""),
),
reference_tokens,
):

name = token.name
value = token.value

Expand Down Expand Up @@ -101,7 +113,7 @@ def style_parse(style_text: str, variables: dict[str, str] | None) -> Style:

if __name__ == "__main__":
variables = {"accent": "red"}
print(style_parse("link=https://www.textualize.io", {}))
print(style_parse("blue 20%", None))

# print(
# style_parse(
Expand Down
21 changes: 8 additions & 13 deletions src/textual/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,7 @@ def markup(self) -> str:
return markup

@classmethod
def from_markup(
cls,
markup: str,
align: TextAlign = "left",
no_wrap: bool = False,
ellipsis: bool = False,
) -> Content:
def from_markup(cls, markup: str) -> Content:
"""Create content from Textual markup.
!!! note
Expand All @@ -187,16 +181,13 @@ def from_markup(
Args:
markup: Textual Markup
align: Align method.
no_wrap: Disable wrapping.
ellipsis: Add ellipsis when wrapping is disabled and text is cropped.
Returns:
New Content instance.
"""
from textual.markup import to_content

content = to_content(markup, align=align, no_wrap=no_wrap, ellipsis=ellipsis)
content = to_content(markup)
return content

@classmethod
Expand Down Expand Up @@ -1145,6 +1136,8 @@ def to_strip(self, widget: Widget, style: Style) -> Strip:
x = self.x
y = self.y

parse_style = widget.app.stylesheet.parse_style

if align in ("start", "left") or (align == "justify" and self.line_end):
pass

Expand Down Expand Up @@ -1172,7 +1165,9 @@ def to_strip(self, widget: Widget, style: Style) -> Strip:
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=parse_style
):
add_segment(
_Segment(
text, (style + text_style).rich_style_with_offset(x, y)
Expand All @@ -1191,7 +1186,7 @@ def to_strip(self, widget: Widget, style: Style) -> Strip:
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=parse_style):
add_segment(
_Segment(text, (style + text_style).rich_style_with_offset(x, y))
)
Expand Down
3 changes: 2 additions & 1 deletion src/textual/css/stylesheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,8 @@ def parse_style(self, style_text: str) -> Style:
"""
if style_text in self._style_parse_cache:
return self._style_parse_cache[style_text]
style = style_parse(style_text, self._variables)
style = style_parse(style_text, None)
self._style_parse_cache[style_text] = style
return style

def _parse_rules(
Expand Down
1 change: 1 addition & 0 deletions src/textual/css/tokenize.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ class StyleTokenizerState(TokenizerState):
key_value=r"[@a-zA-Z_-][a-zA-Z0-9_-]*=.*",
key_value_quote=r"[@a-zA-Z_-][a-zA-Z0-9_-]*='.*'",
key_value_double_quote=r"""[@a-zA-Z_-][a-zA-Z0-9_-]*=".*\"""",
percent=PERCENT,
color=COLOR,
token=TOKEN,
variable_ref=VARIABLE_REF,
Expand Down
8 changes: 1 addition & 7 deletions src/textual/markup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
Union,
)

from textual.css.types import TextAlign
from textual.style import Style

if TYPE_CHECKING:
Expand Down Expand Up @@ -125,17 +124,12 @@ def _parse(markup: str) -> Iterable[Tuple[int, Optional[str], Optional[Tag]]]:
def to_content(
markup: str,
style: Union[str, Style] = "",
align: TextAlign = "left",
no_wrap: bool = False,
ellipsis: bool = False,
) -> Content:
"""Render console markup in to a Text instance.
Args:
markup (str): A string containing console markup.
style: (Union[str, Style]): The style to use.
style: (Union[str, Style]): Base style for entire content, or empty string for no base style.
Raises:
MarkupError: If there is a syntax error in the markup.
Expand Down
3 changes: 3 additions & 0 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -1675,6 +1675,8 @@ def get_content_height(self, container: Size, viewport: Size, width: int) -> int
def watch_hover_style(
self, previous_hover_style: Style, hover_style: Style
) -> None:
# TODO: This will cause the widget to refresh, even when there are no links
# Can we avoid this?
if self.auto_links:
self.highlight_link_id = hover_style.link_id

Expand Down Expand Up @@ -3867,6 +3869,7 @@ def render_line(self, y: int) -> Strip:
line = self._render_cache.lines[y]
except IndexError:
line = Strip.blank(self.size.width, self.rich_style)

return line

def render_lines(self, crop: Region) -> list[Strip]:
Expand Down
2 changes: 1 addition & 1 deletion tests/snapshot_tests/snapshot_apps/markup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def compose(self) -> ComposeResult:
"[on $boost] [on $boost] [on $boost] Three layers of $boost [/] [/] [/]"
)
yield Label("[on $primary 20%]On primary twenty percent")
yield Label("[$text 80% on primary]Hello")
yield Label("[$text 80% on $primary]Hello")


if __name__ == "__main__":
Expand Down

0 comments on commit 2197fd0

Please sign in to comment.