Skip to content

Commit

Permalink
Python: add notebook_do_patch_runners_if_needed
Browse files Browse the repository at this point in the history
  • Loading branch information
pthom committed Dec 18, 2024
1 parent db76618 commit 8745086
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 77 deletions.
6 changes: 6 additions & 0 deletions bindings/imgui_bundle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
138 changes: 61 additions & 77 deletions bindings/imgui_bundle/immapp/immapp_notebook.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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"""
<script>
function btnClick_{run_id}(btn) {{
// alert("btnClick_{run_id}");
cell_element = btn.parentElement.parentElement.parentElement.parentElement.parentElement;
cell_idx = Jupyter.notebook.get_cell_elements().index(cell_element)
IPython.notebook.kernel.execute("imgui_bundle.JAVASCRIPT_RUN_ID='{run_id}'")
Jupyter.notebook.execute_cells([cell_idx])
}}
</script>
<button onClick="btnClick_{run_id}(this)">Run</button>
"""
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)
54 changes: 54 additions & 0 deletions bindings/imgui_bundle/notebook_patch_runners.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 8745086

Please sign in to comment.