Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New widgets being ignored #5024

Closed
xavierog opened this issue Sep 20, 2024 · 11 comments · Fixed by #5048
Closed

New widgets being ignored #5024

xavierog opened this issue Sep 20, 2024 · 11 comments · Fixed by #5048

Comments

@xavierog
Copy link
Contributor

xavierog commented Sep 20, 2024

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:

from textual.app import App
from textual.containers import VerticalScroll
from textual.widgets import Footer, ProgressBar, RichLog

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 ProgressBar(classes='hidden') # removing or displaying this widget prevents the bug
		yield Footer() # clicking "Console" in the footer prevents the bug
		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()

Expectations

After hitting z, this MRE should display these lines at the bottom of the screen:

line #0
line #1
line #2
line #3
line #4

mre-ok

Encountered behaviour

In practice, these lines:

  • sometimes appear as expected
  • sometimes require pressing z multiple times (about 2 to 10 times) before showing up
  • always appear when clicking z Console in the Footer
  • always appear when the seemingly unrelated ProgressBar is removed (just comment out line 13) or displayed (remove hidden)

mre-fail

Early investigations noticed that, afer hitting z, Compositor.reflow() returned that no widget was hidden, no widget was shown and no widget was resized.

Copy link

We found the following entry in the FAQ which you may find helpful:

Feel free to close this issue if you found an answer in the FAQ. Otherwise, please give us a little time to review.

This is an automated reply, generated by FAQtory

@xavierog
Copy link
Contributor Author

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

@xavierog
Copy link
Contributor Author

xavierog commented Sep 20, 2024

Notes:

  • if the ProgressBar is replaced with a Label, the program works fine.
  • if the ProgressBar is given a total in compose(), the program works fine:
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.

@xavierog
Copy link
Contributor Author

xavierog commented Sep 23, 2024

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 #4835?
Edit: probably not: removing the condition in DOMNode.automatic_refresh() does not help.

xavierog added a commit to xavierog/moulti that referenced this issue Sep 24, 2024
This change prevents a bug introduced in Textual 0.80.0 that affects the
console when it becomes visible for the first time:
Textualize/textual#5024
@willmcgugan
Copy link
Collaborator

Thanks for doing the legwork. Confirmed it is something to do with auto refresh.

@willmcgugan willmcgugan changed the title Weird MRE: hidden ProgressBar seemingly prevents RichLog Resize+Show events New widgets being ignored Sep 24, 2024
@willmcgugan
Copy link
Collaborator

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.

@xavierog
Copy link
Contributor Author

Very interesting. Could the same thing happen when a widget becomes invisible?

@xavierog
Copy link
Contributor Author

Also: how come auto_refresh induces a refresh for a hidden widget despite #4847?

Copy link

Don't forget to star the repository!

Follow @textualizeio for Textual updates.

@willmcgugan
Copy link
Collaborator

Potentially. I may have to do something similar with hidden widgets.

@xavierog
Copy link
Contributor Author

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()

issue-5024-is-back

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants