mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
simplify idle events, add batching
This commit is contained in:
@@ -40,6 +40,7 @@ class EventType(Enum):
|
||||
DOUBLE_CLICK = auto()
|
||||
ENTER = auto()
|
||||
LEAVE = auto()
|
||||
UPDATE = auto()
|
||||
CUSTOM = 1000
|
||||
|
||||
|
||||
@@ -230,3 +231,8 @@ class Focus(Event, type=EventType.FOCUS):
|
||||
|
||||
class Blur(Event, type=EventType.BLUR):
|
||||
pass
|
||||
|
||||
|
||||
class Update(Event, type=EventType.UPDATE):
|
||||
def can_batch(self, event: Event) -> bool:
|
||||
return isinstance(event, Update) and event.sender == self.sender
|
||||
|
||||
@@ -12,23 +12,40 @@ class Message:
|
||||
sender: MessageTarget
|
||||
bubble: ClassVar[bool] = False
|
||||
default_priority: ClassVar[int] = 0
|
||||
suppressed: bool = False
|
||||
|
||||
_no_default_action: bool = False
|
||||
_stop_propagaton: bool = False
|
||||
|
||||
def __init__(self, sender: MessageTarget) -> None:
|
||||
self.sender = sender
|
||||
self.name = camel_to_snake(self.__class__.__name__)
|
||||
self.time = monotonic()
|
||||
super().__init__()
|
||||
|
||||
def __init_subclass__(cls, bubble: bool = False, priority: int = 0) -> None:
|
||||
super().__init_subclass__()
|
||||
cls.bubble = bubble
|
||||
cls.default_priority = priority
|
||||
|
||||
def suppress_default(self, suppress: bool = True) -> None:
|
||||
def can_batch(self, message: "Message") -> bool:
|
||||
"""Check if another message may supersede this one.
|
||||
|
||||
Args:
|
||||
message (Message): [description]
|
||||
|
||||
Returns:
|
||||
bool: [description]
|
||||
"""
|
||||
return False
|
||||
|
||||
def prevent_default(self, prevent: bool = True) -> None:
|
||||
"""Suppress the default action.
|
||||
|
||||
Args:
|
||||
suppress (bool, optional): True if the default action should be suppressed,
|
||||
prevent (bool, optional): True if the default action should be suppressed,
|
||||
or False if the default actions should be performed. Defaults to True.
|
||||
"""
|
||||
self.suppress = suppress
|
||||
self._no_default_action = prevent
|
||||
|
||||
def stop_propagation(self, stop: bool = True) -> None:
|
||||
self._stop_propagaton = stop
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Optional, NamedTuple, Set, Type, TYPE_CHECKING
|
||||
import asyncio
|
||||
from asyncio import PriorityQueue
|
||||
from asyncio import PriorityQueue, QueueEmpty
|
||||
|
||||
import logging
|
||||
|
||||
@@ -56,6 +56,7 @@ class MessagePump:
|
||||
self._closing: bool = False
|
||||
self._closed: bool = False
|
||||
self._disabled_messages: Set[Type[Message]] = set()
|
||||
self._pending_message: Optional[MessageQueueItem] = None
|
||||
|
||||
def check_message_enabled(self, message: Message) -> bool:
|
||||
return type(message) not in self._disabled_messages
|
||||
@@ -74,6 +75,11 @@ class MessagePump:
|
||||
Returns:
|
||||
Optional[Event]: Event object or None.
|
||||
"""
|
||||
if self._pending_message is not None:
|
||||
try:
|
||||
return self._pending_message
|
||||
finally:
|
||||
self._pending_message = None
|
||||
if self._closed:
|
||||
raise MessagePumpClosed("The message pump is closed")
|
||||
queue_item = await self._message_queue.get()
|
||||
@@ -82,6 +88,20 @@ class MessagePump:
|
||||
raise MessagePumpClosed("The message pump is now closed")
|
||||
return queue_item
|
||||
|
||||
def peek_message(self) -> Optional[MessageQueueItem]:
|
||||
"""Peek the message at the head of the queue (does not remove it from the queue),
|
||||
or return None if the queue is empty.
|
||||
|
||||
Returns:
|
||||
Optional[Message]: The message or None.
|
||||
"""
|
||||
if self._pending_message is None:
|
||||
self._pending_message = self._message_queue.get_nowait()
|
||||
|
||||
if self._pending_message is not None:
|
||||
return self._pending_message
|
||||
return None
|
||||
|
||||
def set_timer(
|
||||
self,
|
||||
delay: float,
|
||||
@@ -121,11 +141,20 @@ class MessagePump:
|
||||
except Exception:
|
||||
log.exception("error getting message")
|
||||
break
|
||||
# Combine any pending messages that may supersede this one
|
||||
while True:
|
||||
pending = self.peek_message()
|
||||
if pending is None or not message.can_batch(pending.message):
|
||||
break
|
||||
priority, message = pending
|
||||
|
||||
try:
|
||||
await self.dispatch_message(message, priority)
|
||||
finally:
|
||||
if self._message_queue.empty():
|
||||
await self.dispatch_message(events.Idle(self))
|
||||
idle_handler = getattr(self, "on_idle", None)
|
||||
if idle_handler is not None:
|
||||
await idle_handler(events.Idle(self))
|
||||
|
||||
async def dispatch_message(
|
||||
self, message: Message, priority: int = 0
|
||||
@@ -141,7 +170,7 @@ class MessagePump:
|
||||
dispatch_function: MessageHandler = getattr(self, method_name, None)
|
||||
if dispatch_function is not None:
|
||||
await dispatch_function(event)
|
||||
if event.bubble and self._parent:
|
||||
if event.bubble and self._parent and not event._stop_propagaton:
|
||||
if event.sender == self._parent:
|
||||
log.debug("bubbled event abandoned; %r", event)
|
||||
else:
|
||||
|
||||
@@ -33,6 +33,10 @@ log = getLogger("rich")
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class RefreshMessage(Message):
|
||||
pass
|
||||
|
||||
|
||||
class Reactive(Generic[T]):
|
||||
def __init__(self, default: T) -> None:
|
||||
self._default = default
|
||||
@@ -61,7 +65,6 @@ class Widget(MessagePump):
|
||||
_count: ClassVar[int] = 0
|
||||
can_focus: bool = False
|
||||
mouse_events: bool = False
|
||||
idle_events: bool = False
|
||||
|
||||
def __init__(self, name: Optional[str] = None) -> None:
|
||||
self.name = name or f"{self.__class__.__name__}#{self._count}"
|
||||
@@ -79,22 +82,15 @@ class Widget(MessagePump):
|
||||
events.Click,
|
||||
events.DoubleClick,
|
||||
)
|
||||
if not self.idle_events:
|
||||
self.disable_messages(events.Idle)
|
||||
|
||||
def __init_subclass__(
|
||||
cls,
|
||||
can_focus: bool = False,
|
||||
mouse_events: bool = True,
|
||||
idle_events: bool = False,
|
||||
) -> None:
|
||||
super().__init_subclass__()
|
||||
cls.can_focus = can_focus
|
||||
cls.mouse_events = mouse_events
|
||||
cls.idle_events = idle_events
|
||||
|
||||
has_focus: Reactive[bool] = Reactive(False)
|
||||
mouse_over: Reactive[bool] = Reactive(False)
|
||||
|
||||
def __rich_repr__(self) -> RichReprResult:
|
||||
yield "name", self.name
|
||||
@@ -127,15 +123,15 @@ class Widget(MessagePump):
|
||||
def require_refresh(self) -> None:
|
||||
self._line_cache = None
|
||||
|
||||
async def refresh(self) -> None:
|
||||
await self.emit(RefreshMessage(self))
|
||||
|
||||
def render_update(self, x: int, y: int) -> Iterable[Segment]:
|
||||
yield from self.line_cache.render(x, y)
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
return Panel(
|
||||
Align.center(Pretty(self), vertical="middle"),
|
||||
title=self.__class__.__name__,
|
||||
border_style="green" if self.mouse_over else "blue",
|
||||
box=box.HEAVY if self.has_focus else box.ROUNDED,
|
||||
Align.center(Pretty(self), vertical="middle"), title=self.__class__.__name__
|
||||
)
|
||||
|
||||
async def post_message(
|
||||
@@ -147,8 +143,11 @@ class Widget(MessagePump):
|
||||
return await super().post_message(message, priority)
|
||||
|
||||
async def on_event(self, event: events.Event, priority: int) -> None:
|
||||
if isinstance(event, (events.Enter, events.Leave)):
|
||||
self.mouse_over = isinstance(event, events.Enter)
|
||||
if isinstance(event, events.Resize):
|
||||
new_size = WidgetDimensions(event.width, event.height)
|
||||
if self.size != new_size:
|
||||
self.size = new_size
|
||||
self.require_refresh()
|
||||
await super().on_event(event, priority)
|
||||
|
||||
async def on_resize(self, event: events.Resize) -> None:
|
||||
@@ -156,12 +155,3 @@ class Widget(MessagePump):
|
||||
if self.size != new_size:
|
||||
self.size = new_size
|
||||
self.require_refresh()
|
||||
|
||||
async def on_focus(self, event: events.Focus) -> None:
|
||||
self.has_focus = True
|
||||
|
||||
async def on_blur(self, event: events.Focus) -> None:
|
||||
self.has_focus = False
|
||||
|
||||
async def on_idle(self, event: events.Idle) -> None:
|
||||
self.app.refresh()
|
||||
|
||||
@@ -1,10 +1,43 @@
|
||||
from ..widget import Widget
|
||||
from .. import events
|
||||
from ..widget import Reactive, Widget
|
||||
|
||||
from rich import box
|
||||
from rich.align import Align
|
||||
from rich.console import RenderableType
|
||||
from rich.panel import Panel
|
||||
from rich.pretty import Pretty
|
||||
from rich.repr import RichReprResult
|
||||
|
||||
|
||||
class Placeholder(Widget, can_focus=True, mouse_events=True):
|
||||
|
||||
has_focus: Reactive[bool] = Reactive(False)
|
||||
mouse_over: Reactive[bool] = Reactive(False)
|
||||
|
||||
def __rich_repr__(self) -> RichReprResult:
|
||||
yield "name", self.name
|
||||
yield "has_focus", self.has_focus
|
||||
yield "mouse_over", self.mouse_over
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
return Panel(
|
||||
Align.center(Pretty(self), vertical="middle"),
|
||||
title=self.__class__.__name__,
|
||||
border_style="green" if self.mouse_over else "blue",
|
||||
box=box.HEAVY if self.has_focus else box.ROUNDED,
|
||||
)
|
||||
|
||||
async def on_focus(self, event: events.Focus) -> None:
|
||||
self.has_focus = True
|
||||
|
||||
async def on_blur(self, event: events.Blur) -> None:
|
||||
self.has_focus = False
|
||||
|
||||
async def on_enter(self, event: events.Enter) -> None:
|
||||
self.mouse_over = True
|
||||
|
||||
async def on_leave(self, event: events.Leave) -> None:
|
||||
self.mouse_over = False
|
||||
|
||||
async def on_idle(self, event: events.Idle) -> None:
|
||||
self.app.refresh()
|
||||
|
||||
Reference in New Issue
Block a user