Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions examples/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,13 @@
# Note: in this demo we listen to all events (using '*'). In general
# you want to select one or more specific events to handle.

cursor_index = 0


@canvas.add_event_handler("*")
async def process_event(event):
global cursor_index

if event["event_type"] not in ["pointer_move", "before_draw", "animate"]:
print(event)

Expand All @@ -62,10 +66,10 @@ async def process_event(event):
elif event["key"] == "c":
# Swap cursor
shapes = list(rendercanvas.CursorShape)
canvas.cursor_index = getattr(canvas, "cursor_index", -1) + 1
if canvas.cursor_index >= len(shapes):
canvas.cursor_index = 0
cursor = shapes[canvas.cursor_index]
cursor_index += 1
if cursor_index >= len(shapes):
cursor_index = 0
cursor = shapes[cursor_index]
canvas.set_cursor(cursor)
print(f"Cursor: {cursor!r}")
elif event["event_type"] == "close":
Expand Down
2 changes: 1 addition & 1 deletion rendercanvas/_coreutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def log_exception(kind):
except Exception as err:
# Store exc info for postmortem debugging
exc_info = list(sys.exc_info())
exc_info[2] = exc_info[2].tb_next # skip *this* function
exc_info[2] = exc_info[2].tb_next # type: ignore | skip *this* function
sys.last_type, sys.last_value, sys.last_traceback = exc_info
# Show traceback, or a one-line summary
msg = str(err)
Expand Down
15 changes: 9 additions & 6 deletions rendercanvas/_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import time
from inspect import iscoroutinefunction
from collections import defaultdict, deque
from typing import Callable

from ._coreutils import log_exception
from ._enums import EventType
Expand Down Expand Up @@ -44,7 +45,7 @@ def _release(self):
self._pending_events.clear()
self._event_handlers.clear()

def add_handler(self, *args, order: float = 0):
def add_handler(self, *args, order: float = 0) -> Callable:
"""Register an event handler to receive events.

Arguments:
Expand Down Expand Up @@ -89,9 +90,11 @@ def my_handler(event):

"""
order = float(order)
decorating = not callable(args[0])
callback = None if decorating else args[0]
types = args if decorating else args[1:]
callback = None
types = args
if len(args) > 0 and callable(args[0]):
callback = args[0]
types = args[1:]

if not types:
raise TypeError("No event types are given to add_event_handler.")
Expand All @@ -101,11 +104,11 @@ def my_handler(event):
if not (type == "*" or type in valid_event_types):
raise ValueError(f"Adding handler with invalid event_type: '{type}'")

def decorator(_callback):
def decorator(_callback: Callable):
self._add_handler(_callback, order, *types)
return _callback

if decorating:
if callback is None:
return decorator
return decorator(callback)

Expand Down
9 changes: 7 additions & 2 deletions rendercanvas/_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,14 @@ async def _loop_task(self):
break
elif self.__should_stop:
# Close all remaining canvases. Loop will stop in a next iteration.
# We store a flag on the canvas, that we only use here.
for canvas in self.get_canvases():
if not getattr(canvas, "_rc_closed_by_loop", False):
canvas._rc_closed_by_loop = True
try:
closed_by_loop = canvas._rc_closed_by_loop # type: ignore
except AttributeError:
closed_by_loop = False
if not closed_by_loop:
canvas._rc_closed_by_loop = True # type: ignore
canvas.close()
del canvas

Expand Down
6 changes: 3 additions & 3 deletions rendercanvas/_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class Scheduler:
# don't affect the scheduling loop; they are just extra draws.

def __init__(
self, canvas, events, *, update_mode="ondemand", min_fps=0, max_fps=30
self, canvas, events, *, update_mode="ondemand", min_fps=0.0, max_fps=30.0
):
self.name = f"{canvas.__class__.__name__} scheduler"

