Initial testing of screen result callbacks

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?
This commit is contained in:
Dave Pearson
2023-04-17 21:03:49 +01:00
parent e930e82526
commit b67f4f89cc
2 changed files with 51 additions and 5 deletions

View File

@@ -92,7 +92,7 @@ from .keys import (
from .messages import CallbackType
from .reactive import Reactive
from .renderables.blank import Blank
from .screen import Screen
from .screen import Screen, ScreenResultCallbackType
from .widget import AwaitMount, Widget
if TYPE_CHECKING:
@@ -1379,7 +1379,9 @@ class App(Generic[ReturnType], DOMNode):
self.log.system(f"{screen} REMOVED")
return screen
def push_screen(self, screen: Screen | str) -> AwaitMount:
def push_screen(
self, screen: Screen | str, callback: ScreenResultCallbackType | None = None
) -> AwaitMount:
"""Push a new [screen](/guide/screens) on the screen stack, making it the current screen.
Args:
@@ -1395,6 +1397,7 @@ class App(Generic[ReturnType], DOMNode):
self.screen.post_message(events.ScreenSuspend())
self.screen.refresh()
next_screen, await_mount = self._get_screen(screen)
next_screen._result_callback = callback
self._screen_stack.append(next_screen)
next_screen.post_message(events.ScreenResume())
self.log.system(f"{self.screen} is current (PUSHED)")

View File

@@ -6,7 +6,17 @@ The `Screen` class is a special widget which represents the content in the termi
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable, Iterator
from inspect import isawaitable
from typing import (
TYPE_CHECKING,
Awaitable,
Callable,
Generic,
Iterable,
Iterator,
TypeVar,
Union,
)
import rich.repr
from rich.console import RenderableType
@@ -34,9 +44,17 @@ if TYPE_CHECKING:
# Screen updates will be batched so that they don't happen more often than 120 times per second:
UPDATE_PERIOD: Final[float] = 1 / 120
ScreenResultType = TypeVar("ScreenResultType")
"""The result type of a screen."""
ScreenResultCallbackType = Union[
Callable[[ScreenResultType], None], Callable[[ScreenResultType], Awaitable[None]]
]
"""Type of a screen result callback function."""
@rich.repr.auto
class Screen(Widget):
class Screen(Generic[ScreenResultType], Widget):
"""The base class for screens."""
DEFAULT_CSS = """
@@ -77,6 +95,7 @@ class Screen(Widget):
self._dirty_widgets: set[Widget] = set()
self.__update_timer: Timer | None = None
self._callbacks: list[CallbackType] = []
self._result_callback: ScreenResultCallbackType | None = None
@property
def is_modal(self) -> bool:
@@ -643,9 +662,33 @@ class Screen(Widget):
else:
self.post_message(event)
def dismiss(self) -> None:
"""Dismiss the screen."""
self.app.pop_screen()
def action_dismiss(self) -> None:
self.dismiss()
def dismiss_with(self, result: ScreenResultType) -> None:
"""Dismiss the screen with the given result.
Args:
result: The result to be passed to the result callback.
"""
if self._result_callback is not None:
try:
callback_screen = self.app.screen_stack[-2]
except IndexError:
callback_screen = self
callback_screen.call_next(self._result_callback, result)
self.dismiss()
def action_dismiss_with(self, result: ScreenResultType) -> None:
self.dismiss_with(result)
@rich.repr.auto
class ModalScreen(Screen):
class ModalScreen(Screen[ScreenResultType]):
"""A screen with bindings that take precedence over the App's key bindings.
The default styling of a modal screen will dim the screen underneath.