Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 20 additions & 16 deletions examples/guis/imgui_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

# subclass from EdgeWindow to make a custom ImGUI Window to place inside the figure!
from fastplotlib.ui import EdgeWindow
from fastplotlib.utils.imgui import ChangeFlag
from imgui_bundle import imgui

# make some initial data
Expand Down Expand Up @@ -48,41 +49,44 @@ def __init__(self, figure, size, location, title):
# sigma for gaussian noise
self._sigma = 0.0

# a flag that once True, always remains True
self._color_changed = ChangeFlag(False)
self._data_changed = ChangeFlag(False)


def update(self):
# the UI will be used to modify the line
self._line = figure[0, 0]["sine-wave"]
# force flag values to reset
self._color_changed.force_value(False)
self._data_changed.force_value(False)

# get the current line RGB values
rgb_color = self._line.colors[:-1]
# make color picker
changed_color, rgb = imgui.color_picker3("color", col=rgb_color)
self._color_changed.value, rgb = imgui.color_picker3("color", col=rgb_color)

# get current line color alpha value
alpha = self._line.colors[-1]
# make float slider
changed_alpha, new_alpha = imgui.slider_float("alpha", v=alpha, v_min=0.0, v_max=1.0)
self._color_changed.value, new_alpha = imgui.slider_float("alpha", v=alpha, v_min=0.0, v_max=1.0)

# if RGB or alpha changed
if changed_color | changed_alpha:
# if RGB or alpha flag indicates a change
if self._color_changed:
# set new color along with alpha
self._line.colors = [*rgb, new_alpha]

# example of a slider, you can also use input_float
changed, amplitude = imgui.slider_float("amplitude", v=self._amplitude, v_max=10, v_min=0.1)
if changed:
# set y values
self._amplitude = amplitude
self._set_data()

# slider for thickness
changed, thickness = imgui.slider_float("thickness", v=self._line.thickness, v_max=50.0, v_min=2.0)
if changed:
self._line.thickness = thickness

# example of a slider, you can also use input_float
self._data_changed.value, self._amplitude = imgui.slider_float("amplitude", v=self._amplitude, v_max=10, v_min=0.1)

# slider for gaussian noise
changed, sigma = imgui.slider_float("noise-sigma", v=self._sigma, v_max=1.0, v_min=0.0)
if changed:
self._sigma = sigma
self._data_changed.value, self._sigma = imgui.slider_float("noise-sigma", v=self._sigma, v_max=1.0, v_min=0.0)

# data flag indicates change
if self._data_changed:
self._set_data()

# reset button
Expand Down
34 changes: 34 additions & 0 deletions examples/guis/imgui_decorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""
ImGUI Decorator
===============

Create imgui UIs quickly using a decorator!

See the imgui docs for extensive examples on how to create all UI elements: https://pyimgui.readthedocs.io/en/latest/reference/imgui.core.html#imgui.core.begin_combo
"""

# test_example = true
# sphinx_gallery_pygfx_docs = 'screenshot'


import numpy as np
import fastplotlib as fpl
from imgui_bundle import imgui

figure = fpl.Figure(size=(700, 560))
figure[0, 0].add_line(np.random.rand(100))


@figure.add_gui(location="right", title="window", size=200)
def gui(fig_local): # figure is the only argument, so you can use it within the local scope of the GUI function
if imgui.button("reset data"):
fig_local[0, 0].graphics[0].data[:, 1] = np.random.rand(100)
Comment thread
kushalkolar marked this conversation as resolved.
Outdated


figure.show(maintain_aspect=False)

# NOTE: fpl.loop.run() should not be used for interactive sessions
# See the "JupyterLab and IPython" section in the user guide
if __name__ == "__main__":
print(__doc__)
fpl.loop.run()
166 changes: 154 additions & 12 deletions fastplotlib/layouts/_imgui_figure.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from __future__ import annotations
from collections.abc import Callable
from functools import partial
from pathlib import Path
from typing import Literal, Iterable

Expand Down Expand Up @@ -150,34 +153,173 @@ def _draw_imgui(self) -> imgui.ImDrawData:

return imgui.get_draw_data()

def add_gui(self, gui: EdgeWindow):
def add_gui(
self,
gui: EdgeWindow = None,
location: Literal["right", "bottom"] = "right",
title="GUI Window",
size: int = 200,
window_flags: imgui.WindowFlags_ = imgui.WindowFlags_.no_collapse
| imgui.WindowFlags_.no_resize,
):
"""
Add a GUI to the Figure. GUIs can be added to the left or bottom edge.

Can also be used as a decorator, see examples docstring and examples gallery.

For a list of imgui elements see: https://pyimgui.readthedocs.io/en/latest/reference/imgui.core.html#imgui.core.begin_combo

Note that the API docs for ``pyimgui`` do not match up exactly with ``imgui-bundle`` which we use in
fastplotlib. Unfortunately the API docs for imgui-bundle are nonexistent (as far as we know). See the
"imgui" section in the docs User Guide which includes tips on how to develop imgui UIs.

Parameters
----------
gui: EdgeWindow
A GUI EdgeWindow instance
A GUI EdgeWindow instance, if not decorating

location: str, "right" | "bottom"
window location, used if decorating

title: str
window title, used if decorating

size: int
width or height of the window depending on location, used if decorating

window_flags: imgui.WindowFlags_, default imgui.WindowFlags_.no_collapse | imgui.WindowFlags_.no_resize,
imgui.WindowFlags_ enum, used if decorating

Examples
--------

As a decorator::

import numpy as np
import fastplotlib as fpl
from imgui_bundle import imgui

