Closed xavierog closed 1 month ago
Events reflected by the Textual dev console after pressing z
:
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
# Pressing 'z' to make the console appear:
Key(key='z', character='z', name='z', is_printable=True) >>> VerticalScroll() method=<Widget.on_key>
Key(key='z', character='z', name='z', is_printable=True) >>> Screen(id='_default') method=<Widget.on_key>
Key(key='z', character='z', name='z', is_printable=True) >>> MRE(title='MRE', classes={'-dark-mode'}, pseudo_classes={'dark', 'focus'}) method=<App.on_key>
<action> namespace=MRE(title='MRE', classes={'-dark-mode'}, pseudo_classes={'dark', 'focus'}) action_name='toggle_console' params=()
Mount() >>> ScrollBar(name='vertical', window_virtual_size=100, window_size=0, position=0, thickness=2) method=<Widget.on_mount>
# No Show/Resize events, the core of this bugreport.
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
# Pressing 'z' to make the console disappear:
Key(key='z', character='z', name='z', is_printable=True) >>> VerticalScroll() method=<Widget.on_key>
Key(key='z', character='z', name='z', is_printable=True) >>> Screen(id='_default') method=<Widget.on_key>
Key(key='z', character='z', name='z', is_printable=True) >>> MRE(title='MRE', classes={'-dark-mode'}, pseudo_classes={'dark', 'focus'}) method=<App.on_key>
<action> namespace=MRE(title='MRE', classes={'-dark-mode'}, pseudo_classes={'dark', 'focus'}) action_name='toggle_console' params=()
Hide() >>> RichLog() method=<Widget.on_hide>
Resize(size=Size(width=255, height=30), virtual_size=Size(width=255, height=30)) >>> VerticalScroll() method=None
Hide() >>> ScrollBar(name='vertical', window_virtual_size=0, window_size=5, position=0, thickness=2) method=<ScrollBar.on_hide>
Hide() >>> ScrollBar(name='vertical', window_virtual_size=0, window_size=5, position=0, thickness=2) method=<Widget.on_hide>
# Hide and Resize events ok
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
# Pressing 'z' to make the console reappear:
Key(key='z', character='z', name='z', is_printable=True) >>> VerticalScroll() method=<Widget.on_key>
Key(key='z', character='z', name='z', is_printable=True) >>> Screen(id='_default') method=<Widget.on_key>
Key(key='z', character='z', name='z', is_printable=True) >>> MRE(title='MRE', classes={'-dark-mode'}, pseudo_classes={'dark', 'focus'}) method=<App.on_key>
<action> namespace=MRE(title='MRE', classes={'-dark-mode'}, pseudo_classes={'dark', 'focus'}) action_name='toggle_console' params=()
# No Show/Resize events
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
# Pressing 'z' to make the console disappear again:
Key(key='z', character='z', name='z', is_printable=True) >>> VerticalScroll() method=<Widget.on_key>
Key(key='z', character='z', name='z', is_printable=True) >>> Screen(id='_default') method=<Widget.on_key>
Key(key='z', character='z', name='z', is_printable=True) >>> MRE(title='MRE', classes={'-dark-mode'}, pseudo_classes={'dark', 'focus'}) method=<App.on_key>
<action> namespace=MRE(title='MRE', classes={'-dark-mode'}, pseudo_classes={'dark', 'focus'}) action_name='toggle_console' params=()
Hide() >>> RichLog() method=<Widget.on_hide>
Hide() >>> ScrollBar(name='vertical', window_virtual_size=0, window_size=5, position=0, thickness=2) method=<ScrollBar.on_hide>
Hide() >>> ScrollBar(name='vertical', window_virtual_size=0, window_size=5, position=0, thickness=2) method=<Widget.on_hide>
# Hide events ok; missing Resize event to the VerticalScroll?
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
# Pressing 'z' to make the console and its contents appear:
Key(key='z', character='z', name='z', is_printable=True) >>> VerticalScroll() method=<Widget.on_key>
Key(key='z', character='z', name='z', is_printable=True) >>> Screen(id='_default') method=<Widget.on_key>
Key(key='z', character='z', name='z', is_printable=True) >>> MRE(title='MRE', classes={'-dark-mode'}, pseudo_classes={'dark', 'focus'}) method=<App.on_key>
<action> namespace=MRE(title='MRE', classes={'-dark-mode'}, pseudo_classes={'dark', 'focus'}) action_name='toggle_console' params=()
Resize(size=Size(width=2, height=5), virtual_size=Size(width=255, height=5), container_size=Size(width=255, height=5)) >>> ScrollBar(name='vertical', window_virtual_size=0, window_size=5, position=0, thickness=2) method=None
Show() >>> ScrollBar(name='vertical', window_virtual_size=0, window_size=5, position=0, thickness=2) method=<Widget.on_show>
Resize(size=Size(width=255, height=6), virtual_size=Size(width=253, height=5), container_size=Size(width=255, height=5)) >>> RichLog() method=<RichLog.on_resize>
Show() >>> RichLog() method=<Widget.on_show>
Resize(size=Size(width=255, height=24), virtual_size=Size(width=255, height=24)) >>> VerticalScroll() method=None
Show() >>> ScrollBar(name='vertical', window_virtual_size=0, window_size=5, position=0, thickness=2) method=<Widget.on_show>
# Finally: Show and Resize events
Notes:
yield ProgressBar(total=100, classes='hidden')
This suggests Bar's indeterminate animation somehow triggers this issue. ... but adjusting the MRE so it uses Bar instead of ProgressBar actually removes the bug:
from textual.app import App
from textual.containers import VerticalScroll
from textual.widgets import Footer, Label, RichLog
from textual.widgets._progress_bar import Bar
class MRE(App):
BINDINGS = [("z", "toggle_console", "Console")]
CSS = """
RichLog { border-top: dashed blue; height: 6; }
.hidden { display: none; }
"""
def compose(self):
yield VerticalScroll()
yield Bar(classes='hidden')
yield Footer()
yield RichLog(classes='hidden')
def on_ready(self) -> None:
self.query_one('RichLog').write('\n'.join(f'line #{i}' for i in range(5)))
def action_toggle_console(self) -> None:
self.query_one('RichLog').toggle_class('hidden')
if __name__ == '__main__':
app = MRE()
app.run()
Otherly put, it has to be a ProgressBar, and it has to be in indeterminate mode.
FWIW, it looks like the result of a race condition related to the value of self.auto_refresh. I played with that value but still have a hard time getting relevant conclusions out of it. Apparently, auto_refresh is used only in Bar and LoadingIndicator.
Could this issue be a consequence of https://github.com/Textualize/textual/issues/4835? Edit: probably not: removing the condition in DOMNode.automatic_refresh() does not help.
Thanks for doing the legwork. Confirmed it is something to do with auto refresh.
Well that was fun. If a refresh (such as via auto_refresh) occurred at the point a widgets became visible, they could be missed when the layout is reflowed. Which resulted in no Resize message.
Very interesting. Could the same thing happen when a widget becomes invisible?
Also: how come auto_refresh induces a refresh for a hidden widget despite #4847?
Don't forget to star the repository!
Follow @textualizeio for Textual updates.
Potentially. I may have to do something similar with hidden widgets.
Potentially. I may have to do something similar with hidden widgets.
Would it translate into a rendering issue by chance?
from textual.app import App
from textual.containers import VerticalScroll
from textual.widgets import Footer, ProgressBar, RichLog, Placeholder
class MRE(App):
BINDINGS = [("z", "toggle('RichLog')", "Console"), ("x", "toggle('ProgressBar')", "Progress bar")]
CSS = """
Placeholder { height: 15; }
RichLog { border-top: dashed blue; height: 6; }
.hidden { display: none; }
"""
def compose(self):
with VerticalScroll():
for i in range(10):
yield Placeholder()
yield ProgressBar(classes='hidden')
yield RichLog(classes='hidden')
yield Footer()
def on_ready(self) -> None:
self.query_one('RichLog').write('\n'.join(f'line #{i}' for i in range(5)))
def action_toggle(self, widget_type) -> None:
self.query_one(widget_type).toggle_class('hidden')
if __name__ == '__main__':
app = MRE()
app.run()
As of Textual 0.80.0, RichLog delays actual rendering until it gets a Resize event, thus making the lack of such events visible.
This MRE showcases a seemingly nonsensical behaviour where a RichLog widget does not systematically receive Show and Resize events depending on the presence and visibility of a sibling ProgressBar widget:
Expectations
After hitting
z
, this MRE should display these lines at the bottom of the screen:Encountered behaviour
In practice, these lines:
z
multiple times (about 2 to 10 times) before showing upz Console
in the Footerhidden
)Early investigations noticed that, afer hitting
z
, Compositor.reflow() returned that no widget was hidden, no widget was shown and no widget was resized.