Skip to content

scenex.imgui #

ImGui controls for interactive scenex visualization.

This module provides ImGui-based interactive controls for scenex scenes. It adds an overlay widget panel that allows real-time manipulation of scene parameters, view properties, and node attributes through sliders, checkboxes, color pickers, and other widgets.

The controls are automatically generated from Pydantic model fields, providing a consistent interface without requiring manual widget creation.

Requirements

This module requires additional dependencies::

pip install scenex[imgui]

This installs imgui_bundle and pygfx with ImGui support.

Main Function

add_imgui_controls : function Add an interactive ImGui control panel to a view

Example

Add controls to a scene with an image::

import scenex as snx
from scenex.imgui import add_imgui_controls

# Create a scene with some content
image = snx.Image(data=my_array)
view = snx.View(scene=snx.Scene(children=[image]))

# Add interactive controls
add_imgui_controls(view)

# Show and run
snx.show(view)
snx.run()

The control panel will display collapsible sections for the view and each child node, with automatically generated widgets for adjusting properties like opacity, colors, transforms, and node-specific parameters.

Notes

Only works with pygfx backend

Functions:

add_imgui_controls #

add_imgui_controls(view: View) -> None

Add an interactive ImGui control panel to a view.

Creates an overlay control panel that allows real-time manipulation of view properties and scene node attributes through automatically generated widgets. The panel displays collapsible sections for the view and each child node in the scene, with widgets dynamically created based on Pydantic field types.

Parameters:

  • view #

    (View) –

    The view to control.

Raises:

  • NotImplementedError

    If the view is not using the pygfx backend.

  • RuntimeError

    If the pygfx renderer has not been initialized yet.

  • ImportError

    If required dependencies (imgui_bundle, pygfx) are not installed.

Notes
  • Only works with the pygfx backend
  • The control panel is rendered as an overlay on the canvas. It is not (currently) restricted to a specific area of the canvas
  • Current architecture necessitates this function be called AFTER setting up camera interaction strategies and/or view event filters. All view events are intercepted and may not propagate to the user's view filter or camera filter, but a best attempt is made to propagate events that do not interact with the ImGui control panel.
  • Widgets are automatically generated from Pydantic field metadata:
    • Literal types → dropdown menus
    • bool → checkbox
    • int/float with bounds → slider
    • int/float without bounds → input field
    • Color → color picker
    • Colormap → colormap preview button

Examples:

Basic usage with an image::

>>> import scenex as snx
>>> import numpy as np
>>> from scenex.imgui import add_imgui_controls

>>> my_array = np.random.rand(100, 100).astype(np.float32)
>>> image = snx.Image(data=my_array)
>>> view = snx.View(scene=snx.Scene(children=[image]))
>>> snx.show(view)
Canvas(...)
>>> add_imgui_controls(view)
>>> snx.run()

The control panel will show sections for: - View properties (camera, layout, etc.) - Image node (colormap, clims, opacity, etc.) - Points node (size, color, symbol, etc.) - Mesh node (color, opacity, blending, etc.)

Source code in src/scenex/imgui/_controls.py
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
def add_imgui_controls(view: View) -> None:
    """Add an interactive ImGui control panel to a view.

    Creates an overlay control panel that allows real-time manipulation of view
    properties and scene node attributes through automatically generated widgets.
    The panel displays collapsible sections for the view and each child node in
    the scene, with widgets dynamically created based on Pydantic field types.

    Parameters
    ----------
    view : View
        The view to control.

    Raises
    ------
    NotImplementedError
        If the view is not using the pygfx backend.
    RuntimeError
        If the pygfx renderer has not been initialized yet.
    ImportError
        If required dependencies (imgui_bundle, pygfx) are not installed.

    Notes
    -----
    - Only works with the pygfx backend
    - The control panel is rendered as an overlay on the canvas. It is not (currently)
      restricted to a specific area of the canvas
    - Current architecture necessitates this function be called AFTER setting up camera
      interaction strategies and/or view event filters. All view events are intercepted
      and may not propagate to the user's view filter or camera filter, but a best
      attempt is made to propagate events that do not interact with the ImGui control
      panel.
    - Widgets are automatically generated from Pydantic field metadata:
        * Literal types → dropdown menus
        * bool → checkbox
        * int/float with bounds → slider
        * int/float without bounds → input field
        * Color → color picker
        * Colormap → colormap preview button

    Examples
    --------
    Basic usage with an image::

        >>> import scenex as snx
        >>> import numpy as np
        >>> from scenex.imgui import add_imgui_controls

        >>> my_array = np.random.rand(100, 100).astype(np.float32)
        >>> image = snx.Image(data=my_array)
        >>> view = snx.View(scene=snx.Scene(children=[image]))
        >>> snx.show(view)
        Canvas(...)
        >>> add_imgui_controls(view)
        >>> snx.run()

    The control panel will show sections for:
    - View properties (camera, layout, etc.)
    - Image node (colormap, clims, opacity, etc.)
    - Points node (size, color, symbol, etc.)
    - Mesh node (color, opacity, blending, etc.)
    """
    snx_canvas_model = view.canvas
    try:
        snx_canvas_adaptor = snx_canvas_model._get_adaptors(backend="pygfx")[0]
        snx_view_adaptor = view._get_adaptors(backend="pygfx")[0]
    except (KeyError, IndexError):
        warnings.warn(
            "No pygfx adaptor found view/canvas; cannot add imgui controls.",
            stacklevel=2,
        )
        return

    render_canv = cast("CanvasAdaptor", snx_canvas_adaptor)._snx_get_native()

    if not (
        isinstance(snx_canvas_adaptor, PygfxCanvasAdaptor)
        and isinstance(snx_view_adaptor, PygfxViewAdaptor)
        and isinstance(render_canv, BaseRenderCanvas)
    ):
        raise NotImplementedError(
            "Imgui controls can currently only be added to a canvas backed by pygfx."
        )
    if not snx_canvas_adaptor._renderer:
        raise RuntimeError("The pygfx renderer has not been initialized yet.")

    imgui_renderer = ImguiRenderer(
        device=snx_canvas_adaptor._renderer.device,
        canvas=render_canv,  # pyright: ignore[reportArgumentType] (incorrect hint)
    )

    if implot.get_current_context() is None:
        implot.create_context()  # must run after ImGui context exists

    @imgui_renderer.set_gui  # type: ignore [untyped-decorator]
    def _update_gui() -> None:
        render_imgui_view_controls(view)

    @render_canv.request_draw
    def _update() -> None:
        snx_canvas_adaptor._draw()
        imgui_renderer.render()

    class ImguiEventFilter:
        internal_filter: Callable[[Event], bool] | None = None

        def __call__(self, event: Event) -> bool:
            # NOTE: As the scenex event system matures
            # It may capture more events (notably, keypresses).
            # We will have to intercept scenex events here if that occurs
            if isinstance(event, MouseMoveEvent):
                move_dict = {"x": event.canvas_pos[0], "y": event.canvas_pos[1]}
                imgui_renderer._on_mouse_move(move_dict)
                if move_dict.get("stop_propagation", False):
                    return True
            if isinstance(event, MousePressEvent):
                btn = imgui_filter.convert_btn(event.buttons)
                press_dict = {"button": btn, "event_type": "pointer_down"}
                imgui_renderer._on_mouse(press_dict)
                if press_dict.get("stop_propagation", False):
                    return True
            if isinstance(event, MouseReleaseEvent):
                btn = imgui_filter.convert_btn(event.buttons)
                release_dict = {"button": btn, "event_type": "pointer_up"}
                imgui_renderer._on_mouse(release_dict)
                if release_dict.get("stop_propagation", False):
                    return True
            if isinstance(event, WheelEvent):
                # FIXME: Validate correct delta sign
                wheel_dict = {"dx": event.angle_delta[0], "dy": event.angle_delta[1]}
                imgui_renderer._on_wheel(wheel_dict)
                if wheel_dict.get("stop_propagation", False):
                    return True

            if self.internal_filter is None:
                return False
            return self.internal_filter(event)

        def convert_btn(self, btn: MouseButton) -> int:
            if btn & MouseButton.LEFT:
                return 1
            if btn & MouseButton.RIGHT:
                return 2
            if btn & MouseButton.MIDDLE:
                return 3
            return 0

    imgui_filter = ImguiEventFilter()
    imgui_filter.internal_filter = view.set_event_filter(imgui_filter)