From 30da787ea4287e1eda3f1a68d9bdd1c4a883f02d Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Sun, 29 Jun 2025 12:44:57 +0200 Subject: [PATCH] poc event task --- examples/drag.py | 60 +++++++++++++++++++++++++++++++++++++++++ rendercanvas/_events.py | 27 +++++++++++++++++++ rendercanvas/base.py | 8 +++++- 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 examples/drag.py diff --git a/examples/drag.py b/examples/drag.py new file mode 100644 index 0000000..fbe7ac3 --- /dev/null +++ b/examples/drag.py @@ -0,0 +1,60 @@ +""" +Noise +----- + +Simple example that uses the bitmap-context to show images of noise. +""" + +# run_example = true + +import numpy as np +from rendercanvas.auto import RenderCanvas, loop + + +canvas = RenderCanvas(update_mode="continuous") +context = canvas.get_context("bitmap") + + +w, h = 12, 12 +currentpos = [1, 1] + +@canvas.request_draw +def animate(): + x, y = currentpos + + bitmap = np.zeros((h, w, 4), np.uint8) + bitmap[y, x] = 255 + + context.set_bitmap(bitmap) + + + +@canvas.add_event_task +async def foo(emitter): + while True: + + # Wait for pointer down + event = await emitter.for_event("pointer_down") + + # Does this select the current position of the active block? + width, height = canvas.get_logical_size() + x = int(w * event["x"] / width) + y = int(h * event["y"] / height) + if [x, y] != currentpos: + print("nope", x, y) + continue + + # Move until pointer up + while True: + event = await emitter.for_event("pointer_move", "pointer_up") + if event["event_type"] == "pointer_up": + break + + width, height = canvas.get_logical_size() + x = int(w * event["x"] / width) + y = int(h * event["y"] / height) + print(x, y) + currentpos[:] = x, y + + +loop.run() diff --git a/rendercanvas/_events.py b/rendercanvas/_events.py index 4552e4e..cf98bac 100644 --- a/rendercanvas/_events.py +++ b/rendercanvas/_events.py @@ -69,6 +69,7 @@ class EventEmitter: def __init__(self): self._pending_events = deque() self._event_handlers = defaultdict(list) + self._signals = [] self._closed = False def _release(self): @@ -76,6 +77,22 @@ def _release(self): self._pending_events.clear() self._event_handlers.clear() + async def for_event(self, *event_types: str): + from .utils.asyncs import Event as AsyncSignal # rename to avoid confusion + + for type in event_types: + if not (type == "*" or type in valid_event_types): + raise ValueError(f"Calling for_event with invalid event_type: '{type}'") + + # Get signal. This is an asyncio, trio, rendercanvas.utils._async_adapter, or anything else from sniffio + # This is a low level asyncio construct that's actually called "Event", but let's call it signal here. + signal = AsyncSignal() + signal.event_types = set(event_types) + self._signals.append(signal) + + await signal.wait() + return signal.event + def add_handler(self, *args, order: float = 0): """Register an event handler to receive events. @@ -225,6 +242,16 @@ async def emit(self, event): await callback(event) else: callback(event) + # Handle signals that ``await for_event(..)``. + # TODO: Use more performent data structure so we can avoid iterating over all waiting signals. + # TODO: maybe it makes more sense to think of the signals as tasks; each signal represents a waiting task. + # TODO: is there also a common construct that we can pass a value to? + signals_to_remove = [] + for signal in self._signals: + if event_type in signal.event_types: + signals_to_remove.append(signal) + signal.event = event + signal.set() # Close? if event_type == "close": self._release() diff --git a/rendercanvas/base.py b/rendercanvas/base.py index 32ca623..43d581c 100644 --- a/rendercanvas/base.py +++ b/rendercanvas/base.py @@ -15,7 +15,7 @@ from ._coreutils import logger, log_exception, BaseEnum if TYPE_CHECKING: - from typing import Callable, List, Optional, Tuple + from typing import Callable, Coroutine, List, Optional, Tuple EventHandlerFunction = Callable[[dict], None] DrawFunction = Callable[[], None] @@ -309,6 +309,12 @@ def add_event_handler( def remove_event_handler(self, callback: EventHandlerFunction, *types: str) -> None: return self._events.remove_handler(callback, *types) + def add_event_task( + self, async_func: Callable[[], Coroutine], name: str = "unnamed" + ): + loop = self._rc_canvas_group.get_loop() + loop.add_task(async_func, self._events) + def submit_event(self, event: dict) -> None: # Not strictly necessary for normal use-cases, but this allows # the ._event to be an implementation detail to subclasses, and it