-
Notifications
You must be signed in to change notification settings - Fork 83
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
177 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
"""This module provides a way to render hello imgui and immapp applications in the browser using pyodide. | ||
It works by monkey patching the `hello_imgui.run` and `immapp.run` functions to use a custom implementation | ||
that integrates with the browser's requestAnimationFrame API. | ||
""" | ||
from dataclasses import dataclass | ||
from typing import Callable | ||
from imgui_bundle import hello_imgui, immapp | ||
from enum import Enum | ||
from pyodide.ffi import create_proxy # type: ignore | ||
import js # type: ignore | ||
import gc | ||
import logging | ||
|
||
logging.basicConfig(level=logging.DEBUG) | ||
logger = logging.getLogger("pyodide_imgui_render") | ||
logger.setLevel(logging.INFO) | ||
|
||
def _log(msg: str): | ||
# logger.info(msg) | ||
# js.console.log("pyodide_imgui_render: " + msg) | ||
pass | ||
|
||
|
||
class _JsAnimationRenderer: | ||
"""Make it possible to call a python function to do rendering at each javascript frame.""" | ||
render_fn: Callable[[], None] # A python function that performs rendering | ||
stop_requested: bool # A flag to request the animation loop to stop | ||
main_loop_proxy: js.Proxy # A javascript proxy to the main_loop method that is called at each frame | ||
|
||
def __init__(self, render_fn: Callable[[], None]): | ||
self.render_fn = render_fn | ||
self.stop_requested = False | ||
self.main_loop_proxy = create_proxy(self.main_loop) | ||
|
||
def main_loop(self, _timestamp): | ||
if self.stop_requested: | ||
if self.main_loop_proxy: | ||
_log("_JsAnimationRenderer: received stop_requested => destroying main_loop_proxy") | ||
self.main_loop_proxy.destroy() | ||
self.main_loop_proxy = None | ||
return | ||
try: | ||
self.render_fn() | ||
except Exception as e: | ||
self.request_stop() | ||
raise e | ||
|
||
# Schedule the next frame | ||
js.requestAnimationFrame(self.main_loop_proxy) | ||
|
||
def start(self): | ||
_log("_JsAnimationRenderer.start()") | ||
self.stop_requested = False | ||
js.requestAnimationFrame(self.main_loop_proxy) | ||
|
||
def request_stop(self): | ||
_log("_JsAnimationRenderer:stop() => set stop_requested=True") | ||
self.stop_requested = True | ||
|
||
|
||
@dataclass | ||
class _RenderLifeCycleFunctions: | ||
setup: Callable[[], None] = None | ||
render: Callable[[], None] = None | ||
tear_down: Callable[[], None] = None | ||
|
||
|
||
class _HelloImGuiOrImmApp(Enum): | ||
HELLO_IMGUI = 1 | ||
IMMAPP = 2 | ||
|
||
|
||
def _arg_to_render_lifecycle_functions(himgui_or_immapp: _HelloImGuiOrImmApp, *args, **kwargs) -> _RenderLifeCycleFunctions: | ||
"""Converts the arguments to the correct render lifecycle functions, | ||
depending on the type of arguments passed and whether it is a hello_imgui or immapp application.""" | ||
functions = _RenderLifeCycleFunctions() | ||
|
||
if himgui_or_immapp == _HelloImGuiOrImmApp.HELLO_IMGUI: | ||
render_module = hello_imgui.manual_render | ||
elif himgui_or_immapp == _HelloImGuiOrImmApp.IMMAPP: | ||
render_module = immapp.manual_render | ||
else: | ||
raise ValueError("Invalid value for himgui_or_immapp") | ||
|
||
_log(f"{len(args)=} args: {args} kwargs: {kwargs}") | ||
use_runner_params = (len(args) >= 1 and isinstance(args[0], hello_imgui.RunnerParams)) or "runner_params" in kwargs | ||
use_simple_params = (len(args) >= 1 and isinstance(args[0], hello_imgui.SimpleRunnerParams)) or "simple_params" in kwargs | ||
use_gui_function = (len(args) >= 1 and callable(args[0])) or "gui_function" in kwargs | ||
|
||
if use_runner_params: | ||
_log("overload with RunnerParams") | ||
functions.setup = lambda: render_module.setup_from_runner_params(*args, **kwargs) | ||
elif use_simple_params: | ||
_log("overload with SimpleRunnerParams") | ||
functions.setup = lambda: render_module.setup_from_simple_runner_params(*args, **kwargs) | ||
elif use_gui_function: | ||
_log("overload with callable") | ||
functions.setup = lambda:render_module.setup_from_gui_function(*args, **kwargs) | ||
else: | ||
raise ValueError("Invalid arguments") | ||
|
||
functions.render = render_module.render | ||
|
||
functions.tear_down = render_module.tear_down | ||
|
||
return functions | ||
|
||
|
||
class _ManualRenderJs: | ||
"""Manages the ManualRender lifecycle (from HelloImGui or ImmApp) and integrates with _JsAnimationRenderer.""" | ||
js_animation_renderer: _JsAnimationRenderer | None = None | ||
is_running: bool = False | ||
render_lifecycle_functions: _RenderLifeCycleFunctions | None = None | ||
|
||
def _stop(self): | ||
"""Stops the current rendering loop and tears down the renderer.""" | ||
if not self.is_running: | ||
return | ||
if self.js_animation_renderer is not None: | ||
_log("_ManualRenderJs: Stopping js_animation_renderer") | ||
self.js_animation_renderer.request_stop() | ||
self.js_animation_renderer = None | ||
|
||
try: | ||
self.render_lifecycle_functions.tear_down() | ||
self.render_lifecycle_functions = None | ||
_log("_ManualRenderJs: HelloImGuiRunnerJs: Renderer torn down successfully.") | ||
except Exception as e: | ||
import traceback | ||
js.console.error(f"_ManualRenderJs: Error during Renderer teardown: {e}\n{traceback.format_exc()}") | ||
finally: | ||
# Force garbage collection to free resources | ||
gc.collect() | ||
|
||
def _run(self, himgui_or_immapp: _HelloImGuiOrImmApp, *args, **kwargs): | ||
if self.is_running: | ||
self._stop() | ||
self.is_running = True | ||
|
||
self.render_lifecycle_functions = _arg_to_render_lifecycle_functions(himgui_or_immapp, *args, **kwargs) | ||
self.render_lifecycle_functions.setup() | ||
self.js_animation_renderer = _JsAnimationRenderer(self.render_lifecycle_functions.render) | ||
self.js_animation_renderer.start() | ||
|
||
def run_immapp(self, *args, **kwargs): | ||
self._run(_HelloImGuiOrImmApp.IMMAPP, *args, **kwargs) | ||
|
||
def run_hello_imgui(self, *args, **kwargs): | ||
self._run(_HelloImGuiOrImmApp.HELLO_IMGUI, *args, **kwargs) | ||
|
||
|
||
_MANUAL_RENDER_JS: _ManualRenderJs | None = None | ||
|
||
|
||
def pyodide_do_patch_runners(): | ||
# Instantiate global runners | ||
global _MANUAL_RENDER_JS | ||
print("pyodide_do_patch_runners()") | ||
_log("_MANUAL_RENDER_JS: Version 1") | ||
_MANUAL_RENDER_JS = _ManualRenderJs() | ||
# Monkey patch the hello_imgui.run and immapp.run function to use the js version | ||
hello_imgui.run = _MANUAL_RENDER_JS.run_hello_imgui | ||
immapp.run = _MANUAL_RENDER_JS.run_immapp |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters