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