Skip to content

Commit 112355e

Browse files
authored
Merge pull request #5063 from Textualize/invisible-refresh
refreshing invisible widgets is a no-op
2 parents 1e3d018 + df01b7a commit 112355e

File tree

7 files changed

+233
-11
lines changed

7 files changed

+233
-11
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## Unreleased
99

10+
### Fixed
11+
12+
- Fixed issue with screen not updating when auto_refresh was enabled https://github.com/Textualize/textual/pull/5063
13+
1014
### Added
1115

16+
- Added `DOMNode.is_on_screen` property https://github.com/Textualize/textual/pull/5063
1217
- Added support for keymaps (user configurable key bindings) https://github.com/Textualize/textual/pull/5038
1318
- Added descriptions to bindings for all internal widgets, and updated casing to be consistent https://github.com/Textualize/textual/pull/5062
1419

src/textual/_compositor.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -311,9 +311,6 @@ def __init__(self) -> None:
311311
# Mapping of line numbers on to lists of widget and regions
312312
self._layers_visible: list[list[tuple[Widget, Region, Region]]] | None = None
313313

314-
# New widgets added between updates
315-
self._new_widgets: set[Widget] = set()
316-
317314
def clear(self) -> None:
318315
"""Remove all references to widgets (used when the screen closes)."""
319316
self._full_map.clear()
@@ -392,8 +389,7 @@ def reflow(self, parent: Widget, size: Size) -> ReflowResult:
392389
new_widgets = map.keys()
393390

394391
# Newly visible widgets
395-
shown_widgets = (new_widgets - old_widgets) | self._new_widgets
396-
self._new_widgets.clear()
392+
shown_widgets = new_widgets - old_widgets
397393

398394
# Newly hidden widgets
399395
hidden_widgets = self.widgets - widgets
@@ -490,7 +486,6 @@ def full_map(self) -> CompositorMap:
490486
self._full_map_invalidated = False
491487
map, _widgets = self._arrange_root(self.root, self.size, visible_only=False)
492488
# Update any widgets which became visible in the interim
493-
self._new_widgets.update(map.keys() - self._full_map.keys())
494489
self._full_map = map
495490
self._visible_widgets = None
496491
self._visible_map = None
@@ -803,6 +798,22 @@ def layers_visible(self) -> list[list[tuple[Widget, Region, Region]]]:
803798
self._layers_visible = layers_visible
804799
return self._layers_visible
805800

801+
def __contains__(self, widget: Widget) -> bool:
802+
"""Check if the widget was included in the last update.
803+
804+
Args:
805+
widget: A widget.
806+
807+
Returns:
808+
`True` if the widget was in the last refresh, or `False` if it wasn't.
809+
"""
810+
# Try to avoid a recalculation of full_map if possible.
811+
return (
812+
widget in self.widgets
813+
or (self._visible_map is not None and widget in self._visible_map)
814+
or widget in self.full_map
815+
)
816+
806817
def get_offset(self, widget: Widget) -> Offset:
807818
"""Get the offset of a widget.
808819

src/textual/dom.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,11 @@ def is_modal(self) -> bool:
489489
"""Is the node a modal?"""
490490
return False
491491

492+
@property
493+
def is_on_screen(self) -> bool:
494+
"""Check if the node was displayed in the last screen update."""
495+
return False
496+
492497
def automatic_refresh(self) -> None:
493498
"""Perform an automatic refresh.
494499
@@ -497,7 +502,7 @@ def automatic_refresh(self) -> None:
497502
during an automatic refresh.
498503
499504
"""
500-
if self.display and self.visible:
505+
if self.is_on_screen:
501506
self.refresh()
502507

503508
def __init_subclass__(

src/textual/screen.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ class Screen(Generic[ScreenResultType], Widget):
147147

148148
DEFAULT_CSS = """
149149
Screen {
150+
150151
layout: vertical;
151152
overflow-y: auto;
152153
background: $surface;
@@ -162,11 +163,11 @@ class Screen(Generic[ScreenResultType], Widget):
162163
background: ansi_default;
163164
color: ansi_default;
164165
165-
&.-screen-suspended {
166+
&.-screen-suspended {
167+
text-style: dim;
166168
ScrollBar {
167169
text-style: not dim;
168170
}
169-
text-style: dim;
170171
}
171172
}
172173
}
@@ -1136,8 +1137,9 @@ async def _on_update(self, message: messages.Update) -> None:
11361137
widget = message.widget
11371138
assert isinstance(widget, Widget)
11381139

1139-
self._dirty_widgets.add(widget)
1140-
self.check_idle()
1140+
if self in self._compositor:
1141+
self._dirty_widgets.add(widget)
1142+
self.check_idle()
11411143

11421144
async def _on_layout(self, message: messages.Layout) -> None:
11431145
message.stop()

src/textual/widget.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1927,6 +1927,15 @@ def _has_relative_children_height(self) -> bool:
19271927
return True
19281928
return False
19291929

1930+
@property
1931+
def is_on_screen(self) -> bool:
1932+
"""Check if the node was displayed in the last screen update."""
1933+
try:
1934+
self.screen.find_widget(self)
1935+
except (NoScreen, errors.NoWidget):
1936+
return False
1937+
return True
1938+
19301939
def animate(
19311940
self,
19321941
attribute: str,
Lines changed: 154 additions & 0 deletions
Loading

tests/snapshot_tests/test_snapshots.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
Footer,
2424
Log,
2525
OptionList,
26+
Placeholder,
2627
SelectionList,
2728
)
2829
from textual.widgets import ProgressBar, Label, Switch
@@ -2098,3 +2099,38 @@ def action_toggle_console(self) -> None:
20982099

20992100
app = MRE()
21002101
assert snap_compare(app, press=["space", "space", "z"])
2102+
2103+
2104+
def test_updates_with_auto_refresh(snap_compare):
2105+
"""Regression test for https://github.com/Textualize/textual/issues/5056
2106+
2107+
After hiding and unhiding the RichLog, you should be able to see 1.5 fully rendered placeholder widgets.
2108+
Prior to this fix, the bottom portion of the screen did not
2109+
refresh after the RichLog was hidden/unhidden while in the presence of the auto-refreshing ProgressBar widget.
2110+
"""
2111+
2112+
class MRE(App):
2113+
BINDINGS = [
2114+
("z", "toggle_widget('RichLog')", "Console"),
2115+
]
2116+
CSS = """
2117+
Placeholder { height: 15; }
2118+
RichLog { height: 6; }
2119+
.hidden { display: none; }
2120+
"""
2121+
2122+
def compose(self):
2123+
with VerticalScroll():
2124+
for i in range(10):
2125+
yield Placeholder()
2126+
yield ProgressBar(classes="hidden")
2127+
yield RichLog(classes="hidden")
2128+
2129+
def on_ready(self) -> None:
2130+
self.query_one(RichLog).write("\n".join(f"line #{i}" for i in range(5)))
2131+
2132+
def action_toggle_widget(self, widget_type: str) -> None:
2133+
self.query_one(widget_type).toggle_class("hidden")
2134+
2135+
app = MRE()
2136+
assert snap_compare(app, press=["z", "z"])

0 commit comments

Comments
 (0)