simplify idle events, add batching

This commit is contained in:
Will McGugan
2021-06-06 12:09:51 +01:00
parent 91b7efa4a9
commit d9fe76ac5a
5 changed files with 106 additions and 31 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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()

View File

@@ -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()