mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
simplify compose
This commit is contained in:
@@ -3,8 +3,8 @@ from textual.widgets import Welcome
|
||||
|
||||
|
||||
class WelcomeApp(App):
|
||||
def on_key(self) -> None:
|
||||
self.mount(Welcome())
|
||||
async def on_key(self) -> None:
|
||||
await self.mount(Welcome())
|
||||
|
||||
def on_button_pressed(self) -> None:
|
||||
self.exit()
|
||||
|
||||
@@ -183,7 +183,7 @@ Let's look at an example which looks up word definitions from an [api](https://d
|
||||
|
||||
=== "Output"
|
||||
|
||||
```{.textual path="docs/examples/events/dictionary.py" press="tab,t,e,x,t,_,_,_,_,_,_,_,_,_,_,_"}
|
||||
```{.textual path="docs/examples/events/dictionary.py" press="t,e,x,t,_,_,_,_,_,_,_,_,_,_,_"}
|
||||
```
|
||||
|
||||
Note the highlighted line in the above code which calls `asyncio.create_task` to run coroutine in the background. Without this you would find typing in to the text box to be unresponsive.
|
||||
|
||||
@@ -70,7 +70,7 @@ Textual is a framework for building applications that run within your terminal.
|
||||
```
|
||||
|
||||
|
||||
```{.textual path="docs/examples/events/dictionary.py" columns="100" lines="30" press="tab,_,t,e,x,t,_,_,_,_,_,_,_,_,_,_,_,_,_"}
|
||||
```{.textual path="docs/examples/events/dictionary.py" columns="100" lines="30" press="_,t,e,x,t,_,_,_,_,_,_,_,_,_,_,_,_,_"}
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,11 @@ class DictionaryApp(App):
|
||||
yield Input(placeholder="Search for a word")
|
||||
yield Content(Static(id="results"), id="results-container")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Called when app starts."""
|
||||
# Give the input focus, so we can start typing straight away
|
||||
self.query_one(Input).focus()
|
||||
|
||||
async def on_input_changed(self, message: Input.Changed) -> None:
|
||||
"""A coroutine to handle a text changed message."""
|
||||
if message.value:
|
||||
|
||||
35
sandbox/will/mount.py
Normal file
35
sandbox/will/mount.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from textual.app import App, ComposeResult
|
||||
|
||||
from textual.containers import Container
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Static
|
||||
|
||||
|
||||
class MountWidget(Widget):
|
||||
def on_mount(self) -> None:
|
||||
print("Widget mounted")
|
||||
|
||||
|
||||
class MountContainer(Container):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Container(MountWidget(id="bar"))
|
||||
|
||||
def on_mount(self) -> None:
|
||||
bar = self.query_one("#bar")
|
||||
print("MountContainer got", bar)
|
||||
|
||||
|
||||
class MountApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield MountContainer(id="foo")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
foo = self.query_one("#foo")
|
||||
print("foo is", foo)
|
||||
static = self.query_one("#bar")
|
||||
print("App got", static)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = MountApp()
|
||||
app.run()
|
||||
@@ -47,7 +47,7 @@ from .messages import CallbackType
|
||||
from .reactive import Reactive
|
||||
from .renderables.blank import Blank
|
||||
from .screen import Screen
|
||||
from .widget import Widget, _wait_for_mount
|
||||
from .widget import AwaitMount, Widget
|
||||
|
||||
PLATFORM = platform.system()
|
||||
WINDOWS = PLATFORM == "Windows"
|
||||
@@ -696,15 +696,15 @@ class App(Generic[ReturnType], DOMNode):
|
||||
self._require_stylesheet_update.add(self.screen if node is None else node)
|
||||
self.check_idle()
|
||||
|
||||
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> list[Widget]:
|
||||
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> AwaitMount:
|
||||
"""Mount widgets. Widgets specified as positional args, or keywords args. If supplied
|
||||
as keyword args they will be assigned an id of the key.
|
||||
|
||||
"""
|
||||
mounted_widgets = self._register(self.screen, *anon_widgets, **widgets)
|
||||
return mounted_widgets
|
||||
return AwaitMount(mounted_widgets)
|
||||
|
||||
def mount_all(self, widgets: Iterable[Widget]) -> list[Widget]:
|
||||
def mount_all(self, widgets: Iterable[Widget]) -> AwaitMount:
|
||||
"""Mount widgets from an iterable.
|
||||
|
||||
Args:
|
||||
@@ -713,7 +713,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
mounted_widgets = list(widgets)
|
||||
for widget in mounted_widgets:
|
||||
self._register(self.screen, widget)
|
||||
return mounted_widgets
|
||||
return AwaitMount(mounted_widgets)
|
||||
|
||||
def is_screen_installed(self, screen: Screen | str) -> bool:
|
||||
"""Check if a given screen has been installed.
|
||||
@@ -1011,14 +1011,13 @@ class App(Generic[ReturnType], DOMNode):
|
||||
self.set_interval(0.25, self.css_monitor, name="css monitor")
|
||||
self.log.system("[b green]STARTED[/]", self.css_monitor)
|
||||
|
||||
process_messages = super()._process_messages
|
||||
|
||||
async def run_process_messages():
|
||||
|
||||
compose_event = events.Compose(sender=self)
|
||||
await self._dispatch_message(compose_event)
|
||||
mount_event = events.Mount(sender=self)
|
||||
await self._dispatch_message(mount_event)
|
||||
try:
|
||||
await self._dispatch_message(events.Compose(sender=self))
|
||||
await self._dispatch_message(events.Mount(sender=self))
|
||||
finally:
|
||||
self._mounted_event.set()
|
||||
|
||||
Reactive._initialize_object(self)
|
||||
|
||||
@@ -1030,7 +1029,18 @@ class App(Generic[ReturnType], DOMNode):
|
||||
await self._ready()
|
||||
if ready_callback is not None:
|
||||
await ready_callback()
|
||||
await process_messages()
|
||||
|
||||
self._running = True
|
||||
|
||||
try:
|
||||
await self._process_messages_loop()
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
self._running = False
|
||||
for timer in list(self._timers):
|
||||
await timer.stop()
|
||||
|
||||
await self.animator.stop()
|
||||
await self._close_all()
|
||||
|
||||
@@ -1065,6 +1075,9 @@ class App(Generic[ReturnType], DOMNode):
|
||||
if self.devtools.is_connected:
|
||||
await self._disconnect_devtools()
|
||||
|
||||
async def _pre_process(self) -> None:
|
||||
pass
|
||||
|
||||
async def _ready(self) -> None:
|
||||
"""Called immediately prior to processing messages.
|
||||
|
||||
@@ -1091,8 +1104,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
|
||||
async def _on_compose(self) -> None:
|
||||
widgets = list(self.compose())
|
||||
self.mount_all(widgets)
|
||||
await _wait_for_mount(widgets)
|
||||
await self.mount_all(widgets)
|
||||
|
||||
def _on_idle(self) -> None:
|
||||
"""Perform actions when there are no messages in the queue."""
|
||||
|
||||
@@ -286,10 +286,12 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
def _start_messages(self) -> None:
|
||||
"""Start messages task."""
|
||||
self._task = asyncio.create_task(self._process_messages())
|
||||
self.post_message_no_wait(events.Compose(sender=self))
|
||||
|
||||
async def _process_messages(self) -> None:
|
||||
self._running = True
|
||||
|
||||
await self._pre_process()
|
||||
|
||||
try:
|
||||
await self._process_messages_loop()
|
||||
except CancelledError:
|
||||
@@ -299,6 +301,18 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
for timer in list(self._timers):
|
||||
await timer.stop()
|
||||
|
||||
async def _pre_process(self) -> None:
|
||||
"""Procedure to run before processing messages."""
|
||||
# Dispatch compose and mount messages without going through loop
|
||||
# These events must occur in this order, and at the start.
|
||||
try:
|
||||
await self._dispatch_message(events.Compose(sender=self))
|
||||
await self._dispatch_message(events.Mount(sender=self))
|
||||
finally:
|
||||
# This is critical, mount may be waiting
|
||||
self._mounted_event.set()
|
||||
Reactive._initialize_object(self)
|
||||
|
||||
async def _process_messages_loop(self) -> None:
|
||||
"""Process messages until the queue is closed."""
|
||||
_rich_traceback_guard = True
|
||||
@@ -326,9 +340,6 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
except MessagePumpClosed:
|
||||
break
|
||||
|
||||
is_mount = isinstance(message, events.Mount)
|
||||
if is_mount:
|
||||
Reactive._initialize_object(self)
|
||||
try:
|
||||
await self._dispatch_message(message)
|
||||
except CancelledError:
|
||||
@@ -338,8 +349,6 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
self.app._handle_exception(error)
|
||||
break
|
||||
finally:
|
||||
if is_mount:
|
||||
self._mounted_event.set()
|
||||
|
||||
self._message_queue.task_done()
|
||||
current_time = time()
|
||||
|
||||
@@ -4,7 +4,17 @@ from asyncio import Lock, wait, create_task
|
||||
from fractions import Fraction
|
||||
from itertools import islice
|
||||
from operator import attrgetter
|
||||
from typing import TYPE_CHECKING, ClassVar, Collection, Iterable, NamedTuple, cast
|
||||
from typing import (
|
||||
Awaitable,
|
||||
Generator,
|
||||
TYPE_CHECKING,
|
||||
ClassVar,
|
||||
Collection,
|
||||
Iterable,
|
||||
NamedTuple,
|
||||
Sequence,
|
||||
cast,
|
||||
)
|
||||
|
||||
import rich.repr
|
||||
from rich.console import (
|
||||
@@ -59,11 +69,26 @@ _JUSTIFY_MAP: dict[str, JustifyMethod] = {
|
||||
}
|
||||
|
||||
|
||||
async def _wait_for_mount(widgets: list[Widget]) -> None:
|
||||
"""Wait for widget to be mounted."""
|
||||
aws = [create_task(widget._mounted_event.wait()) for widget in widgets]
|
||||
if aws:
|
||||
await wait(aws)
|
||||
class AwaitMount:
|
||||
"""An awaitable returned by mount().
|
||||
|
||||
Example:
|
||||
await self.mount(Static("foo"))
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, widgets: Sequence[Widget]) -> None:
|
||||
self._widgets = widgets
|
||||
|
||||
def __await__(self) -> Generator[None, None, None]:
|
||||
async def await_mount() -> None:
|
||||
aws = [
|
||||
create_task(widget._mounted_event.wait()) for widget in self._widgets
|
||||
]
|
||||
if aws:
|
||||
await wait(aws)
|
||||
|
||||
return await_mount().__await__()
|
||||
|
||||
|
||||
class _Styled:
|
||||
@@ -323,7 +348,7 @@ class Widget(DOMNode):
|
||||
"""Clear arrangement cache, forcing a new arrange operation."""
|
||||
self._arrangement = None
|
||||
|
||||
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None:
|
||||
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> AwaitMount:
|
||||
"""Mount child widgets (making this widget a container).
|
||||
|
||||
Widgets may be passed as positional arguments or keyword arguments. If keyword arguments,
|
||||
@@ -335,7 +360,8 @@ class Widget(DOMNode):
|
||||
```
|
||||
|
||||
"""
|
||||
self.app._register(self, *anon_widgets, **widgets)
|
||||
mounted_widgets = self.app._register(self, *anon_widgets, **widgets)
|
||||
return AwaitMount(mounted_widgets)
|
||||
# self.app.screen.refresh(layout=True)
|
||||
# self.refresh(layout=True)
|
||||
|
||||
@@ -1878,9 +1904,7 @@ class Widget(DOMNode):
|
||||
|
||||
async def _on_compose(self, event: events.Compose) -> None:
|
||||
widgets = list(self.compose())
|
||||
self.mount(*widgets)
|
||||
await _wait_for_mount(widgets)
|
||||
await self.post_message(events.Mount(self))
|
||||
await self.mount(*widgets)
|
||||
|
||||
def _on_mount(self, event: events.Mount) -> None:
|
||||
if self.styles.overflow_y == "scroll":
|
||||
|
||||
Reference in New Issue
Block a user