Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor to go async #41

Merged
merged 22 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 19 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
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)
Korijn marked this conversation as resolved.
Show resolved Hide resolved

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)
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
8 changes: 6 additions & 2 deletions examples/qt_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
------

An example demonstrating a qt app with a wgpu viz inside.
If needed, change the PySide6 import to e.g. PyQt6, PyQt5, or PySide2.

Note how the ``rendercanvas.qt.loop`` object is not even imported;
you can simply run ``app.exec()`` the Qt way.
"""

# ruff: noqa: N802, E402
Expand All @@ -13,6 +14,9 @@
import importlib


# Normally you'd just write e.g.
# from PySide6 import QtWidgets

# For the sake of making this example Just Work, we try multiple QT libs
for lib in ("PySide6", "PyQt6", "PySide2", "PyQt5"):
try:
Expand Down Expand Up @@ -65,5 +69,5 @@ def whenButtonClicked(self):
draw_frame = setup_drawing_sync(example.canvas)
example.canvas.request_draw(draw_frame)

# Enter Qt event loop (compatible with qt5/qt6)
# Enter Qt event-loop (compatible with qt5/qt6)
app.exec() if hasattr(app, "exec") else app.exec_()
Loading
Loading