mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
fix stuck screen
This commit is contained in:
@@ -1,9 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from functools import lru_cache
|
||||
|
||||
from inspect import signature, isawaitable
|
||||
from typing import Any, Callable
|
||||
from typing import Any, Callable, TYPE_CHECKING
|
||||
|
||||
from . import active_app
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .app import App
|
||||
|
||||
# Maximum seconds before warning about a slow callback
|
||||
INVOKE_TIMEOUT_WARNING = 3
|
||||
|
||||
|
||||
@lru_cache(maxsize=2048)
|
||||
@@ -12,7 +20,7 @@ def count_parameters(func: Callable) -> int:
|
||||
return len(signature(func).parameters)
|
||||
|
||||
|
||||
async def invoke(callback: Callable, *params: object) -> Any:
|
||||
async def _invoke(callback: Callable, *params: object) -> Any:
|
||||
"""Invoke a callback with an arbitrary number of parameters.
|
||||
|
||||
Args:
|
||||
@@ -27,3 +35,38 @@ async def invoke(callback: Callable, *params: object) -> Any:
|
||||
if isawaitable(result):
|
||||
result = await result
|
||||
return result
|
||||
|
||||
|
||||
async def invoke(callback: Callable, *params: object) -> Any:
|
||||
"""Invoke a callback with an arbitrary number of parameters.
|
||||
|
||||
Args:
|
||||
callback: The callable to be invoked.
|
||||
|
||||
Returns:
|
||||
The return value of the invoked callable.
|
||||
"""
|
||||
|
||||
app: App | None
|
||||
try:
|
||||
app = active_app.get()
|
||||
except LookupError:
|
||||
app = None
|
||||
|
||||
if app is not None and "debug" in app.features:
|
||||
# In debug mode we will warn about callbacks that may be stuck
|
||||
def log_slow() -> None:
|
||||
"""Log a message regarding a slow callback."""
|
||||
app.log.warning(
|
||||
f"Callback {callback} is still pending after {INVOKE_TIMEOUT_WARNING} seconds"
|
||||
)
|
||||
|
||||
call_later_handle = asyncio.get_running_loop().call_later(
|
||||
INVOKE_TIMEOUT_WARNING, log_slow
|
||||
)
|
||||
try:
|
||||
return await _invoke(callback, *params)
|
||||
finally:
|
||||
call_later_handle.cancel()
|
||||
else:
|
||||
return await _invoke(callback, *params)
|
||||
|
||||
@@ -1191,9 +1191,13 @@ class App(Generic[ReturnType], DOMNode):
|
||||
_screen = self.get_screen(screen)
|
||||
if not _screen.is_running:
|
||||
widgets = self._register(self, _screen)
|
||||
return (_screen, AwaitMount(_screen, widgets))
|
||||
await_mount = AwaitMount(_screen, widgets)
|
||||
self.call_next(await_mount)
|
||||
return (_screen, await_mount)
|
||||
else:
|
||||
return (_screen, AwaitMount(_screen, []))
|
||||
await_mount = AwaitMount(_screen, [])
|
||||
self.call_next(await_mount)
|
||||
return (_screen, await_mount)
|
||||
|
||||
def _replace_screen(self, screen: Screen) -> Screen:
|
||||
"""Handle the replaced screen.
|
||||
@@ -2013,6 +2017,12 @@ class App(Generic[ReturnType], DOMNode):
|
||||
await self._close_messages()
|
||||
|
||||
async def _on_resize(self, event: events.Resize) -> None:
|
||||
# print(self.screen, self._screen_stack)
|
||||
# print(self.screen.size)
|
||||
# print(self.screen.is_running)
|
||||
# print((self.screen._message_queue))
|
||||
# print(self.screen._active_message)
|
||||
# print(self.screen._task)
|
||||
event.stop()
|
||||
await self.screen.post_message(event)
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ class MessagePumpMeta(type):
|
||||
class MessagePump(metaclass=MessagePumpMeta):
|
||||
def __init__(self, parent: MessagePump | None = None) -> None:
|
||||
self._message_queue: Queue[Message | None] = Queue()
|
||||
self._active_nessage: Message | None = None
|
||||
self._parent = parent
|
||||
self._running: bool = False
|
||||
self._closing: bool = False
|
||||
@@ -75,6 +76,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
self._last_idle: float = time()
|
||||
self._max_idle: float | None = None
|
||||
self._mounted_event = asyncio.Event()
|
||||
self._next_callbacks: list[CallbackType] = []
|
||||
|
||||
@property
|
||||
def task(self) -> Task:
|
||||
@@ -271,10 +273,22 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
|
||||
Args:
|
||||
callback: Callable to call next.
|
||||
*args: Positional arguments to pass to the callable.
|
||||
**kwargs: Keyword arguments to pass to the callable.
|
||||
"""
|
||||
message = events.Callback(self, callback=partial(callback, *args, **kwargs))
|
||||
self.post_message_no_wait(message)
|
||||
|
||||
def call_next(self, callback: Callable, *args, **kwargs) -> None:
|
||||
"""Schedule a callback to run immediately after processing the current message.
|
||||
|
||||
Args:
|
||||
callback: Callable to run after current event.
|
||||
*args: Positional arguments to pass to the callable.
|
||||
**kwargs: Keyword arguments to pass to the callable.
|
||||
"""
|
||||
self._next_callbacks.append(partial(callback, *args, **kwargs))
|
||||
|
||||
def _on_invoke_later(self, message: messages.InvokeLater) -> None:
|
||||
# Forward InvokeLater message to the Screen
|
||||
self.app.screen._invoke_later(message.callback)
|
||||
@@ -367,35 +381,52 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
except MessagePumpClosed:
|
||||
break
|
||||
|
||||
self._active_message = message
|
||||
|
||||
try:
|
||||
await self._dispatch_message(message)
|
||||
except CancelledError:
|
||||
raise
|
||||
try:
|
||||
await self._dispatch_message(message)
|
||||
except CancelledError:
|
||||
raise
|
||||
except Exception as error:
|
||||
self._mounted_event.set()
|
||||
self.app._handle_exception(error)
|
||||
break
|
||||
finally:
|
||||
|
||||
self._message_queue.task_done()
|
||||
|
||||
current_time = time()
|
||||
|
||||
# Insert idle events
|
||||
if self._message_queue.empty() or (
|
||||
self._max_idle is not None
|
||||
and current_time - self._last_idle > self._max_idle
|
||||
):
|
||||
self._last_idle = current_time
|
||||
if not self._closed:
|
||||
event = events.Idle(self)
|
||||
for _cls, method in self._get_dispatch_methods(
|
||||
"on_idle", event
|
||||
):
|
||||
try:
|
||||
await invoke(method, event)
|
||||
except Exception as error:
|
||||
self.app._handle_exception(error)
|
||||
break
|
||||
finally:
|
||||
self._active_message = None
|
||||
|
||||
async def _flush_next_callbacks(self) -> None:
|
||||
"""Invoke pending callbacks in next callbacks queue."""
|
||||
callbacks = self._next_callbacks.copy()
|
||||
self._next_callbacks.clear()
|
||||
for callback in callbacks:
|
||||
try:
|
||||
await invoke(callback)
|
||||
except Exception as error:
|
||||
self._mounted_event.set()
|
||||
self.app._handle_exception(error)
|
||||
break
|
||||
finally:
|
||||
|
||||
self._message_queue.task_done()
|
||||
current_time = time()
|
||||
|
||||
# Insert idle events
|
||||
if self._message_queue.empty() or (
|
||||
self._max_idle is not None
|
||||
and current_time - self._last_idle > self._max_idle
|
||||
):
|
||||
self._last_idle = current_time
|
||||
if not self._closed:
|
||||
event = events.Idle(self)
|
||||
for _cls, method in self._get_dispatch_methods(
|
||||
"on_idle", event
|
||||
):
|
||||
try:
|
||||
await invoke(method, event)
|
||||
except Exception as error:
|
||||
self.app._handle_exception(error)
|
||||
break
|
||||
|
||||
async def _dispatch_message(self, message: Message) -> None:
|
||||
"""Dispatch a message received from the message queue.
|
||||
@@ -412,6 +443,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
await self.on_event(message)
|
||||
else:
|
||||
await self._on_message(message)
|
||||
await self._flush_next_callbacks()
|
||||
|
||||
def _get_dispatch_methods(
|
||||
self, method_name: str, message: Message
|
||||
|
||||
@@ -367,19 +367,20 @@ class Screen(Widget):
|
||||
# Check for any widgets marked as 'dirty' (needs a repaint)
|
||||
event.prevent_default()
|
||||
|
||||
async with self.app._dom_lock:
|
||||
if self.is_current:
|
||||
if self._layout_required:
|
||||
self._refresh_layout()
|
||||
self._layout_required = False
|
||||
self._dirty_widgets.clear()
|
||||
if self._repaint_required:
|
||||
self._dirty_widgets.clear()
|
||||
self._dirty_widgets.add(self)
|
||||
self._repaint_required = False
|
||||
if self.is_current:
|
||||
async with self.app._dom_lock:
|
||||
if self.is_current:
|
||||
if self._layout_required:
|
||||
self._refresh_layout()
|
||||
self._layout_required = False
|
||||
self._dirty_widgets.clear()
|
||||
if self._repaint_required:
|
||||
self._dirty_widgets.clear()
|
||||
self._dirty_widgets.add(self)
|
||||
self._repaint_required = False
|
||||
|
||||
if self._dirty_widgets:
|
||||
self.update_timer.resume()
|
||||
if self._dirty_widgets:
|
||||
self.update_timer.resume()
|
||||
|
||||
# The Screen is idle - a good opportunity to invoke the scheduled callbacks
|
||||
await self._invoke_and_clear_callbacks()
|
||||
@@ -423,6 +424,7 @@ class Screen(Widget):
|
||||
|
||||
def _refresh_layout(self, size: Size | None = None, full: bool = False) -> None:
|
||||
"""Refresh the layout (can change size and positions of widgets)."""
|
||||
print("Screen._refresh_layout", size, full)
|
||||
size = self.outer_size if size is None else size
|
||||
if not size:
|
||||
return
|
||||
|
||||
@@ -91,6 +91,9 @@ class AwaitMount:
|
||||
self._parent = parent
|
||||
self._widgets = widgets
|
||||
|
||||
async def __call__(self) -> None:
|
||||
await self
|
||||
|
||||
def __await__(self) -> Generator[None, None, None]:
|
||||
async def await_mount() -> None:
|
||||
if self._widgets:
|
||||
@@ -606,7 +609,9 @@ class Widget(DOMNode):
|
||||
parent, *widgets, before=insert_before, after=insert_after
|
||||
)
|
||||
|
||||
return AwaitMount(self, mounted)
|
||||
await_mount = AwaitMount(self, mounted)
|
||||
self.call_next(await_mount)
|
||||
return await_mount
|
||||
|
||||
def move_child(
|
||||
self,
|
||||
|
||||
@@ -55,6 +55,7 @@ class Footer(Widget):
|
||||
async def watch_highlight_key(self, value) -> None:
|
||||
"""If highlight key changes we need to regenerate the text."""
|
||||
self._key_text = None
|
||||
self.refresh()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
watch(self.screen, "focused", self._focus_changed)
|
||||
|
||||
Reference in New Issue
Block a user