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

Footer binding disappear #5153

Open
mzebrak opened this issue Oct 22, 2024 · 3 comments
Open

Footer binding disappear #5153

mzebrak opened this issue Oct 22, 2024 · 3 comments

Comments

@mzebrak
Copy link

mzebrak commented Oct 22, 2024

I encountered a weird situation when bindings were missing in the footer.
Also, ctrl+c doesn't work until they appear again, however, the UI keeps refreshing.
Pressing a button / resizing the terminal causes everything to work properly again.

Version: 0.83.0.

Reproduction steps:

  • press A
  • press S
  • <bindings visible>
  • press A
  • press S
  • <missing bindings>
  • resize / click outside the terminal / press button
  • <bindings visible>

MRE:

from __future__ import annotations

import uuid
from typing import cast

from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Horizontal
from textual.reactive import var
from textual.screen import Screen
from textual.widget import Widget
from textual.widgets import Button, Footer, Label


class AddNewElementScreen(Screen):
    BINDINGS = [
        Binding("escape", "dismiss()", "Back"),
        Binding("s", "save", "Save a new element"),
    ]

    def compose(self) -> ComposeResult:
        yield Label("Press 's' to save a new element")
        yield Footer()

    def action_save(self) -> None:
        app = cast(MyApp, self.app)
        app.elements.append(uuid.uuid4().hex)
        app.mutate_reactive(app.__class__.elements)
        self.dismiss()


class Elements(Widget):
    DEFAULT_CSS = """
    Elements {
        height: auto;

        Horizontal {
            height: auto;
            background: $primary-background;

            Label {
                padding: 1;
            }
        }
    }
    """

    def compose(self) -> ComposeResult:
        yield Label("Press 'a' to add a new element")

    def on_mount(self) -> None:
        self.watch(self.app, "elements", self.remount_elements, init=False)

    async def remount_elements(self) -> None:
        with self.app.batch_update():
            await self.remove_children()  # this is crucial for the bug, bug does not occur when no children are removed
            await self.mount_all(self.create_elements())

    def create_elements(self) -> list[Widget]:
        app = cast(MyApp, self.app)
        return [  # has to include a Button, when no Button is included, the bug does not occur
            Horizontal(Label(element), Button("1")) for element in app.elements
        ]


class SomeScreen(Screen):
    BINDINGS = [
        Binding("q", "quit", "Quit"),
        Binding("a", "add_new_element", "Add new element"),
    ]

    def compose(self) -> ComposeResult:
        yield Elements()
        yield Footer()

    def action_add_new_element(self) -> None:
        self.app.push_screen(AddNewElementScreen())


class MyApp(App):
    BINDINGS = [
        Binding("q", "quit", "Quit"),
    ]

    elements: var[list[str]] = var([], init=False)

    def on_mount(self) -> None:
        self.push_screen(SomeScreen())

    def action_quit(self) -> None:
        self.exit()


MyApp().run()
Screencast.from.10-22-2024.12.42.55.PM.webm

What is really weird is that when no Button is mounted - everything works perfectly fine:

Screencast.from.10-22-2024.12.52.39.PM.webm
Copy link

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

@willmcgugan
Copy link
Collaborator

Would you mind attempting to make that MRE more minimal? i.e. the simplest possible code that causes the issue. Remove all code that isn't essential in reproducing the issue.

@mzebrak
Copy link
Author

mzebrak commented Oct 22, 2024

From 94 to 58 - I guess this is all I can get keeping the readability.
The bug does not trigger when the remove and mount_all happens on a single screen without a reactive update.
I also think the dismiss is crucial - otherwise, there is no need to update the bindings?

from __future__ import annotations

import uuid
from typing import cast

from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Horizontal
from textual.reactive import var
from textual.screen import Screen
from textual.widget import Widget
from textual.widgets import Button, Footer, Label


class AddNewElementScreen(Screen):
    BINDINGS = [Binding("s", "save", "Save a new element")]

    def compose(self) -> ComposeResult:
        yield Label("Press 's' to save a new element")
        yield Footer()

    def action_save(self) -> None:
        app = cast(MyApp, self.app)
        app.elements.append(uuid.uuid4().hex)
        app.mutate_reactive(app.__class__.elements)
        self.dismiss()


class Elements(Widget):
    def on_mount(self) -> None:
        self.watch(self.app, "elements", self.remount_elements, init=False)

    async def remount_elements(self) -> None:
        with self.app.batch_update():
            await self.remove_children()  # this is crucial for the bug, bug does not occur when no children are removed
            await self.mount_all(self.create_elements())

    def create_elements(self) -> list[Widget]:
        app = cast(MyApp, self.app)
        return [  # has to include a Button, when no Button is included, the bug does not occur
            Horizontal(Label(element), Button("1")) for element in app.elements
        ]


class MyApp(App):
    BINDINGS = [Binding("a", "add_new_element", "Add new element")]

    elements: var[list[str]] = var([], init=False)

    def compose(self) -> ComposeResult:
        yield Elements()
        yield Footer()

    def action_add_new_element(self) -> None:
        self.app.push_screen(AddNewElementScreen())


MyApp().run()

--- EDIT
Here it also occurs:

from __future__ import annotations

from typing import cast

from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.reactive import var
from textual.screen import Screen
from textual.widgets import Button, Footer, Label


class TriggerUpdateScreen(Screen):
    BINDINGS = [Binding("t", "trigger_update", "Trigger the reactive update")]

    def compose(self) -> ComposeResult:
        yield Label("Press 't' to trigger the reactive update")
        yield Footer()

    def action_trigger_update(self) -> None:
        app = cast(MyApp, self.app)
        app.trigger_update = not app.trigger_update
        self.dismiss()


class MyApp(App):
    BINDINGS = [Binding("t", "trigger_update_via_new_screen", "Trigger update via new screen")]

    trigger_update = var(False, init=False)

    def compose(self) -> ComposeResult:
        yield Footer()

    def on_mount(self) -> None:
        self.watch(self, "trigger_update", self.remount_button, init=False)

    async def remount_button(self) -> None:
        with self.batch_update():
            # has to be a Button, bug does not occur with Label. Also removing the button before mounting again is crucial
            await self.query(Button).remove()
            await self.mount(Button("anything"))

    def action_trigger_update_via_new_screen(self) -> None:
        self.app.push_screen(TriggerUpdateScreen())


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