diff --git a/bindings/imgui_bundle/__init__.py b/bindings/imgui_bundle/__init__.py index 926fa26a..d0edb1da 100644 --- a/bindings/imgui_bundle/__init__.py +++ b/bindings/imgui_bundle/__init__.py @@ -151,6 +151,12 @@ def has_submodule(submodule_name): from imgui_bundle.pyodide_patch_runners import pyodide_do_patch_runners pyodide_do_patch_runners() +# +# Jupyter notebook: patch hello_imgui.run and immapp.run to work with Jupyter notebook +# +from imgui_bundle.notebook_patch_runners import notebook_do_patch_runners_if_needed # noqa: E402 +notebook_do_patch_runners_if_needed() + # # Override assets folder diff --git a/bindings/imgui_bundle/immapp/immapp_notebook.py b/bindings/imgui_bundle/immapp/immapp_notebook.py index 3f6f145a..972e24d8 100644 --- a/bindings/imgui_bundle/immapp/immapp_notebook.py +++ b/bindings/imgui_bundle/immapp/immapp_notebook.py @@ -1,27 +1,28 @@ # Part of ImGui Bundle - MIT License - Copyright (c) 2022-2023 Pascal Thomet - https://github.com/pthom/imgui_bundle # mypy: disable_error_code=no-untyped-call -from typing import Optional, Callable, Tuple +from typing import Callable, Tuple from imgui_bundle import immapp, hello_imgui -import imgui_bundle GuiFunction = Callable[[], None] ScreenSize = Tuple[int, int] -def run_nb( - gui_function: GuiFunction, - window_title: str = "", - window_size_auto: bool = True, - window_restore_previous_geometry: bool = False, - window_size: ScreenSize = (0, 0), - fps_idle: float = 10.0, - with_implot: bool = True, - with_markdown: bool = True, - with_node_editor: bool = True, - thumbnail_height: int = 0, - thumbnail_ratio: float = 0.0, - run_id: Optional[str] = None, +def _make_gui_with_light_theme(gui_function: GuiFunction) -> GuiFunction: + @immapp.static(was_theme_set=False) + def inner(): + static = inner + if not static.was_theme_set: + hello_imgui.apply_theme(hello_imgui.ImGuiTheme_.white_is_white) + static.was_theme_set = True + gui_function() + return inner + + +def _run_app_function_and_display_image_in_notebook( + app_function: GuiFunction, # A function that runs the app entirely + thumbnail_height: int = 0, + thumbnail_ratio: float = 0.0, ) -> None: """ImguiBundle app runner for jupyter notebook @@ -38,32 +39,6 @@ def run_nb( import cv2 # type: ignore import PIL.Image # pip install pillow from IPython.display import display # type: ignore - from IPython.core.display import HTML # type: ignore - - def run_app(): - nonlocal window_size_auto - if window_size[0] > 0 and window_size[1] > 0: - window_size_auto = False - - @immapp.static(was_theme_set=False) - def gui_with_light_theme(): - static = gui_with_light_theme - if not static.was_theme_set: - hello_imgui.apply_theme(hello_imgui.ImGuiTheme_.white_is_white) - static.was_theme_set = True - gui_function() - - immapp.run( - gui_function=gui_with_light_theme, - window_title=window_title, - window_size_auto=window_size_auto, - window_restore_previous_geometry=window_restore_previous_geometry, - window_size=window_size, - fps_idle=fps_idle, - with_implot=with_implot, - with_markdown=with_markdown, - with_node_editor=with_node_editor, - ) def make_thumbnail(image): resize_ratio = 1.0 @@ -109,7 +84,7 @@ def run_app_and_display_thumb(): nonlocal thumbnail_ratio from imgui_bundle import hello_imgui - run_app() + app_function() app_image = hello_imgui.final_app_window_screenshot() scale = hello_imgui.final_app_window_screenshot_framebuffer_scale() @@ -119,39 +94,48 @@ def run_app_and_display_thumb(): thumbnail = make_thumbnail(app_image) display_image(thumbnail) - def display_run_button(): - html_code = f""" - - - """ - display(HTML(html_code)) - - def display_app_with_run_button(run_id): - """Experiment displaying a "run" button in the notebook below the screenshot. Disabled as of now - If using this, it would be possible to run the app only if the user clicks on the Run button - (and not during normal cell execution). - """ - if run_id is None: - run_app_and_display_thumb() - else: - if hasattr(imgui_bundle, "JAVASCRIPT_RUN_ID"): - print( - "imgui_bundle.JAVASCRIPT_RUN_ID=" - + imgui_bundle.JAVASCRIPT_RUN_ID - + "{run_id=}" - ) - if imgui_bundle.JAVASCRIPT_RUN_ID == run_id: - run_app_and_display_thumb() - else: - print("imgui_bundle: no JAVASCRIPT_RUN_ID") - - # display_app_with_run_button(run_id) run_app_and_display_thumb() + + +def run_nb( + gui_function: GuiFunction, + window_title: str = "", + window_size_auto: bool = True, + window_restore_previous_geometry: bool = False, + window_size: ScreenSize = (0, 0), + fps_idle: float = 10.0, + with_implot: bool = True, + with_markdown: bool = True, + with_node_editor: bool = True, + thumbnail_height: int = 0, + thumbnail_ratio: float = 0.0, +) -> None: + """ImguiBundle app runner for jupyter notebook + + This runner is able to: + * run an ImGui app from a jupyter notebook + * display a thumbnail of the app + a "run" button in the cell output + + The GUI will be rendered with a white theme. + If window_size is left as its default value (0, 0), then the window will autosize. + + thumbnail_height and thumbnail_ratio control the size of the screenshot that is displayed after execution. + """ + def run_app(): + nonlocal window_size_auto + if window_size[0] > 0 and window_size[1] > 0: + window_size_auto = False + + immapp.run( + gui_function=_make_gui_with_light_theme(gui_function), + window_title=window_title, + window_size_auto=window_size_auto, + window_restore_previous_geometry=window_restore_previous_geometry, + window_size=window_size, + fps_idle=fps_idle, + with_implot=with_implot, + with_markdown=with_markdown, + with_node_editor=with_node_editor, + ) + + _run_app_function_and_display_image_in_notebook(run_app, thumbnail_height, thumbnail_ratio) diff --git a/bindings/imgui_bundle/notebook_patch_runners.py b/bindings/imgui_bundle/notebook_patch_runners.py new file mode 100644 index 00000000..9fc6f031 --- /dev/null +++ b/bindings/imgui_bundle/notebook_patch_runners.py @@ -0,0 +1,54 @@ +"""Patch the immapp and hello_imgui runners for Jupyter notebook. +- Will display a screenshot of the final app state in the notebook output. +- Will use a white theme for the GUI. +- Will make the window autosize by default. +- Will patch hello_imgui.run and immapp.run +""" + +def is_in_notebook() -> bool: + try: + import sys + return 'ipykernel' in sys.modules and 'IPython' in sys.modules + except ImportError: + return False + + +def notebook_do_patch_runners_if_needed() -> None: + if not is_in_notebook(): + return + + from imgui_bundle import immapp, hello_imgui + from imgui_bundle.immapp.immapp_notebook import _run_app_function_and_display_image_in_notebook + + def patch_runner(run_backup): + def patched_run(*args, **kwargs): + # Are we using hello_imgui.RunnerParams, or are we using raw parameters (i.e. a gui_function + other parameters)? + use_gui_function = (len(args) >= 1 and callable(args[0])) or "gui_function" in kwargs + + # Set window_size_auto to True if not set, to make the window smaller and reduce the size of the screenshot + if use_gui_function and "window_size" not in kwargs and "window_size_auto" not in kwargs: + kwargs["window_size_auto"] = True + + # If using a gui function, patch it so that it uses a white theme + if use_gui_function: + gui_function = args[0] if len(args) >= 1 else kwargs.get("gui_function", None) + if gui_function: + from imgui_bundle.immapp.immapp_notebook import _make_gui_with_light_theme + gui_function_with_light_theme = _make_gui_with_light_theme(gui_function) + if "gui_function" in kwargs: + kwargs["gui_function"] = gui_function_with_light_theme + else: + args = (gui_function_with_light_theme, *args[1:]) + + # define a function that will run the full app, then run this function + # via _run_app_function_and_display_image_in_notebook + def app_function(): + run_backup(*args, **kwargs) + _run_app_function_and_display_image_in_notebook(app_function) + return patched_run + + immapp_run_backup = immapp.run + immapp.run = patch_runner(immapp_run_backup) + + hello_imgui_run_backup = hello_imgui.run + hello_imgui.run = patch_runner(hello_imgui_run_backup)