figure = fpl.Figure()
figure[0, 0].add_line(np.random.rand(100))


@figure.add_gui(location="right", title="yay", size=100)
def gui(fig): # figure is the only argument, so you can use it within the local scope of the GUI function
if imgui.button("reset data"):
fig[0, 0].graphics[0].data[:, 1] = np.random.rand(100)

figure.show(maintain_aspect=False)

Subclass EdgeWindow::

import numpy as np
import fastplotlib as fpl
from fastplotlib.ui import EdgeWindow

figure = fpl.Figure()
figure[0, 0].add_line(np.sin(np.linspace(0, np.pi * 4, 0.1)), name="sine")

class GUI(EdgeWindow):
def __init__(self, figure, location="right", size=200, title="My GUI", amplitude=1.0)
self._figure = figure

self._amplitude = 1

def compute_data(self):
ampl = self._amplitude
new_data = ampl * np.sin(np.linspace(0, np.pi * 4, 0.1))
self._figure[0, 0]["sine"].data[:, 1] = new_data

def update(self):
# gui update function
changed, amplitude = imgui.slider_float("amplitude", v=self._amplitude, v_max=10, v_min=0.1)
if changed:
self._amplitude = amplitude
self.compute_data()

# create GUI instance and add to the figure
gui = GUI(figure)
figure.add_gui(gui)

"""
if not isinstance(gui, EdgeWindow):
raise TypeError(
f"GUI must be of type: {EdgeWindow} you have passed a {type(gui)}"
)

location = gui.location
def decorator(_gui: EdgeWindow | Callable):
if not callable(_gui) and not isinstance(_gui, EdgeWindow):
raise TypeError(
"figure.add_gui() must be used as a decorator, or `gui` must be an `EdgeWindow` instance"
)

if location not in GUI_EDGES:
raise ValueError(
f"GUI does not have a valid location, valid locations are: {GUI_EDGES}, you have passed: {location}"
)

if self.guis[location] is not None:
raise ValueError(
f"GUI already exists in the desired location: {location}"
)

if not isinstance(gui, EdgeWindow):
# being used as a decorator, create an EdgeWindow
edge_window = EdgeWindow(
figure=self,
size=size,
location=location,
title=title,
update_call=partial(
_gui, self
), # provide figure instance in scope of the gui function
window_flags=window_flags,
)
window_location = location # creating this reference is required
else:
edge_window = _gui # creating this reference is required
window_location = _gui.location # creating this reference is required

# store the gui
self.guis[window_location] = edge_window

# redo the layout
self._fpl_reset_layout()

# return function being decorated
return _gui

if gui is None:
# assume decorating
return decorator

# EdgeWindow instance passed
decorator(gui)

def remove_gui(self, location: str) -> EdgeWindow:
"""
Remove an imgui UI

Parameters
----------
location: str
"right" | "bottom"

Returns
-------
EdgeWindow | Callable
The removed EdgeWindow instance

"""

if location not in GUI_EDGES:
raise ValueError(
f"GUI does not have a valid location, valid locations are: {GUI_EDGES}, you have passed: {location}"
f"location not valid, valid locations are: {GUI_EDGES}, you have passed: {location}"
)

if self.guis[location] is not None:
raise ValueError(f"GUI already exists in the desired location: {location}")
gui = self.guis.pop(location)

self.guis[location] = gui
# reset to None for this location
self.guis[location] = None

self._fpl_reset_layout()
# return EdgeWindow instance, it can be added again later
return gui

def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]:
"""
Expand Down
23 changes: 13 additions & 10 deletions fastplotlib/ui/_base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from collections.abc import Callable
from typing import Literal
import numpy as np

from imgui_bundle import imgui

Expand Down Expand Up @@ -42,8 +42,9 @@ def __init__(
size: int,
location: Literal["bottom", "right"],
title: str,
window_flags: int = imgui.WindowFlags_.no_collapse
window_flags: imgui.WindowFlags_ = imgui.WindowFlags_.no_collapse
| imgui.WindowFlags_.no_resize,
update_call: Callable = None,
*args,
**kwargs,
):
Expand All @@ -64,7 +65,7 @@ def __init__(
title: str
window title

window_flags: int
window_flags: imgui.WindowFlags_
window flag enum, valid flags are:

.. code-block:: py
Expand Down Expand Up @@ -94,10 +95,10 @@ def __init__(
imgui.WindowFlags_.no_inputs

*args
additional args for the GUI
additional args

**kwargs
additional kwargs for teh GUI
additional kwargs
"""
super().__init__()

Expand All @@ -115,6 +116,11 @@ def __init__(

self._figure.canvas.add_event_handler(self._set_rect, "resize")

if update_call is None:
self._update_call = self.update
else:
self._update_call = update_call

@property
def size(self) -> int | None:
"""width or height of the edge window"""
Expand Down Expand Up @@ -185,11 +191,8 @@ def get_rect(self) -> tuple[int, int, int, int]:
def draw_window(self):
"""helps simplify using imgui by managing window creation & position, and pushing/popping the ID"""
# window position & size
x, y, w, h = self.get_rect()
imgui.set_next_window_size((self.width, self.height))
imgui.set_next_window_pos((self.x, self.y))
# imgui.set_next_window_pos((x, y))
# imgui.set_next_window_size((w, h))
flags = self._window_flags

# begin window
Expand All @@ -198,8 +201,8 @@ def draw_window(self):
# push ID to prevent conflict between multiple figs with same UI
imgui.push_id(self._id_counter)

# draw stuff from subclass into window
self.update()
# draw imgui UI elements into window
self._update_call()

# pop ID
imgui.pop_id()
Expand Down
Loading
Loading