From ecc6e98d2b175ab9cef5a2c107a88af232258d73 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Thu, 12 Mar 2026 13:00:05 +0100 Subject: [PATCH 1/5] Resize and title support for Pyodide canvases --- docs/backends.rst | 33 ++++- docs/static/_pyodide_iframe.html | 18 +-- examples/pyscript.html | 17 +-- examples/serve_browser_examples.py | 18 +-- examples/snake.py | 2 +- rendercanvas/core/renderview-pyodide.js | 60 +++++++++ rendercanvas/core/renderview.css | 35 ++--- rendercanvas/core/renderview.js | 163 ++++++++++++++++++------ rendercanvas/pyodide.py | 66 ++-------- 9 files changed, 266 insertions(+), 146 deletions(-) create mode 100644 rendercanvas/core/renderview-pyodide.js diff --git a/docs/backends.rst b/docs/backends.rst index 56b4abf..6472c35 100644 --- a/docs/backends.rst +++ b/docs/backends.rst @@ -290,9 +290,8 @@ additional dependencies. Currently only presenting a bitmap is supported, as shown in the examples :doc:`noise.py ` and :doc:`snake.py`. Support for wgpu is underway. -An HTMLCanvasElement is assumed to be present in the -DOM. By default it connects to the canvas with id "canvas", but a -different id or element can also be provided using ``RenderCanvas(canvas_element)``. +The backend will render to an HTML ````. This can be provided with ``RenderCanvas(canvas_element='canvas')``, +either by providing the element as an object, or via it's id. By default, it connects with the element with id "canvas". An example using PyScript (which uses Pyodide): @@ -312,8 +311,34 @@ An example using PyScript (which uses Pyodide): +The 'canvas_element' can also be a ``
`` element with the class 'rendercanvas-wrapper'. In this +case the canvas will be created inside that wrapper, plus additional things to support features like +a title bar and manual resizing. These features can be enabled with the 'has-titlebar' and 'is-resizable' css classes. +The wrapper can also contain placeholder elements that will be deleted once the canvas is loaded: -An example using Pyodide directly: +.. code-block:: html + + + + + + + + +
+

+ Loading ... +

+
+
+ + + + + +It is also possible to use Pyodide directly. It requires a bit more plumbing. +Similar as with PyScript, you can chose between a ```` and a ``
``: .. code-block:: html diff --git a/docs/static/_pyodide_iframe.html b/docs/static/_pyodide_iframe.html index 34da006..602f528 100644 --- a/docs/static/_pyodide_iframe.html +++ b/docs/static/_pyodide_iframe.html @@ -8,14 +8,17 @@ - -

Loading...

-
- + +
+

+ Loading ... +

+
+ -

This example demonstrates using PyScript. It needs to be loaded through a web-server for it to access the Python file. @@ -29,7 +19,12 @@

Loading...

there.

- +
+

+ Loading ... +

+

diff --git a/examples/serve_browser_examples.py b/examples/serve_browser_examples.py index 5fc81da..65648e7 100644 --- a/examples/serve_browser_examples.py +++ b/examples/serve_browser_examples.py @@ -89,20 +89,10 @@ def get_html_index(): Back to list

- -

- docstring -

- -

Loading...

-
- - - +

docstring

+
+

Loading ...

