Skip to content

Commit

Permalink
Refactor to go async (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
almarklein authored Dec 9, 2024
1 parent cf9192a commit 16e5079
Show file tree
Hide file tree
Showing 39 changed files with 1,906 additions and 1,055 deletions.
9 changes: 2 additions & 7 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ API

These are the base classes that make up the rendercanvas API:

* The :class:`~rendercanvas.BaseRenderCanvas` represets the main API.
* The :class:`~rendercanvas.BaseLoop` provides functionality to work with the event-loop and timers in a generic way.
* The :class:`~rendercanvas.BaseTimer` is returned by some methods of ``loop``.
* The :class:`~rendercanvas.BaseRenderCanvas` represents the main API.
* The :class:`~rendercanvas.BaseLoop` provides functionality to work with the event-loop in a generic way.
* The :class:`~rendercanvas.EventType` specifies the different types of events that can be connected to with :func:`canvas.add_event_handler() <rendercanvas.BaseRenderCanvas.add_event_handler>`.

.. autoclass:: rendercanvas.BaseRenderCanvas
Expand All @@ -16,10 +15,6 @@ These are the base classes that make up the rendercanvas API:
:members:
:member-order: bysource

.. autoclass:: rendercanvas.BaseTimer
:members:
:member-order: bysource

.. autoclass:: rendercanvas.EventType
:members:
:member-order: bysource
17 changes: 11 additions & 6 deletions docs/backendapi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,28 @@ This page documents what's needed to implement a backend for ``rendercanvas``. T
to help maintain current and new backends. Making this internal API clear helps understanding how the backend-system works.
Also see https://github.com/pygfx/rendercanvas/blob/main/rendercanvas/stub.py.

It is possible to create a custom backend (outside of the ``rendercanvas`` package). However, we consider this API an internal detail that may change
with each version without warning.
.. note::

.. autoclass:: rendercanvas.base.WrapperRenderCanvas
It is possible to create a custom backend (outside of the ``rendercanvas`` package). However, we consider this API an internal detail that may change
with each version without warning.

.. autoclass:: rendercanvas.stub.StubRenderCanvas

.. autoclass:: rendercanvas.stub.StubLoop
:members:
:private-members:
:member-order: bysource


.. autoclass:: rendercanvas.stub.StubTimer
.. autoclass:: rendercanvas.stub.StubCanvasGroup
:members:
:private-members:
:member-order: bysource

.. autoclass:: rendercanvas.stub.StubLoop

.. autoclass:: rendercanvas.stub.StubRenderCanvas
:members:
:private-members:
:member-order: bysource


.. autoclass:: rendercanvas.base.WrapperRenderCanvas
42 changes: 39 additions & 3 deletions docs/backends.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The auto backend

Generally the best approach for examples and small applications is to use the
automatically selected backend. This ensures that the code is portable
across different machines and environments. Using ``rendercanvas.auto`` selects a
across different machines and environments. Importing from ``rendercanvas.auto`` selects a
suitable backend depending on the environment and more. See
:ref:`interactive_use` for details.

Expand Down Expand Up @@ -37,6 +37,26 @@ but you can replace ``from rendercanvas.auto`` with ``from rendercanvas.glfw`` t
loop.run()
By default, the ``glfw`` backend uses an event-loop based on asyncio. But you can also select e.g. trio:

.. code-block:: py
from rendercanvas.glfw import RenderCanvas
from rendercanvas.trio import loop
# Use another loop than the default
RenderCanvas.select_loop(loop)
canvas = RenderCanvas(title="Example")
canvas.request_draw(your_draw_function)
async def main():
.. do your trio stuff
await loop.run_async()
trio.run(main)
Support for Qt
--------------

Expand Down Expand Up @@ -80,6 +100,23 @@ Alternatively, you can select the specific qt library to use, making it easy to
loop.run() # calls app.exec_()
It is technically possible to e.g. use a ``glfw`` canvas with the Qt loop. However, this is not recommended because Qt gets confused in the precense of other windows and may hang or segfault.
But the other way around, running a Qt canvas in e.g. the trio loop, works fine:

.. code-block:: py
from rendercanvas.pyside6 import RenderCanvas
from rendercanvas.trio import loop
# Use another loop than the default
RenderCanvas.select_loop(loop)
canvas = RenderCanvas(title="Example")
canvas.request_draw(your_draw_function)
trio.run(loop.run_async)
Support for wx
--------------

Expand All @@ -104,7 +141,6 @@ embed the canvas as a subwidget, use ``rendercanvas.wx.RenderWidget`` instead.
app.MainLoop()
Support for offscreen
---------------------

Expand Down Expand Up @@ -191,7 +227,7 @@ making it (about) impossible to tell that we cannot actually use
ipywidgets. So it will try to use ``jupyter_rfb``, but cannot render anything.
It's therefore advised to either use ``%gui qt`` or set the ``RENDERCANVAS_BACKEND`` env var
to "glfw". The latter option works well, because these kernels *do* have a
running asyncio event loop!
running asyncio event-loop!

On other environments that have a running ``asyncio`` loop, the glfw backend is
preferred. E.g on ``ptpython --asyncio``.
Expand Down
11 changes: 5 additions & 6 deletions docs/start.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,23 @@ Getting started
Installation
------------

You can install ``rendercanvas`` via pip or similar.
You can install ``rendercanvas`` via pip (or most other Python package managers).
Python 3.9 or higher is required. Pypy is supported.

.. code-block:: bash
pip install rendercanvas
Since most users will want to render something to screen, we recommend installing GLFW as well:
Multiple backends are supported, including multiple GUI libraries, but none of these are installed by default. See :doc:`backends` for details.

We recommend also installing `GLFW <https://github.com/FlorianRhiem/pyGLFW>`_, so that you have a lightweight backend available from the start:

.. code-block:: bash
pip install rendercanvas glfw
Multiple backends are supported, including multiple GUI libraries, but none of these are installed by default. See :doc:`backends` for details.


Creating a canvas
-----------------

Expand All @@ -44,7 +43,7 @@ Rendering to the canvas
The above just shows a grey window. We want to render to it by using wgpu or by generating images.

Depending on the tool you'll use to render to the canvas, you need a different context.
The purpose of the context to present the rendered result to the canvas.
The purpose of the context is to present the rendered result to the canvas.
There are currently two types of contexts.

Rendering using bitmaps:
Expand Down
31 changes: 31 additions & 0 deletions examples/cube_asyncio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
Cube asyncio
------------
Run a wgpu example on the glfw backend, and the asyncio loop
"""

import asyncio

from rendercanvas.glfw import RenderCanvas
from rendercanvas.asyncio import loop
from rendercanvas.utils.cube import setup_drawing_sync


# The asyncio loop is the default, but this may change, so better be explicit.
RenderCanvas.select_loop(loop)

canvas = RenderCanvas(
title="The wgpu cube on $backend with $loop", update_mode="continuous"
)
draw_frame = setup_drawing_sync(canvas)
canvas.request_draw(draw_frame)


if __name__ == "__main__":

async def main():
# ... add asyncio stuff here
await loop.run_async()

asyncio.run(main())
6 changes: 2 additions & 4 deletions examples/cube_auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@

# run_example = true

from rendercanvas.auto import RenderCanvas, run

from rendercanvas.auto import RenderCanvas, loop
from rendercanvas.utils.cube import setup_drawing_sync


canvas = RenderCanvas(
title="The wgpu cube example on $backend", update_mode="continuous"
)
Expand All @@ -20,4 +18,4 @@


if __name__ == "__main__":
run()
loop.run()
7 changes: 3 additions & 4 deletions examples/cube_glfw.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@
Cube glfw
---------
Run a wgpu example on the glfw backend.
Run a wgpu example on the glfw backend (with the default asyncio loop).
"""

from rendercanvas.glfw import RenderCanvas, run

from rendercanvas.glfw import RenderCanvas, loop
from rendercanvas.utils.cube import setup_drawing_sync


Expand All @@ -18,4 +17,4 @@


if __name__ == "__main__":
run()
loop.run()
27 changes: 27 additions & 0 deletions examples/cube_qt_trio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""
Cube qt canvas on the trio loop
-------------------------------
Run a wgpu example on the Qt backend, but with the trio loop.
Not sure why you'd want this, but it works! Note that the other way
around, e.g. runnning a glfw canvas with the Qt loop does not work so
well.
"""

# ruff: noqa: E402

import trio
from rendercanvas.pyside6 import RenderCanvas
from rendercanvas.trio import loop
from rendercanvas.utils.cube import setup_drawing_sync

RenderCanvas.select_loop(loop)

canvas = RenderCanvas(title="The $backend with $loop", update_mode="continuous")
draw_frame = setup_drawing_sync(canvas)
canvas.request_draw(draw_frame)


if __name__ == "__main__":
trio.run(loop.run_async)
36 changes: 36 additions & 0 deletions examples/cube_trio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
Cube trio
---------
Run a wgpu example on the glfw backend, and the trio loop
"""

import trio
from rendercanvas.glfw import RenderCanvas
from rendercanvas.trio import loop
from rendercanvas.utils.cube import setup_drawing_sync


RenderCanvas.select_loop(loop)

canvas = RenderCanvas(
title="The wgpu cube on $backend with $loop", update_mode="continuous"
)
draw_frame = setup_drawing_sync(canvas)
canvas.request_draw(draw_frame)


if __name__ == "__main__":
# This works, but is not very trio-ish
# loop.run()

# This looks more like it
# trio.run(loop.run_async)

# But for the sake of completeness ...

async def main():
# ... add Trio stuff here
await loop.run_async()

trio.run(main)
4 changes: 2 additions & 2 deletions examples/cube_wx.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Run a wgpu example on the wx backend.
"""

from rendercanvas.wx import RenderCanvas, run
from rendercanvas.wx import RenderCanvas, loop

from rendercanvas.utils.cube import setup_drawing_sync

Expand All @@ -18,4 +18,4 @@


if __name__ == "__main__":
run()
loop.run()
21 changes: 14 additions & 7 deletions examples/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,21 @@
* Can be closed with Escape or by pressing the window close button.
* In both cases, it should print "Close detected" exactly once.
* Hit space to spend 2 seconds doing direct draws.
* Hit "f" to spend 2 seconds doing direct draws.
* Hit "s" to async-sleep the scheduling loop for 2 seconds. Resizing
and closing the window still work.
"""

import time

from rendercanvas.auto import RenderCanvas, loop

from rendercanvas.utils.cube import setup_drawing_sync

from rendercanvas.utils.asyncs import sleep

canvas = RenderCanvas(
size=(640, 480),
title="Canvas events on $backend - $fps fps",
title="Canvas events with $backend - $fps fps",
max_fps=10,
update_mode="continuous",
present_method="",
Expand All @@ -33,20 +34,26 @@


@canvas.add_event_handler("*")
def process_event(event):
async def process_event(event):
if event["event_type"] not in ["pointer_move", "before_draw", "animate"]:
print(event)

if event["event_type"] == "key_down":
if event["key"] == "Escape":
canvas.close()
elif event["key"] == " ":
elif event["key"] in " f":
# Force draw for 2 secs
print("force-drawing ...")
etime = time.time() + 2
i = 0
while time.time() < etime:
i += 1
canvas.force_draw()
print(f"force-drawed {i} frames in 2s.")
print(f"Drew {i} frames in 2s.")
elif event["key"] == "s":
print("Async sleep ... zzzz")
await sleep(2)
print("waking up")
elif event["event_type"] == "close":
# Should see this exactly once, either when pressing escape, or
# when pressing the window close button.
Expand Down
Loading

0 comments on commit 16e5079

Please sign in to comment.