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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
from inspect import signature, isawaitable
|
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)
|
@lru_cache(maxsize=2048)
|
||||||
@@ -12,7 +20,7 @@ def count_parameters(func: Callable) -> int:
|
|||||||
return len(signature(func).parameters)
|
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.
|
"""Invoke a callback with an arbitrary number of parameters.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -27,3 +35,38 @@ async def invoke(callback: Callable, *params: object) -> Any:
|
|||||||
if isawaitable(result):
|
if isawaitable(result):
|
||||||
result = await result
|
result = await result
|
||||||
return 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)
|
_screen = self.get_screen(screen)
|
||||||
if not _screen.is_running:
|
if not _screen.is_running:
|
||||||
widgets = self._register(self, _screen)
|
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:
|
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:
|
def _replace_screen(self, screen: Screen) -> Screen:
|
||||||
"""Handle the replaced screen.
|
"""Handle the replaced screen.
|
||||||
@@ -2013,6 +2017,12 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
await self._close_messages()
|
await self._close_messages()
|
||||||
|
|
||||||
async def _on_resize(self, event: events.Resize) -> None:
|
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()
|
event.stop()
|
||||||
await self.screen.post_message(event)
|
await self.screen.post_message(event)
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class MessagePumpMeta(type):
|
|||||||
class MessagePump(metaclass=MessagePumpMeta):
|
class MessagePump(metaclass=MessagePumpMeta):
|
||||||
def __init__(self, parent: MessagePump | None = None) -> None:
|
def __init__(self, parent: MessagePump | None = None) -> None:
|
||||||
self._message_queue: Queue[Message | None] = Queue()
|
self._message_queue: Queue[Message | None] = Queue()
|
||||||
|
self._active_nessage: Message | None = None
|
||||||
self._parent = parent
|
self._parent = parent
|
||||||
self._running: bool = False
|
self._running: bool = False
|
||||||
self._closing: bool = False
|
self._closing: bool = False
|
||||||
@@ -75,6 +76,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
|||||||
self._last_idle: float = time()
|
self._last_idle: float = time()
|
||||||
self._max_idle: float | None = None
|
self._max_idle: float | None = None
|
||||||
self._mounted_event = asyncio.Event()
|
self._mounted_event = asyncio.Event()
|
||||||
|
self._next_callbacks: list[CallbackType] = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def task(self) -> Task:
|
def task(self) -> Task:
|
||||||
@@ -271,10 +273,22 @@ class MessagePump(metaclass=MessagePumpMeta):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
callback: Callable to call next.
|
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))
|
message = events.Callback(self, callback=partial(callback, *args, **kwargs))
|
||||||
self.post_message_no_wait(message)
|
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:
|
def _on_invoke_later(self, message: messages.InvokeLater) -> None:
|
||||||
# Forward InvokeLater message to the Screen
|
# Forward InvokeLater message to the Screen
|
||||||
self.app.screen._invoke_later(message.callback)
|
self.app.screen._invoke_later(message.callback)
|
||||||
@@ -367,35 +381,52 @@ class MessagePump(metaclass=MessagePumpMeta):
|
|||||||
except MessagePumpClosed:
|
except MessagePumpClosed:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
self._active_message = message
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self._dispatch_message(message)
|
try:
|
||||||
except CancelledError:
|
await self._dispatch_message(message)
|
||||||
raise
|
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:
|
except Exception as error:
|
||||||
self._mounted_event.set()
|
|
||||||
self.app._handle_exception(error)
|
self.app._handle_exception(error)
|
||||||
break
|
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:
|
async def _dispatch_message(self, message: Message) -> None:
|
||||||
"""Dispatch a message received from the message queue.
|
"""Dispatch a message received from the message queue.
|
||||||
@@ -412,6 +443,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
|||||||
await self.on_event(message)
|
await self.on_event(message)
|
||||||
else:
|
else:
|
||||||
await self._on_message(message)
|
await self._on_message(message)
|
||||||
|
await self._flush_next_callbacks()
|
||||||
|
|
||||||
def _get_dispatch_methods(
|
def _get_dispatch_methods(
|
||||||
self, method_name: str, message: Message
|
self, method_name: str, message: Message
|
||||||
|
|||||||
@@ -367,19 +367,20 @@ class Screen(Widget):
|
|||||||
# Check for any widgets marked as 'dirty' (needs a repaint)
|
# Check for any widgets marked as 'dirty' (needs a repaint)
|
||||||
event.prevent_default()
|
event.prevent_default()
|
||||||
|
|
||||||
async with self.app._dom_lock:
|
if self.is_current:
|
||||||
if self.is_current:
|
async with self.app._dom_lock:
|
||||||
if self._layout_required:
|
if self.is_current:
|
||||||
self._refresh_layout()
|
if self._layout_required:
|
||||||
self._layout_required = False
|
self._refresh_layout()
|
||||||
self._dirty_widgets.clear()
|
self._layout_required = False
|
||||||
if self._repaint_required:
|
self._dirty_widgets.clear()
|
||||||
self._dirty_widgets.clear()
|
if self._repaint_required:
|
||||||
self._dirty_widgets.add(self)
|
self._dirty_widgets.clear()
|
||||||
self._repaint_required = False
|
self._dirty_widgets.add(self)
|
||||||
|
self._repaint_required = False
|
||||||
|
|
||||||
if self._dirty_widgets:
|
if self._dirty_widgets:
|
||||||
self.update_timer.resume()
|
self.update_timer.resume()
|
||||||
|
|
||||||
# The Screen is idle - a good opportunity to invoke the scheduled callbacks
|
# The Screen is idle - a good opportunity to invoke the scheduled callbacks
|
||||||
await self._invoke_and_clear_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:
|
def _refresh_layout(self, size: Size | None = None, full: bool = False) -> None:
|
||||||
"""Refresh the layout (can change size and positions of widgets)."""
|
"""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
|
size = self.outer_size if size is None else size
|
||||||
if not size:
|
if not size:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -91,6 +91,9 @@ class AwaitMount:
|
|||||||
self._parent = parent
|
self._parent = parent
|
||||||
self._widgets = widgets
|
self._widgets = widgets
|
||||||
|
|
||||||
|
async def __call__(self) -> None:
|
||||||
|
await self
|
||||||
|
|
||||||
def __await__(self) -> Generator[None, None, None]:
|
def __await__(self) -> Generator[None, None, None]:
|
||||||
async def await_mount() -> None:
|
async def await_mount() -> None:
|
||||||
if self._widgets:
|
if self._widgets:
|
||||||
@@ -606,7 +609,9 @@ class Widget(DOMNode):
|
|||||||
parent, *widgets, before=insert_before, after=insert_after
|
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(
|
def move_child(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ class Footer(Widget):
|
|||||||
async def watch_highlight_key(self, value) -> None:
|
async def watch_highlight_key(self, value) -> None:
|
||||||
"""If highlight key changes we need to regenerate the text."""
|
"""If highlight key changes we need to regenerate the text."""
|
||||||
self._key_text = None
|
self._key_text = None
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
watch(self.screen, "focused", self._focus_changed)
|
watch(self.screen, "focused", self._focus_changed)
|
||||||
|
|||||||
Reference in New Issue
Block a user