+
diff --git a/examples/snake.py b/examples/snake.py index 6d54ab8..040bae0 100644 --- a/examples/snake.py +++ b/examples/snake.py @@ -12,7 +12,7 @@ from rendercanvas.auto import RenderCanvas, loop -canvas = RenderCanvas(present_method=None, size=(640, 480), update_mode="continuous") +canvas = RenderCanvas(title="Snake on $backend", present_method=None, size=(640, 480), update_mode="continuous") context = canvas.get_bitmap_context() diff --git a/rendercanvas/core/renderview-pyodide.js b/rendercanvas/core/renderview-pyodide.js new file mode 100644 index 0000000..e809645 --- /dev/null +++ b/rendercanvas/core/renderview-pyodide.js @@ -0,0 +1,60 @@ +/* global BaseRenderView Element HTMLDivElement HTMLCanvasElement */ + +/** + * Adapter between the JS canvas and the Python canvas. + */ +class PyodideRenderView extends BaseRenderView { + constructor (canvasElement, pycanvas) { + let canvasId = null + let viewElement = null + let wrapperElement = null + + // Turn element id into the element + if (typeof canvasElement === 'string' || canvasElement instanceof String) { + canvasId = canvasElement + canvasElement = document.getElementById(canvasId) + if (!canvasElement) { + throw new Error(`Given canvas id '${canvasId}' does not match an element in the DOM.`) + } + } + + // Get whether we have a wrapper of an actual canvas + if (canvasElement instanceof HTMLCanvasElement) { + viewElement = canvasElement + } else if (canvasElement instanceof Element && canvasElement.classList.contains('renderview-wrapper')) { + wrapperElement = canvasElement + viewElement = document.createElement('canvas') + } else { + let repr = `${canvasElement}` + if (canvasId) { + repr = `id '${canvasId}' -> ` + repr + } + if (canvasElement instanceof HTMLDivElement) { + repr += ' (Maybe you forgot to add class=\'renderview-wrapper\'?)' + } + throw new Error('Given canvas element does not look like a : ' + repr) + } + + super(viewElement, wrapperElement) + this.pycanvas = pycanvas + this.setThrottle(0) + } + + onVisibleChanged (visible) { + this.pycanvas._onVisibleChanged(visible) + } + + onResize (physicalWidth, physicalHeight, pixelRatio) { + // Set canvas physical size + this.viewElement.width = physicalWidth + this.viewElement.height = physicalHeight + // Notify canvas, so the render code knows the size + this.pycanvas._onResize(physicalWidth, physicalHeight, pixelRatio) + } + + onEvent (event) { + this.pycanvas._onEvent(event) + } +} + +window.PyodideRenderView = PyodideRenderView diff --git a/rendercanvas/core/renderview.css b/rendercanvas/core/renderview.css index 8571820..1a7a92c 100644 --- a/rendercanvas/core/renderview.css +++ b/rendercanvas/core/renderview.css @@ -1,4 +1,4 @@ -div.renderview-canvas-wrapper { +div.renderview-wrapper { display: inline-block; position: relative; box-sizing: border-box; @@ -7,16 +7,17 @@ div.renderview-canvas-wrapper { min-height: 32px; } -div.renderview-canvas-wrapper img, -div.renderview-canvas-wrapper canvas { +div.renderview-wrapper img, +div.renderview-wrapper canvas { display: block; box-sizing: border-box; width: 100%; height: 100%; - border-radius: 6px; + border-radius: 5px; + background: #777; } -div.renderview-canvas-wrapper div.renderview-top { +div.renderview-wrapper div.renderview-top { display: none; position: absolute; box-sizing: border-box; @@ -25,20 +26,20 @@ div.renderview-canvas-wrapper div.renderview-top { height: 1.5em; width: 100%; border-top: 1px solid rgba(128, 128, 128, 0.5); - border-radius: 6px 6px 0 0; + border-radius: 5px 5px 0 0; } -div.renderview-canvas-wrapper div.renderview-top span { +div.renderview-wrapper div.renderview-top span { display: inline-block; box-sizing: border-box; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - padding: 0 0.5em 0 0.5em; + padding: 0.1em 0.5em; width: 100%; } -div.renderview-canvas-wrapper div.renderview-resizer { +div.renderview-wrapper div.renderview-resizer { display: none; position: absolute; box-sizing: border-box; @@ -50,20 +51,20 @@ div.renderview-canvas-wrapper div.renderview-resizer { cursor: nwse-resize; } -div.renderview-canvas-wrapper.has-titlebar { - margin-top: 2em; +div.renderview-wrapper.has-titlebar { + margin-top: 2em !important; } -div.renderview-canvas-wrapper.has-titlebar div.renderview-top { +div.renderview-wrapper.has-titlebar div.renderview-top { display: block; } -div.renderview-canvas-wrapper.has-titlebar img, -div.renderview-canvas-wrapper.has-titlebar canvas { - border-top-left-radius: 0; - border-top-right-radius: 0; +div.renderview-wrapper.has-titlebar img, +div.renderview-wrapper.has-titlebar canvas { + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; } -div.renderview-canvas-wrapper.is-resizable div.renderview-resizer { +div.renderview-wrapper.is-resizable div.renderview-resizer { display: block; } \ No newline at end of file diff --git a/rendercanvas/core/renderview.js b/rendercanvas/core/renderview.js index ba3a2c5..5ad67ae 100644 --- a/rendercanvas/core/renderview.js +++ b/rendercanvas/core/renderview.js @@ -1,12 +1,10 @@ /************************************************************************************************* renderview.js - This module implements a common event spec for render targets in a browser. It - implements observers and event listeners and converts these into event - objects/dictionaries. The code is written with little assumptions about the - application, so that it can be shared between different use-cases, such as - rendercanvas backends (pyodide, anywidget, remote-browser), jupyter_rfb, and - other projects. + This module implements a common event spec for render targets in a browser. + The code is written with little assumptions about the application, so that it + can be shared between different use-cases, such as rendercanvas backends + (pyodide, anywidget, remote-browser), jupyter_rfb, and other projects. Code that loads this script should avoid loading it in the global scope. Either by putting it in a ``