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

Implement mode concept #2327

Closed
willmcgugan opened this issue Apr 19, 2023 · 6 comments · Fixed by #2540
Closed

Implement mode concept #2327

willmcgugan opened this issue Apr 19, 2023 · 6 comments · Fixed by #2540
Assignees
Labels
enhancement New feature or request Task

Comments

@willmcgugan
Copy link
Collaborator

willmcgugan commented Apr 19, 2023

The switch_screen method doesn't fit with the stack of screens concept.

It was introduced to provide switchable modes in an app, but doesn't quite capture that. I think what we need is a concept of "modes" where each mode has it's own independant stack of screens. The app can switch between these modes as required.

Suggest we have a MODES class var similar to screens, which should be a sequence of modes.

Something like this:

MODES = ["calculator", "help", "error", "sign-up"]

Each of those modes has an associated stack of screens.

App would have a switch_mode method which changes to the given mode. Modes could also be created dynamically, with add_mode and remove_mode.

I'm hoping this could be implemented without much impact on the rest of the code. But there may be edge cases.

If you tackle this, please check with @willmcgugan

@davep davep added enhancement New feature or request Task labels Apr 19, 2023
@willmcgugan willmcgugan changed the title Consider replacing switch with mode concept Implement mode concept May 9, 2023
@rodrigogiraoserrao rodrigogiraoserrao self-assigned this May 10, 2023
@rodrigogiraoserrao
Copy link
Contributor

rodrigogiraoserrao commented May 10, 2023

#2540 adds a possible implementation that follows your instructions to the letter.
However, I suspect you might be thinking of things that aren't clear from the description alone.

  • For example, is the point of add_mode just to add a new mode to the set of modes available?
  • Does each custom mode get a default screen at the base of the stack?
  • Should the user be able to set up a mode with a custom default screen? As it stands, when you switch to a mode for the first time, it shows a default empty screen.

The app below lets you play with modes.
Press 1 and 2 to switch between modes 1 and 2.
Press P to push random fruit modals into the screen.
Press the letter O to pop a screen from the stack.

Demo code.
from random import choice

from textual.app import App, ComposeResult
from textual.screen import Screen, ModalScreen
from textual.widgets import Footer, Header, Label


FRUITS = "apple mango strawberry banana peach pear melon watermelon".split()


class BaseScreen(Screen[None]):
    BINDINGS = [
        ("1", "one", "Mode 1"),
        ("2", "two", "Mode 2"),
        ("p", "push", "Push rnd scrn"),
        ("o", "pop_screen", "Pop"),
        ("r", "remove", "Remove mode 1"),
    ]

    def __init__(self, label):
        super().__init__()
        self.label = label

    def compose(self) -> ComposeResult:
        yield Header()
        yield Label(self.label)
        yield Footer()

    def action_one(self) -> None:
        self.app.switch_mode("one")

    def action_two(self) -> None:
        self.app.switch_mode("two")

    def action_push(self) -> None:
        self.app.push_screen(FruitModal())

    def action_remove(self) -> None:
        self.app.remove_mode("one")


class FruitModal(ModalScreen[None]):
    BINDINGS = [
        ("1", "one", "Mode 1"),
        ("2", "two", "Mode 2"),
        ("p", "push", "Push rnd scrn"),
        ("o", "pop_screen", "Pop"),
    ]

    def compose(self) -> ComposeResult:
        yield Label(choice(FRUITS))

    def action_one(self) -> None:
        self.app.switch_mode("one")

    def action_two(self) -> None:
        self.app.switch_mode("two")

    def action_push(self) -> None:
        self.app.push_screen(FruitModal())


class ModesApp(App[None]):
    MODES = ["one", "two"]

    def on_mount(self) -> None:
        self.switch_mode("two")
        self.push_screen(BaseScreen("two"))
        self.switch_mode("one")
        self.push_screen(BaseScreen("one"))


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

@willmcgugan
Copy link
Collaborator Author

Don't think of this as interpreting what Will wants. I'd like you to design the API.

You've identified problems with the proposed API, which is great. But I'd like you to propose and implement solutions in the spirit of the rest of Textual. So simple, declarative, with good error handling.

It will likely be an iterative process. It will take a few back and forth attempts before it is finalized.

@rodrigogiraoserrao
Copy link
Contributor

rodrigogiraoserrao commented May 11, 2023

@willmcgugan understood.

I like the new version of the API better:

  1. user specifies modes and base screens for each mode (the screen at the base of the stack);
  2. add_mode takes a mode name and a base screen for that mode;
  3. remove_mode and switch_mode stay the same.
class HelpScreen(Screen[None]):
    ...

class MainAppScreen(Screen[None]):
    ...

class MyApp(App[None]):
    MODES = {
        "default": "main",
        "help": HelpScreen,
    }

    SCREENS = {
        "main": MainAppScreen,
    }

    ...
Previous app rewritten with this API.
from random import choice

from textual.app import App, ComposeResult
from textual.screen import Screen, ModalScreen
from textual.widgets import Footer, Header, Label


FRUITS = "apple mango strawberry banana peach pear melon watermelon".split()


class BaseScreen(Screen[None]):
    BINDINGS = [
        ("1", "one", "Mode 1"),
        ("2", "two", "Mode 2"),
        ("p", "push", "Push rnd scrn"),
        ("o", "pop_screen", "Pop"),
        ("r", "remove", "Remove mode 1"),
    ]

    def __init__(self, label):
        super().__init__()
        self.label = label

    def compose(self) -> ComposeResult:
        yield Header()
        yield Label(self.label)
        yield Footer()

    def action_one(self) -> None:
        self.app.switch_mode("one")

    def action_two(self) -> None:
        self.app.switch_mode("two")

    def action_push(self) -> None:
        self.app.push_screen(FruitModal())

    def action_remove(self) -> None:
        self.app.remove_mode("one")


class FruitModal(ModalScreen[None]):
    BINDINGS = [
        ("1", "one", "Mode 1"),
        ("2", "two", "Mode 2"),
        ("p", "push", "Push rnd scrn"),
        ("o", "pop_screen", "Pop"),
    ]

    def compose(self) -> ComposeResult:
        yield Label(choice(FRUITS))

    def action_one(self) -> None:
        self.app.switch_mode("one")

    def action_two(self) -> None:
        self.app.switch_mode("two")

    def action_push(self) -> None:
        self.app.push_screen(FruitModal())


class ModesApp(App[None]):
    MODES = {
        "one": lambda: BaseScreen("one"),
        "two": lambda: BaseScreen("two"),
    }

    def on_mount(self) -> None:
        self.switch_mode("one")


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

@willmcgugan
Copy link
Collaborator Author

Liking the look of this! Will dig in to it when I’m back.

@willmcgugan
Copy link
Collaborator Author

@rodrigogiraoserrao Yes, I do like this API. Make it so.

@github-actions
Copy link

Don't forget to star the repository!

Follow @textualizeio for Textual updates.

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 a pull request may close this issue.

3 participants