diff --git a/examples/rendercanvas.ipynb b/examples/rendercanvas.ipynb
index 6f4d1c3a..7ef3679b 100644
--- a/examples/rendercanvas.ipynb
+++ b/examples/rendercanvas.ipynb
@@ -18,46 +18,13 @@
},
{
"cell_type": "code",
- "execution_count": 1,
+ "execution_count": null,
"id": "e4f9f67d",
"metadata": {},
- "outputs": [
- {
- "data": {
- "application/vnd.jupyter.widget-view+json": {
- "model_id": "f13251ba883843a39be8a9a6b4e76970",
- "version_major": 2,
- "version_minor": 0
- },
- "text/plain": [
- "RFBOutputContext()"
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "application/vnd.jupyter.widget-view+json": {
- "model_id": "fecfdbe30cef480da747f15986df29da",
- "version_major": 2,
- "version_minor": 0
- },
- "text/html": [
- "

snapshot
"
- ],
- "text/plain": [
- "JupyterRenderCanvas(css_height='480.0px', css_width='640.0px')"
- ]
- },
- "execution_count": 1,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
+ "outputs": [],
"source": [
"from rendercanvas.utils.cube import setup_drawing_sync\n",
- "from rendercanvas.auto import RenderCanvas\n",
+ "from rendercanvas.jupyter import RenderCanvas\n",
"\n",
"canvas = RenderCanvas(update_mode=\"continuous\")\n",
"draw_frame = setup_drawing_sync(canvas)\n",
@@ -66,6 +33,17 @@
"canvas"
]
},
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8e9945cb-4541-4df9-968b-ed50b8465e5b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Set title to non-empty string to show the title bar\n",
+ "canvas.set_title(\"Rotating cube\")"
+ ]
+ },
{
"cell_type": "markdown",
"id": "749ffb40",
@@ -76,26 +54,10 @@
},
{
"cell_type": "code",
- "execution_count": 2,
+ "execution_count": null,
"id": "6d0e64b7-a208-4be6-99eb-9f666ab8c2ae",
"metadata": {},
- "outputs": [
- {
- "data": {
- "application/vnd.jupyter.widget-view+json": {
- "model_id": "d4b7240a4cbf4b5686ab11c6c1c480eb",
- "version_major": 2,
- "version_minor": 0
- },
- "text/plain": [
- "Textarea(value='', rows=10)"
- ]
- },
- "execution_count": 2,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
+ "outputs": [],
"source": [
"import ipywidgets\n",
"\n",
@@ -110,6 +72,14 @@
"\n",
"out"
]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "65f86895-9e9a-4c22-8a54-919bd70fd80b",
+ "metadata": {},
+ "outputs": [],
+ "source": []
}
],
"metadata": {
@@ -128,7 +98,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.12.4"
+ "version": "3.14.0"
}
},
"nbformat": 4,
diff --git a/rendercanvas/auto.py b/rendercanvas/auto.py
index 8f93844a..288091b5 100644
--- a/rendercanvas/auto.py
+++ b/rendercanvas/auto.py
@@ -8,6 +8,7 @@
import sys
import importlib
from typing import cast
+import __main__ as main_module
from .core.coreutils import (
logger,
@@ -88,8 +89,8 @@ def backends_generator():
"""Generator that iterates over all sub-generators."""
for gen in [
backends_by_env_vars,
- backends_by_browser,
- backends_by_jupyter,
+ backends_by_pyodide,
+ backends_by_notebook,
backends_by_imported_modules,
backends_by_trying_in_order,
]:
@@ -129,8 +130,16 @@ def get_env_var(*varnames):
yield backend_name, f"{varname} is set"
-def backends_by_jupyter():
+def backends_by_notebook():
"""Generate backend names that are appropriate for the current Jupyter session (if any)."""
+
+ # Detect Marimo: https://github.com/marimo-team/marimo/discussions/8865
+ try:
+ _ = main_module.__marimo__
+ yield "jupyter", "running in Marimo"
+ except AttributeError:
+ pass
+
try:
ip = get_ipython() # type: ignore
except NameError:
@@ -212,8 +221,8 @@ def backends_by_trying_in_order():
yield backend_name, f"{libname} can be imported"
-def backends_by_browser():
- """If python runs in a web browser, we use the pyodide backend."""
+def backends_by_pyodide():
+ """If python runs inside a web browser, we use the pyodide backend."""
# https://pyodide.org/en/stable/usage/faq.html#how-to-detect-that-code-is-run-with-pyodide
# Technically, we could also be in microPython/RustPython/etc. For now, we only target Pyodide.
if sys.platform == "emscripten":
diff --git a/rendercanvas/jupyter.py b/rendercanvas/jupyter.py
index 0c80d33b..deea7084 100644
--- a/rendercanvas/jupyter.py
+++ b/rendercanvas/jupyter.py
@@ -24,7 +24,15 @@ class JupyterRenderCanvas(BaseRenderCanvas, RemoteFrameBuffer):
_rc_canvas_group = JupyterCanvasGroup(loop)
+ # Set jupyter_rfb bitmask to use the old-style events. Pygfx assumes these. We will solve this compat issue
+ # when we refactor rendercanvas event objects.
+ # In the new events:(event_type -> type, time_stamp -> timestamp, pixel_ratio -> ratio
+ _event_compatibility = 1
+
def __init__(self, *args, **kwargs):
+ # The jupyter backend's default title is empty
+ kwargs["title"] = kwargs.get("title", "")
+
super().__init__(*args, **kwargs)
# Internal variables
@@ -33,6 +41,13 @@ def __init__(self, *args, **kwargs):
self._draw_request_time = 0
self._rendercanvas_event_types = set(EventType)
+ # The send_frame() method was added in jupyter_rfb 1.0, but it was always there as a private method,
+ # so we can make it backwards compatible.
+ try:
+ self.send_frame # noqa
+ except AttributeError:
+ self.send_frame = self._rfb_send_frame
+
# Set size, title, etc.
self._final_canvas_init()
@@ -42,7 +57,7 @@ def get_frame(self):
# The result is either a numpy array or None, and this matches
# with what this method is expected to return.
self._time_to_draw()
- return self._last_image
+ return None
# %% Methods to implement RenderCanvas
@@ -74,18 +89,12 @@ def _rc_request_paint(self):
loop.call_soon(self._time_to_paint)
def _rc_force_paint(self):
- # A bit hacky to use the internals of jupyter_rfb this way.
- # This pushes frames to the browser as long as the websocket
- # buffer permits it. It works!
- # But a better way would be `await canvas.wait_draw()`.
- # Todo: would also be nice if jupyter_rfb had a public api for this.
- array = self._last_image
- if array is not None:
- self._rfb_send_frame(array)
+ pass
def _rc_present_bitmap(self, *, data, format, **kwargs):
assert format == "rgba-u8"
self._last_image = np.asarray(data)
+ self.send_frame(self._last_image)
def _rc_set_logical_size(self, width, height):
self.css_width = f"{width}px"
@@ -98,7 +107,8 @@ def _rc_get_closed(self):
return self._is_closed
def _rc_set_title(self, title):
- pass # not supported yet
+ self.title = title
+ self.has_titlebar = bool(title) # show titlebar when a title is set
def _rc_set_cursor(self, cursor):
self.cursor = cursor