From 9538fbd418e31af61c73acbc396e425247bcd943 Mon Sep 17 00:00:00 2001 From: TomJGooding <101601846+TomJGooding@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:28:36 +0000 Subject: [PATCH] fix(widget): fix visual not updated after style changes Fix a significant regression in Textual v6.0.0 where the widget visual is not updated after style changes. This effectively reverts the changes in dca9439, which intended to optimize styles caching but clearly had unintended consequences. Fixes https://github.com/Textualize/textual/issues/6235 Fixes https://github.com/Textualize/textual/issues/6289 Fixes https://github.com/Textualize/textual/issues/6322 --- src/textual/widget.py | 79 ++++----- ...est_visual_updates_after_style_changes.svg | 150 ++++++++++++++++++ tests/snapshot_tests/test_snapshots.py | 25 +++ 3 files changed, 208 insertions(+), 46 deletions(-) create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_visual_updates_after_style_changes.svg diff --git a/src/textual/widget.py b/src/textual/widget.py index 68c88b0f39..57243f763b 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -455,8 +455,6 @@ def __init__( self._layout_cache: dict[str, object] = {} """A dict that is refreshed when the widget is resized / refreshed.""" - self._visual_style: VisualStyle | None = None - self._render_cache = _RenderCache(_null_size, []) # Regions which need to be updated (in Widget) self._dirty_regions: set[Region] = set() @@ -721,14 +719,6 @@ def preflight_checks(self) -> None: f"'{self.__class__.__name__}.CSS' will be ignored (use 'DEFAULT_CSS' class variable for widgets)" ) - def pre_render(self) -> None: - """Called prior to rendering. - - If you implement this in a subclass, be sure to call the base class method via super. - - """ - self._visual_style = None - def _cover(self, widget: Widget) -> None: """Set a widget used to replace the visuals of this widget (used for loading indicator). @@ -4127,43 +4117,41 @@ def _scroll_update(self, virtual_size: Size) -> None: @property def visual_style(self) -> VisualStyle: - if self._visual_style is None: - background = Color(0, 0, 0, 0) - color = Color(255, 255, 255, 0) - - style = Style() - opacity = 1.0 + background = Color(0, 0, 0, 0) + color = Color(255, 255, 255, 0) - for node in reversed(self.ancestors_with_self): - styles = node.styles - has_rule = styles.has_rule - opacity *= styles.opacity - if has_rule("background"): - text_background = background + styles.background.tint( - styles.background_tint - ) - background += ( - styles.background.tint(styles.background_tint) - ).multiply_alpha(opacity) - else: - text_background = background - if has_rule("color"): - color = styles.color - style += styles.text_style - if has_rule("auto_color") and styles.auto_color: - color = text_background.get_contrast_text(color.a) + style = Style() + opacity = 1.0 - self._visual_style = VisualStyle( - background, - color, - bold=style.bold, - dim=style.dim, - italic=style.italic, - reverse=style.reverse, - underline=style.underline, - strike=style.strike, - ) - return self._visual_style + for node in reversed(self.ancestors_with_self): + styles = node.styles + has_rule = styles.has_rule + opacity *= styles.opacity + if has_rule("background"): + text_background = background + styles.background.tint( + styles.background_tint + ) + background += ( + styles.background.tint(styles.background_tint) + ).multiply_alpha(opacity) + else: + text_background = background + if has_rule("color"): + color = styles.color + style += styles.text_style + if has_rule("auto_color") and styles.auto_color: + color = text_background.get_contrast_text(color.a) + + return VisualStyle( + background, + color, + bold=style.bold, + dim=style.dim, + italic=style.italic, + reverse=style.reverse, + underline=style.underline, + strike=style.strike, + ) def get_selection(self, selection: Selection) -> tuple[str, str] | None: """Get the text under the selection. @@ -4633,7 +4621,6 @@ async def broker_event(self, event_name: str, event: events.Event) -> bool: def notify_style_update(self) -> None: self._rich_style_cache.clear() self._visual_style_cache.clear() - self._visual_style = None super().notify_style_update() async def _on_mouse_down(self, event: events.MouseDown) -> None: diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_visual_updates_after_style_changes.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_visual_updates_after_style_changes.svg new file mode 100644 index 0000000000..0b341d348b --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_visual_updates_after_style_changes.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + UpdateStylesApp + + + + + + + + + + + + + + + + + + + + +yellow on red + + + + + + + + + + + + + + + diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index c84b34e467..f53a320c42 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -4804,3 +4804,28 @@ async def run_before(pilot: Pilot) -> None: await pilot.press("ctrl+v") assert snap_compare(TextAreaApp(), run_before=run_before) + + +def test_visual_updates_after_style_changes(snap_compare) -> None: + """Regression test for https://github.com/Textualize/textual/issues/6289 + + You should see a widget with a red background and yellow text. + """ + + class UpdateStylesApp(App): + CSS = """ + Static { + height: 1fr; + content-align: center middle; + } + """ + + def compose(self) -> ComposeResult: + yield Static("yellow on red") + + def key_s(self) -> None: + widget = self.query_one(Static) + widget.styles.color = "yellow" + widget.styles.background = "red" + + assert snap_compare(UpdateStylesApp(), press=["s"])