Expand All @@ -65,7 +65,7 @@ def __init__(
def get_task(self):
"""Get task. Can be called exactly once. Used by the canvas."""
task = self.__scheduler_task
self.__scheduler_task = None
self.__scheduler_task = None # type: ignore
assert task is not None
return task

Expand Down Expand Up @@ -102,7 +102,7 @@ def request_draw(self):
self._draw_requested = True

async def __scheduler_task(self):
"""The coro that reprsents the scheduling loop for a canvas."""
"""The coro that represents the scheduling loop for a canvas."""

last_draw_time = 0
last_tick_time = 0
Expand Down
4 changes: 3 additions & 1 deletion rendercanvas/auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ def select_backend():
module = None
failed_backends = {} # name -> error

backend_name = "none"
reason = "no reason"
for backend_name, reason in backends_generator():
if "force" in reason.lower():
return _load_backend(backend_name)
Expand Down Expand Up @@ -122,7 +124,7 @@ def get_env_var(*varnames):
def backends_by_jupyter():
"""Generate backend names that are appropriate for the current Jupyter session (if any)."""
try:
ip = get_ipython()
ip = get_ipython() # type: ignore
except NameError:
return
if not ip.has_trait("kernel"):
Expand Down
59 changes: 31 additions & 28 deletions rendercanvas/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

from ._enums import (
EventTypeEnum,
UpdateMode,
UpdateModeEnum,
CursorShape,
CursorShapeEnum,
Expand Down Expand Up @@ -45,7 +44,7 @@
class BaseCanvasGroup:
"""Represents a group of canvas objects from the same class, that share a loop."""

def __init__(self, default_loop):
def __init__(self, default_loop: BaseLoop):
self._canvases = weakref.WeakSet()
self._loop = None
self.select_loop(default_loop)
Expand All @@ -71,11 +70,11 @@ def select_loop(self, loop: BaseLoop) -> None:
self._loop._unregister_canvas_group(self)
self._loop = loop

def get_loop(self) -> BaseLoop:
def get_loop(self) -> BaseLoop | None:
"""Get the currently associated loop (can be None for canvases that don't run a scheduler)."""
return self._loop

def get_canvases(self) -> List["BaseRenderCanvas"]:
def get_canvases(self) -> List[BaseRenderCanvas]:
"""Get a list of currently active (not-closed) canvases for this group."""
return [canvas for canvas in self._canvases if not canvas.get_closed()]

Expand Down Expand Up @@ -123,7 +122,7 @@ def select_loop(cls, loop: BaseLoop) -> None:
def __init__(
self,
*args,
size: Tuple[int] = (640, 480),
size: Tuple[int, int] = (640, 480),
title: str = "$backend",
update_mode: UpdateModeEnum = "ondemand",
min_fps: float = 0.0,
Expand Down Expand Up @@ -190,8 +189,8 @@ def _final_canvas_init(self):
del self.__kwargs_for_later
# Apply
if not isinstance(self, WrapperRenderCanvas):
self.set_logical_size(*kwargs["size"])
self.set_title(kwargs["title"])
self.set_logical_size(*kwargs["size"]) # type: ignore
self.set_title(kwargs["title"]) # type: ignore

def __del__(self):
# On delete, we call the custom destroy method.
Expand All @@ -202,15 +201,15 @@ def __del__(self):
# Since this is sometimes used in a multiple inheritance, the
# superclass may (or may not) have a __del__ method.
try:
super().__del__()
super().__del__() # type: ignore
except Exception:
pass

# %% Implement WgpuCanvasInterface

_canvas_context = None # set in get_context()

def get_physical_size(self) -> Tuple[int]:
def get_physical_size(self) -> Tuple[int, int]:
"""Get the physical size of the canvas in integer pixels."""
return self._rc_get_physical_size()

Expand Down Expand Up @@ -368,7 +367,10 @@ def set_update_mode(
max_fps (float): The maximum fps with update mode 'ondemand' and 'continuous'.

"""
self.__scheduler.set_update_mode(update_mode, min_fps=min_fps, max_fps=max_fps)
if self.__scheduler is not None:
self.__scheduler.set_update_mode(
update_mode, min_fps=min_fps, max_fps=max_fps
)

def request_draw(self, draw_function: Optional[DrawFunction] = None) -> None:
"""Schedule a new draw event.
Expand Down Expand Up @@ -463,7 +465,7 @@ def _draw_frame_and_present(self):

# %% Primary canvas management methods

def get_logical_size(self) -> Tuple[float]:
def get_logical_size(self) -> Tuple[float, float]:
"""Get the logical size (width, height) in float pixels.

The logical size can be smaller than the physical size, e.g. on HiDPI
Expand All @@ -485,12 +487,12 @@ def get_pixel_ratio(self) -> float:
def close(self) -> None:
"""Close the canvas."""
# Clear the draw-function, to avoid it holding onto e.g. wgpu objects.
self._draw_frame = None
self._draw_frame = None # type: ignore
# Clear the canvas context too.
if hasattr(self._canvas_context, "_release"):
# ContextInterface (and GPUCanvasContext) has _release()
try:
self._canvas_context._release()
self._canvas_context._release() # type: ignore
except Exception:
pass
self._canvas_context = None
Expand Down Expand Up @@ -541,12 +543,12 @@ def set_cursor(self, cursor: CursorShapeEnum) -> None:
cursor = "default"
if not isinstance(cursor, str):
raise TypeError("Canvas cursor must be str.")
cursor = cursor.lower().replace("_", "-")
if cursor not in CursorShape:
cursor_normed = cursor.lower().replace("_", "-")
if cursor_normed not in CursorShape:
raise ValueError(
f"Canvas cursor {cursor!r} not known, must be one of {CursorShape}"
)
self._rc_set_cursor(cursor)
self._rc_set_cursor(cursor_normed)

# %% Methods for the subclass to implement

Expand Down Expand Up @@ -609,19 +611,19 @@ def _rc_present_bitmap(self, *, data, format, **kwargs):
"""
raise NotImplementedError()

def _rc_get_physical_size(self):
def _rc_get_physical_size(self) -> Tuple[int, int]:
"""Get the physical size (with, height) in integer pixels."""
raise NotImplementedError()

def _rc_get_logical_size(self):
def _rc_get_logical_size(self) -> Tuple[float, float]:
"""Get the logical size (with, height) in float pixels."""
raise NotImplementedError()

def _rc_get_pixel_ratio(self):
def _rc_get_pixel_ratio(self) -> float:
"""Get ratio between physical and logical size."""
raise NotImplementedError()

def _rc_set_logical_size(self, width, height):
def _rc_set_logical_size(self, width: float, height: float):
"""Set the logical size. May be ignired when it makes no sense.

The default implementation does nothing.
Expand All @@ -644,18 +646,18 @@ def _rc_close(self):
"""
pass

def _rc_get_closed(self):
def _rc_get_closed(self) -> bool:
"""Get whether the canvas is closed."""
return False

def _rc_set_title(self, title):
def _rc_set_title(self, title: str):
"""Set the canvas title. May be ignored when it makes no sense.

The default implementation does nothing.
"""
pass

def _rc_set_cursor(self, cursor):
def _rc_set_cursor(self, cursor: str):
"""Set the cursor shape. May be ignored.

The default implementation does nothing.
Expand All @@ -672,15 +674,16 @@ class WrapperRenderCanvas(BaseRenderCanvas):
"""

_rc_canvas_group = None # No grouping for these wrappers
_subwidget: BaseRenderCanvas

@classmethod
def select_loop(cls, loop: BaseLoop) -> None:
m = sys.modules[cls.__module__]
return m.RenderWidget.select_loop(loop)

def add_event_handler(
self, *args: str | EventHandlerFunction, order: float = 0
) -> None:
self, *args: EventTypeEnum | EventHandlerFunction, order: float = 0
) -> Callable:
return self._subwidget._events.add_handler(*args, order=order)

def remove_event_handler(self, callback: EventHandlerFunction, *types: str) -> None:
Expand All @@ -694,7 +697,7 @@ def get_context(self, context_type: str) -> object:

def set_update_mode(
self,
update_mode: UpdateMode,
update_mode: UpdateModeEnum,
*,
min_fps: Optional[float] = None,
max_fps: Optional[float] = None,
Expand All @@ -707,10 +710,10 @@ def request_draw(self, draw_function: Optional[DrawFunction] = None) -> None:
def force_draw(self) -> None:
self._subwidget.force_draw()

def get_physical_size(self) -> Tuple[int]:
def get_physical_size(self) -> Tuple[int, int]:
return self._subwidget.get_physical_size()

def get_logical_size(self) -> Tuple[float]:
def get_logical_size(self) -> Tuple[float, float]:
return self._subwidget.get_logical_size()

def get_pixel_ratio(self) -> float:
Expand Down
5 changes: 3 additions & 2 deletions rendercanvas/utils/cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

import time
from typing import Callable

import wgpu
import numpy as np
Expand All @@ -14,7 +15,7 @@

def setup_drawing_sync(
canvas, power_preference="high-performance", limits=None, format=None
):
) -> Callable[[], None]:
"""Setup to draw a rotating cube on the given canvas.

The given canvas must implement WgpuCanvasInterface, but nothing more.
Expand Down Expand Up @@ -220,7 +221,7 @@ def create_pipeline_layout(device):

def get_draw_function(
canvas, device, render_pipeline, uniform_buffer, bind_groups, *, asynchronous
):
) -> Callable[[], None]:
# Create vertex buffer, and upload data
vertex_buffer = device.create_buffer_with_data(
data=vertex_data, usage=wgpu.BufferUsage.VERTEX
Expand Down