Skip to content

Commit 5f7355c

Browse files
committed
cache visual styles, speedup OptionList
1 parent ae03feb commit 5f7355c

File tree

7 files changed

+58
-43
lines changed

7 files changed

+58
-43
lines changed

src/textual/content.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,9 @@ def assemble(
313313
part_text, part_style = part
314314
text_append(part_text)
315315
if part_style:
316-
spans.append(_Span(position, position + len(part_text), part_style))
316+
spans.append(
317+
_Span(position, position + len(part_text), part_style),
318+
)
317319
position += len(part_text)
318320
elif isinstance(part, Content):
319321
text_append(part.plain)

src/textual/widget.py

+49-41
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@ def __init__(
450450

451451
self._styles_cache = StylesCache()
452452
self._rich_style_cache: dict[tuple[str, ...], tuple[Style, Style]] = {}
453+
self._visual_style_cache: dict[tuple[str, ...], VisualStyle] = {}
453454

454455
self._tooltip: RenderableType | None = None
455456
"""The tooltip content."""
@@ -1043,7 +1044,7 @@ def get_component_rich_style(self, *names: str, partial: bool = False) -> Style:
10431044

10441045
return partial_style if partial else style
10451046

1046-
def get_visual_style(self, component_classes: Iterable[str]) -> VisualStyle:
1047+
def get_visual_style(self, *component_classes: str) -> VisualStyle:
10471048
"""Get the visual style for the widget, including any component styles.
10481049
10491050
Args:
@@ -1053,46 +1054,51 @@ def get_visual_style(self, component_classes: Iterable[str]) -> VisualStyle:
10531054
A Visual style instance.
10541055
10551056
"""
1056-
background = Color(0, 0, 0, 0)
1057-
color = Color(255, 255, 255, 0)
1058-
1059-
style = Style()
1060-
opacity = 1.0
1061-
1062-
def iter_styles() -> Iterable[StylesBase]:
1063-
"""Iterate over the styles from the DOM and additional components styles."""
1064-
for node in reversed(self.ancestors_with_self):
1065-
yield node.styles
1066-
for name in component_classes:
1067-
yield node.get_component_styles(name)
1068-
1069-
for styles in iter_styles():
1070-
has_rule = styles.has_rule
1071-
opacity *= styles.opacity
1072-
if has_rule("background"):
1073-
text_background = background + styles.background.tint(
1074-
styles.background_tint
1075-
)
1076-
background += (
1077-
styles.background.tint(styles.background_tint)
1078-
).multiply_alpha(opacity)
1079-
else:
1080-
text_background = background
1081-
if has_rule("color"):
1082-
color = styles.color
1083-
style += styles.text_style
1084-
if has_rule("auto_color") and styles.auto_color:
1085-
color = text_background.get_contrast_text(color.a)
1086-
1087-
visual_style = VisualStyle(
1088-
background,
1089-
color,
1090-
bold=style.bold,
1091-
dim=style.dim,
1092-
italic=style.italic,
1093-
underline=style.underline,
1094-
strike=style.strike,
1095-
)
1057+
if (
1058+
visual_style := self._visual_style_cache.get(component_classes, None)
1059+
) is None:
1060+
# TODO: cache this?
1061+
background = Color(0, 0, 0, 0)
1062+
color = Color(255, 255, 255, 0)
1063+
1064+
style = Style()
1065+
opacity = 1.0
1066+
1067+
def iter_styles() -> Iterable[StylesBase]:
1068+
"""Iterate over the styles from the DOM and additional components styles."""
1069+
for node in reversed(self.ancestors_with_self):
1070+
yield node.styles
1071+
for name in component_classes:
1072+
yield node.get_component_styles(name)
1073+
1074+
for styles in iter_styles():
1075+
has_rule = styles.has_rule
1076+
opacity *= styles.opacity
1077+
if has_rule("background"):
1078+
text_background = background + styles.background.tint(
1079+
styles.background_tint
1080+
)
1081+
background += (
1082+
styles.background.tint(styles.background_tint)
1083+
).multiply_alpha(opacity)
1084+
else:
1085+
text_background = background
1086+
if has_rule("color"):
1087+
color = styles.color
1088+
style += styles.text_style
1089+
if has_rule("auto_color") and styles.auto_color:
1090+
color = text_background.get_contrast_text(color.a)
1091+
1092+
visual_style = VisualStyle(
1093+
background,
1094+
color,
1095+
bold=style.bold,
1096+
dim=style.dim,
1097+
italic=style.italic,
1098+
underline=style.underline,
1099+
strike=style.strike,
1100+
)
1101+
self._visual_style_cache[component_classes] = visual_style
10961102

10971103
return visual_style
10981104

@@ -4270,6 +4276,8 @@ async def broker_event(self, event_name: str, event: events.Event) -> bool:
42704276

42714277
def notify_style_update(self) -> None:
42724278
self._rich_style_cache.clear()
4279+
self._visual_style_cache.clear()
4280+
super().notify_style_update()
42734281

42744282
async def _on_mouse_down(self, event: events.MouseDown) -> None:
42754283
await self.broker_event("mouse.down", event)

src/textual/widgets/_data_table.py

+1
Original file line numberDiff line numberDiff line change
@@ -1107,6 +1107,7 @@ def get_row_height(self, row_key: RowKey) -> int:
11071107
return self.rows[row_key].height
11081108

11091109
def notify_style_update(self) -> None:
1110+
super().notify_style_update()
11101111
self._row_render_cache.clear()
11111112
self._cell_render_cache.clear()
11121113
self._line_cache.clear()

src/textual/widgets/_log.py

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def lines(self) -> Sequence[str]:
9494

9595
def notify_style_update(self) -> None:
9696
"""Called by Textual when styles update."""
97+
super().notify_style_update()
9798
self._render_line_cache.clear()
9899

99100
def _update_maximum_width(self, updates: int, size: int) -> None:

src/textual/widgets/_option_list.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ def _refresh_lines(self) -> None:
341341

342342
def notify_style_update(self) -> None:
343343
self._content_render_cache.clear()
344+
super().notify_style_update()
344345

345346
def _on_resize(self):
346347
self._refresh_lines()
@@ -486,7 +487,7 @@ def _render_option_content(
486487
if component_class:
487488
component_class_list.append(component_class)
488489

489-
visual_style = self.get_visual_style(component_class_list)
490+
visual_style = self.get_visual_style(*component_class_list)
490491

491492
strips = Visual.to_strips(self, visual, width, None, visual_style, pad=True)
492493
style_meta = Style.from_meta({"option": option_index})

src/textual/widgets/_rich_log.py

+1
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def __init__(
126126
indicating we can proceed with rendering deferred writes."""
127127

128128
def notify_style_update(self) -> None:
129+
super().notify_style_update()
129130
self._line_cache.clear()
130131

131132
def on_resize(self, event: Resize) -> None:

src/textual/widgets/_tree.py

+1
Original file line numberDiff line numberDiff line change
@@ -1462,6 +1462,7 @@ async def _on_click(self, event: events.Click) -> None:
14621462
await self.run_action("select_cursor")
14631463

14641464
def notify_style_update(self) -> None:
1465+
super().notify_style_update()
14651466
self._invalidate()
14661467

14671468
def action_cursor_up(self) -> None:

0 commit comments

Comments
 (0)