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()
|
DOUBLE_CLICK = auto()
|
||||||
ENTER = auto()
|
ENTER = auto()
|
||||||
LEAVE = auto()
|
LEAVE = auto()
|
||||||
|
UPDATE = auto()
|
||||||
CUSTOM = 1000
|
CUSTOM = 1000
|
||||||
|
|
||||||
|
|
||||||
@@ -230,3 +231,8 @@ class Focus(Event, type=EventType.FOCUS):
|
|||||||
|
|
||||||
class Blur(Event, type=EventType.BLUR):
|
class Blur(Event, type=EventType.BLUR):
|
||||||
pass
|
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
|
sender: MessageTarget
|
||||||
bubble: ClassVar[bool] = False
|
bubble: ClassVar[bool] = False
|
||||||
default_priority: ClassVar[int] = 0
|
default_priority: ClassVar[int] = 0
|
||||||
suppressed: bool = False
|
|
||||||
|
_no_default_action: bool = False
|
||||||
|
_stop_propagaton: bool = False
|
||||||
|
|
||||||
def __init__(self, sender: MessageTarget) -> None:
|
def __init__(self, sender: MessageTarget) -> None:
|
||||||
self.sender = sender
|
self.sender = sender
|
||||||
self.name = camel_to_snake(self.__class__.__name__)
|
self.name = camel_to_snake(self.__class__.__name__)
|
||||||
self.time = monotonic()
|
self.time = monotonic()
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
def __init_subclass__(cls, bubble: bool = False, priority: int = 0) -> None:
|
def __init_subclass__(cls, bubble: bool = False, priority: int = 0) -> None:
|
||||||
super().__init_subclass__()
|
super().__init_subclass__()
|
||||||
cls.bubble = bubble
|
cls.bubble = bubble
|
||||||
cls.default_priority = priority
|
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.
|
"""Suppress the default action.
|
||||||
|
|
||||||
Args:
|
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.
|
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
|
from typing import Optional, NamedTuple, Set, Type, TYPE_CHECKING
|
||||||
import asyncio
|
import asyncio
|
||||||
from asyncio import PriorityQueue
|
from asyncio import PriorityQueue, QueueEmpty
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -56,6 +56,7 @@ class MessagePump:
|
|||||||
self._closing: bool = False
|
self._closing: bool = False
|
||||||
self._closed: bool = False
|
self._closed: bool = False
|
||||||
self._disabled_messages: Set[Type[Message]] = set()
|
self._disabled_messages: Set[Type[Message]] = set()
|
||||||
|
self._pending_message: Optional[MessageQueueItem] = None
|
||||||
|
|
||||||
def check_message_enabled(self, message: Message) -> bool:
|
def check_message_enabled(self, message: Message) -> bool:
|
||||||
return type(message) not in self._disabled_messages
|
return type(message) not in self._disabled_messages
|
||||||
@@ -74,6 +75,11 @@ class MessagePump:
|
|||||||
Returns:
|
Returns:
|
||||||
Optional[Event]: Event object or None.
|
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:
|
if self._closed:
|
||||||
raise MessagePumpClosed("The message pump is closed")
|
raise MessagePumpClosed("The message pump is closed")
|
||||||
queue_item = await self._message_queue.get()
|
queue_item = await self._message_queue.get()
|
||||||
@@ -82,6 +88,20 @@ class MessagePump:
|
|||||||
raise MessagePumpClosed("The message pump is now closed")
|
raise MessagePumpClosed("The message pump is now closed")
|
||||||
return queue_item
|
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(
|
def set_timer(
|
||||||
self,
|
self,
|
||||||
delay: float,
|
delay: float,
|
||||||
@@ -121,11 +141,20 @@ class MessagePump:
|
|||||||
except Exception:
|
except Exception:
|
||||||
log.exception("error getting message")
|
log.exception("error getting message")
|
||||||
break
|
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:
|
try:
|
||||||
await self.dispatch_message(message, priority)
|
await self.dispatch_message(message, priority)
|
||||||
finally:
|
finally:
|
||||||
if self._message_queue.empty():
|
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(
|
async def dispatch_message(
|
||||||
self, message: Message, priority: int = 0
|
self, message: Message, priority: int = 0
|
||||||
@@ -141,7 +170,7 @@ class MessagePump:
|
|||||||
dispatch_function: MessageHandler = getattr(self, method_name, None)
|
dispatch_function: MessageHandler = getattr(self, method_name, None)
|
||||||
if dispatch_function is not None:
|
if dispatch_function is not None:
|
||||||
await dispatch_function(event)
|
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:
|
if event.sender == self._parent:
|
||||||
log.debug("bubbled event abandoned; %r", event)
|
log.debug("bubbled event abandoned; %r", event)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ log = getLogger("rich")
|
|||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshMessage(Message):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Reactive(Generic[T]):
|
class Reactive(Generic[T]):
|
||||||
def __init__(self, default: T) -> None:
|
def __init__(self, default: T) -> None:
|
||||||
self._default = default
|
self._default = default
|
||||||
@@ -61,7 +65,6 @@ class Widget(MessagePump):
|
|||||||
_count: ClassVar[int] = 0
|
_count: ClassVar[int] = 0
|
||||||
can_focus: bool = False
|
can_focus: bool = False
|
||||||
mouse_events: bool = False
|
mouse_events: bool = False
|
||||||
idle_events: bool = False
|
|
||||||
|
|
||||||
def __init__(self, name: Optional[str] = None) -> None:
|
def __init__(self, name: Optional[str] = None) -> None:
|
||||||
self.name = name or f"{self.__class__.__name__}#{self._count}"
|
self.name = name or f"{self.__class__.__name__}#{self._count}"
|
||||||
@@ -79,22 +82,15 @@ class Widget(MessagePump):
|
|||||||
events.Click,
|
events.Click,
|
||||||
events.DoubleClick,
|
events.DoubleClick,
|
||||||
)
|
)
|
||||||
if not self.idle_events:
|
|
||||||
self.disable_messages(events.Idle)
|
|
||||||
|
|
||||||
def __init_subclass__(
|
def __init_subclass__(
|
||||||
cls,
|
cls,
|
||||||
can_focus: bool = False,
|
can_focus: bool = False,
|
||||||
mouse_events: bool = True,
|
mouse_events: bool = True,
|
||||||
idle_events: bool = False,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init_subclass__()
|
super().__init_subclass__()
|
||||||
cls.can_focus = can_focus
|
cls.can_focus = can_focus
|
||||||
cls.mouse_events = mouse_events
|
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:
|
def __rich_repr__(self) -> RichReprResult:
|
||||||
yield "name", self.name
|
yield "name", self.name
|
||||||
@@ -127,15 +123,15 @@ class Widget(MessagePump):
|
|||||||
def require_refresh(self) -> None:
|
def require_refresh(self) -> None:
|
||||||
self._line_cache = 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]:
|
def render_update(self, x: int, y: int) -> Iterable[Segment]:
|
||||||
yield from self.line_cache.render(x, y)
|
yield from self.line_cache.render(x, y)
|
||||||
|
|
||||||
def render(self) -> RenderableType:
|
def render(self) -> RenderableType:
|
||||||
return Panel(
|
return Panel(
|
||||||
Align.center(Pretty(self), vertical="middle"),
|
Align.center(Pretty(self), vertical="middle"), title=self.__class__.__name__
|
||||||
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 post_message(
|
async def post_message(
|
||||||
@@ -147,8 +143,11 @@ class Widget(MessagePump):
|
|||||||
return await super().post_message(message, priority)
|
return await super().post_message(message, priority)
|
||||||
|
|
||||||
async def on_event(self, event: events.Event, priority: int) -> None:
|
async def on_event(self, event: events.Event, priority: int) -> None:
|
||||||
if isinstance(event, (events.Enter, events.Leave)):
|
if isinstance(event, events.Resize):
|
||||||
self.mouse_over = isinstance(event, events.Enter)
|
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)
|
await super().on_event(event, priority)
|
||||||
|
|
||||||
async def on_resize(self, event: events.Resize) -> None:
|
async def on_resize(self, event: events.Resize) -> None:
|
||||||
@@ -156,12 +155,3 @@ class Widget(MessagePump):
|
|||||||
if self.size != new_size:
|
if self.size != new_size:
|
||||||
self.size = new_size
|
self.size = new_size
|
||||||
self.require_refresh()
|
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
|
from rich.repr import RichReprResult
|
||||||
|
|
||||||
|
|
||||||
class Placeholder(Widget, can_focus=True, mouse_events=True):
|
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:
|
def __rich_repr__(self) -> RichReprResult:
|
||||||
yield "name", self.name
|
yield "name", self.name
|
||||||
yield "has_focus", self.has_focus
|
yield "has_focus", self.has_focus
|
||||||
yield "mouse_over", self.mouse_over
|
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