Skip to content

Collapsible: title formatting is not updated unless title text actually changes #5786

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

Open
2 tasks done
xavierog opened this issue May 4, 2025 · 2 comments
Open
2 tasks done

Comments

@xavierog
Copy link
Contributor

xavierog commented May 4, 2025

The bug

MRE

#!/usr/bin/env python3
from textual.app import App, ComposeResult
from textual.widgets import Label, Collapsible

INSTRUCTIONS = """[u]Instructions:[/u]
1. Wait for 2 seconds
2. The label inside the collapsible changes, this is good
3. The collapsible title remains unchanged, this is problematic
4. Wait for 2 seconds again
5. Both label and title change.
"""
COLLAPSIBLE_TITLE_1 = '[#ff0000]title[/]'
COLLAPSIBLE_TITLE_2 = '[#00ff00]title[/]' # color change
COLLAPSIBLE_TITLE_3 = '[#00ff00]TITLE[/]' # color and text change

class Textual3Collapsible(App):
	def compose(self) -> ComposeResult:
		yield Label(INSTRUCTIONS)
		with Collapsible(title=COLLAPSIBLE_TITLE_1, collapsed=False):
			yield Label(COLLAPSIBLE_TITLE_1)

	def change(self, new_title) -> None:
		self.query_one('Collapsible', Collapsible).title = new_title
		self.query_one('Collapsible Label', Label).update(new_title)

	def on_ready(self) -> None:
		self.set_timer(2, self.change_1)

	def change_1(self) -> None:
		self.change(COLLAPSIBLE_TITLE_2)
		self.set_timer(2, self.change_2)

	def change_2(self) -> None:
		self.change(COLLAPSIBLE_TITLE_3)


if __name__ == "__main__":
	app = Textual3Collapsible()
	app.run()

At t=0: ok:
Image

At t=2: not ok, the collapsible title remains unchanged:
Image

At t=4: ok, the collapsible title changed
Image

Additionally, Collapsible's title reactive is a string whereas CollapsibleTitle's label reactive is a ContentText created using Content.from_text(). It would be nice if we could pass a ContentText to Collapsible without vexing mypy.
Alternatively, being able to import CollapsibleTitle would allow mypy to understand what is happening when working on a CollapsibleTitle widget obtained through query_one().

Textual Diagnostics

Versions

Name Value
Textual 3.2.0
Rich 14.0.0

Python

Name Value
Version 3.13.3
Implementation CPython
Compiler GCC 14.2.0
Executable /path/to/my/venv/bin/python3

Operating System

Name Value
System Linux
Release 6.12.25-amd64
Version #1 SMP PREEMPT_DYNAMIC Debian 6.12.25-1 (2025-04-25)

Terminal

Name Value
Terminal Application tmux (3.5a)
TERM tmux-256color
COLORTERM truecolor
FORCE_COLOR Not set
NO_COLOR Not set

Rich Console options

Name Value
size width=106, height=57
legacy_windows False
min_width 1
max_width 106
is_terminal False
encoding utf-8
max_height 57
justify None
overflow None
no_wrap False
highlight None
markup None
height None
Copy link

github-actions bot commented May 4, 2025

Thank you for your issue. Give us a little time to review it.

PS. You might want to check the FAQ if you haven't done so already.

This is an automated reply, generated by FAQtory

@TomJGooding
Copy link
Contributor

TomJGooding commented May 7, 2025

The problem is that Content objects are defined as equal if they have the same plain text, so because the reactive value hasn't changed the watch method is never called.

It looks like there's a similar problem in the Button (and maybe other widgets?) where the "smart refresh" isn't invoked when the new content only updates the style.

I'm not sure what the correct fix is here, perhaps these reactives need marking as always_update=True?

from textual.app import App, ComposeResult
from textual.widgets import Button, Static

CONTENT_PLAIN = "Update styles"
CONTENT_MARKUP = f"[magenta on yellow]{CONTENT_PLAIN}[/]"


class ExampleApp(App):
    def compose(self) -> ComposeResult:
        yield Button(CONTENT_PLAIN)
        yield Static(CONTENT_PLAIN)

    def on_button_pressed(self) -> None:
        # This works...
        self.query_one(Static).update(CONTENT_MARKUP)
        # ...but this doesn't
        self.query_one(Button).label = CONTENT_MARKUP


if __name__ == "__main__":
    app = ExampleApp()
    app.run()

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

No branches or pull requests

2 participants