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

Add support for "returning" a result value from screens. #2321

Merged
merged 8 commits into from
Apr 19, 2023

Conversation

davep
Copy link
Contributor

@davep davep commented Apr 18, 2023

This is still in the "let's see if we like this approach" stage, although I think @willmcgugan and myself are liking this approach. Testing and review of these changes is highly encouraged. Beyond docstrings there are no documentation changes at the moment. This will need a slight reworking of the Screen guide so it's better that we're happy with the approach and the code before we write this up fully.

Please review without worrying about documentation for now.

This PR seeks to satisfy #2267 and does take the suggested callback approach. The alternative was to go with a "use messages" story that, while working well, places more work on the developer using Textual and potentially results in more boilerplate.

Changes of note here are:

  • Much like with App, a Screen can now be typed. The type signifies the type of the result that it will "return".
  • App.push_screen has acquired a callback parameter. If provided the callback function should be a function that takes a single argument, that is the type of the type given to the screen being pushed.
  • Screen has acquired a new method called dismiss. This is a thin wrapper around App.pop_screen. Called with no parameter it performs the same as if App.pop_screen were called. If called with a parameter the callback provided with App.push_screen is called and then the screen is popped.
  • Screen has also acquired an action_dismiss method to allow for easy dismissal of a screen from within an action.

Other things to keep in mind:

  • A callback can optionally be awaitable.
  • Because any given instance of a screen can end up on the screen stack multiple times, the result callback hook is done as a stack.

As an illustration of how this might be used, here's a small program that uses a ModalScreen to ask the user a question and then act on their answer:

from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical
from textual.screen import ModalScreen
from textual.widgets import Header, Footer, OptionList, Button, Label


class AyeNaw(ModalScreen[bool]):
    DEFAULT_CSS = """
    AyeNaw {
        align: center middle;
    }

    AyeNaw > Vertical {
        background: $secondary;
        width: auto;
        height: auto;
        border: thick $primary;
        padding: 2 4;
    }

    AyeNaw > Vertical > * {
        width: auto;
        height: auto;
    }

    AyeNaw > Vertical > Label {
        padding-bottom: 2;
    }

    AyeNaw > Vertical > Horizontal {
        align: right middle;
    }

    AyeNaw Button {
        margin-left: 2;
    }
    """

    def compose(self) -> ComposeResult:
        with Vertical():
            yield Label("Well, what do you think?")
            with Horizontal():
                yield Button("Aye!", id="aye")
                yield Button("Naw!", id="naw")

    def on_button_pressed(self, event: Button.Pressed) -> None:
        self.dismiss(result=event.button.id == "aye")


class ScreenResultExampleApp(App[None]):
    BINDINGS = [
        ("question_mark", "ask", "Ask a question"),
    ]

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

    def accept_answer(self, answer: bool) -> None:
        self.query_one(OptionList).add_option(
            f"Computer said {'[green]Aye' if answer else '[red]Naw'}[/]"
        )

    def action_ask(self) -> None:
        self.push_screen(AyeNaw(), callback=self.accept_answer)


if __name__ == "__main__":
    ScreenResultExampleApp().run()
Screen.Recording.2023-04-18.at.14.44.50.mov

This is roughly how it should work. Having got this going and constructed
test code to go with it (outwith of this commit, not unit testing code, just
a test app to try out the ideas), I wanted to get this onto the forge for
further mulling over tomorrow.

The one sneaky/questionable thing here is that I'm sort of dumpster-diving
the screen stack to get the "parent" screen, to make the callback in
context. This both feels right and feels like a cheat. On the other hand
it's public for a reason, right?

Right?
It is possible for the same instance of a screen to get pushed onto the
screen stack multiple times; as such we really need to keep track of all the
callback requests.

So here I register a callback for every screen push and clean it up on every
screen pop; with those without callbacks being no-ops.
Under normal circumstances the code wouldn't encounter this problem as
there's always a default screen; but a handful of tests that were testing
the screen stack broke after the recent additions relating to result
callbacks.

This cleans up that problem.
Just go with a single dismiss method.
@davep davep added enhancement New feature or request Task labels Apr 18, 2023
@davep davep marked this pull request as ready for review April 19, 2023 10:16
@davep davep changed the title WiP: Add support for "returning" a result value from screens. Add support for "returning" a result value from screens. Apr 19, 2023
Copy link
Collaborator

@willmcgugan willmcgugan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we will need to discard callbacks for switch screens.

src/textual/app.py Outdated Show resolved Hide resolved
davep and others added 2 commits April 19, 2023 11:39
Co-authored-by: Will McGugan <willmcgugan@gmail.com>
@davep
Copy link
Contributor Author

davep commented Apr 19, 2023

I think we will need to discard callbacks for switch screens.

@willmcgugan Good call. Done: 9123a80

@davep davep requested a review from willmcgugan April 19, 2023 10:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request Task
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants