nb.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. # Part of ImGui Bundle - MIT License - Copyright (c) 2022-2026 Pascal Thomet - https://github.com/pthom/imgui_bundle
  2. """
  3. Notebook-specific API for immapp - simplified async GUI execution in Jupyter notebooks.
  4. This module provides convenient functions for running ImGui apps in Jupyter notebooks:
  5. - run(): Blocking execution with screenshot output (delegates to patched immapp.run)
  6. - start(): Non-blocking execution, returns auto-started asyncio.Task
  7. - stop(): Stop the currently running async GUI
  8. - is_running(): Check if an async GUI is currently running
  9. """
  10. import asyncio
  11. from typing import Optional
  12. # from imgui_bundle.immapp import AddOnsParams
  13. from imgui_bundle.hello_imgui import RunnerParams, SimpleRunnerParams
  14. # Global state for tracking the current async GUI task
  15. _current_task: Optional[asyncio.Task] = None
  16. def is_running() -> bool:
  17. """Check if an async GUI is currently running.
  18. Returns:
  19. True if a GUI started with start() is currently running, False otherwise
  20. Example:
  21. if immapp.nb.is_running():
  22. print("GUI is running")
  23. else:
  24. print("No GUI running")
  25. """
  26. global _current_task
  27. return _current_task is not None and not _current_task.done()
  28. def stop() -> None:
  29. """Stop the currently running async GUI.
  30. This sets the app_shall_exit flag, causing the GUI to close gracefully.
  31. The GUI will clean up and the task will complete.
  32. Example:
  33. task = immapp.nb.start(my_gui)
  34. # ... work in other cells ...
  35. immapp.nb.stop() # Close the GUI
  36. """
  37. if not is_running():
  38. print("Warning: No GUI is currently running")
  39. return
  40. from imgui_bundle import hello_imgui
  41. hello_imgui.get_runner_params().app_shall_exit = True
  42. def run(*args, **kwargs):
  43. """Run an ImGui application in blocking mode with screenshot output.
  44. This is a convenience wrapper that delegates to the patched immapp.run(),
  45. which runs the GUI, waits for it to close, and displays a screenshot in
  46. the notebook cell output.
  47. Signatures:
  48. run(runner_params: RunnerParams, addons_params: Optional[AddOnsParams] = None)
  49. run(simple_params: SimpleRunnerParams, addons_params: Optional[AddOnsParams] = None)
  50. run(gui_function, window_title="", ..., with_implot=False, ...)
  51. Example:
  52. def my_gui():
  53. imgui.text("Hello from notebook!")
  54. immapp.nb.run(my_gui, window_title="My App", window_size_auto=True)
  55. # Screenshot appears in cell output after closing
  56. Additional parameters to controls the thumbnail screenshot:
  57. * thumbnail_ratio: (default=1.0)
  58. You can use it to change the size of the thumbnail.
  59. Passing 0.5 will create a thumbnail half the width of the window.
  60. * thumbnail_height: (default=0)
  61. You can use it to set a fixed height for the thumbnail (in pixels).
  62. If 0, the height is computed from the app window size.
  63. (choose only one of the two parameters to control size)
  64. """
  65. from imgui_bundle import immapp
  66. return immapp.run(*args, **kwargs)
  67. def start(*args, **kwargs) -> asyncio.Task:
  68. """Start an ImGui application in non-blocking mode for interactive notebook use.
  69. This function starts a GUI that runs asynchronously, allowing you to:
  70. - Continue executing other notebook cells while the GUI is open
  71. - Modify variables from other cells and see updates in real-time
  72. - Run multiple sequential GUIs (close one before starting another)
  73. The function automatically applies notebook-friendly defaults:
  74. - Light theme for better visibility
  75. - window_size_auto=True (if not specified)
  76. - top_most=True (if not specified)
  77. If a GUI is already running, it will be automatically stopped with a warning.
  78. Signatures:
  79. start(runner_params: RunnerParams, addons_params: Optional[AddOnsParams] = None)
  80. start(simple_params: SimpleRunnerParams, addons_params: Optional[AddOnsParams] = None)
  81. start(gui_function, window_title="", ..., with_implot=False, ...)
  82. Returns:
  83. asyncio.Task: The task running the GUI (can be awaited if needed)
  84. Example:
  85. def my_gui():
  86. global freq
  87. changed, freq = imgui.slider_float("Frequency", freq, 0.1, 10.0)
  88. # Start the GUI
  89. task = immapp.nb.start(my_gui, with_implot=True, window_size_auto=True)
  90. print("GUI started")
  91. # In another cell: modify variables
  92. freq = 5.0 # GUI updates immediately!
  93. # In another cell: close the GUI
  94. immapp.nb.stop()
  95. """
  96. global _current_task
  97. # Import here to avoid circular imports
  98. from imgui_bundle import hello_imgui, immapp
  99. from imgui_bundle.immapp.immapp_notebook import _make_gui_with_light_theme
  100. # Capture the previous task (if any) and signal it to exit. We do NOT
  101. # block here: actually waiting for it to finish must happen on the event
  102. # loop, inside the new task — otherwise we'd freeze the loop and the
  103. # previous task could never reach its tear_down().
  104. previous_task: Optional[asyncio.Task] = None
  105. if is_running():
  106. print("Warning: A GUI is already running. Stopping it first...")
  107. hello_imgui.get_runner_params().app_shall_exit = True
  108. previous_task = _current_task
  109. # Determine which signature is being used and build the run_async coroutine
  110. if len(args) >= 1:
  111. first_arg = args[0]
  112. # Case 1: RunnerParams
  113. if isinstance(first_arg, RunnerParams):
  114. runner_params = first_arg
  115. addons_params = args[1] if len(args) > 1 else kwargs.get("addons_params", None)
  116. # Apply notebook-friendly defaults
  117. # if runner_params.app_window_params.window_geometry.size_auto is None:
  118. # runner_params.app_window_params.window_geometry.size_auto = True
  119. # if not runner_params.app_window_params.top_most:
  120. # runner_params.app_window_params.top_most = True
  121. # Wrap GUI function with light theme
  122. if runner_params.callbacks.show_gui is not None:
  123. original_gui = runner_params.callbacks.show_gui
  124. runner_params.callbacks.show_gui = _make_gui_with_light_theme(original_gui)
  125. run_coro_factory = lambda: immapp.run_async(runner_params, addons_params) # noqa: E731
  126. # Case 2: SimpleRunnerParams
  127. elif isinstance(first_arg, SimpleRunnerParams):
  128. simple_params = first_arg
  129. addons_params = args[1] if len(args) > 1 else kwargs.get("addons_params", None)
  130. # Apply notebook-friendly defaults
  131. # if not simple_params.window_size_auto:
  132. # simple_params.window_size_auto = True
  133. # if not simple_params.top_most:
  134. # simple_params.top_most = True
  135. # Wrap GUI function with light theme
  136. if simple_params.gui_function is not None:
  137. original_gui = simple_params.gui_function
  138. simple_params.gui_function = _make_gui_with_light_theme(original_gui)
  139. run_coro_factory = lambda: immapp.run_async(simple_params, addons_params) # noqa: E731
  140. # Case 3: GUI function with keyword arguments
  141. elif callable(first_arg):
  142. gui_function = first_arg
  143. # Wrap GUI function with light theme
  144. gui_with_theme = _make_gui_with_light_theme(gui_function)
  145. # Apply notebook-friendly defaults to kwargs
  146. if "window_size_auto" not in kwargs and "window_size" not in kwargs:
  147. kwargs["window_size_auto"] = True
  148. extra_args = args[1:]
  149. run_coro_factory = lambda: immapp.run_async(gui_with_theme, *extra_args, **kwargs) # noqa: E731
  150. else:
  151. raise TypeError(f"First argument must be RunnerParams, SimpleRunnerParams, or a callable GUI function, got {type(first_arg)}")
  152. else:
  153. raise TypeError("start() requires at least one argument")
  154. async def _drain_then_run():
  155. if previous_task is not None:
  156. try:
  157. await previous_task
  158. except Exception:
  159. pass # Don't let a previous failure mask the new launch
  160. await run_coro_factory()
  161. _current_task = asyncio.create_task(_drain_then_run())
  162. return _current_task