From 2197fd08e64f20398f71a553b252c4d5dd58545f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 Jan 2025 12:01:18 +0000 Subject: [PATCH] optimize style parse --- src/textual/_compositor.py | 18 ++++++++------ src/textual/_style_parse.py | 26 ++++++++++++++------ src/textual/content.py | 21 ++++++---------- src/textual/css/stylesheet.py | 3 ++- src/textual/css/tokenize.py | 1 + src/textual/markup.py | 8 +----- src/textual/widget.py | 3 +++ tests/snapshot_tests/snapshot_apps/markup.py | 2 +- 8 files changed, 45 insertions(+), 37 deletions(-) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index e82e8b8b00..b481f915e2 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -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() @@ -901,10 +902,11 @@ 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 @@ -912,7 +914,7 @@ def get_widget_and_offset_at( 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: diff --git a/src/textual/_style_parse.py b/src/textual/_style_parse.py index f12a465cee..8720f38f83 100644 --- a/src/textual/_style_parse.py +++ b/src/textual/_style_parse.py @@ -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 @@ -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 @@ -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( diff --git a/src/textual/content.py b/src/textual/content.py index 87e6aa32c9..25ddb3ff18 100644 --- a/src/textual/content.py +++ b/src/textual/content.py @@ -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 @@ -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 @@ -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 @@ -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) @@ -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)) ) diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py index b2b670823b..bec2c1a2a9 100644 --- a/src/textual/css/stylesheet.py +++ b/src/textual/css/stylesheet.py @@ -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( diff --git a/src/textual/css/tokenize.py b/src/textual/css/tokenize.py index f02e2516ef..b20b14f17b 100644 --- a/src/textual/css/tokenize.py +++ b/src/textual/css/tokenize.py @@ -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, diff --git a/src/textual/markup.py b/src/textual/markup.py index 1257216f8a..84f20c5522 100644 --- a/src/textual/markup.py +++ b/src/textual/markup.py @@ -15,7 +15,6 @@ Union, ) -from textual.css.types import TextAlign from textual.style import Style if TYPE_CHECKING: @@ -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. diff --git a/src/textual/widget.py b/src/textual/widget.py index 9fffa1fb0c..a1a0890c19 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -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 @@ -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]: diff --git a/tests/snapshot_tests/snapshot_apps/markup.py b/tests/snapshot_tests/snapshot_apps/markup.py index 1c7fc30a53..defdc35b84 100644 --- a/tests/snapshot_tests/snapshot_apps/markup.py +++ b/tests/snapshot_tests/snapshot_apps/markup.py @@ -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__":