Thanks to visit codestin.com
Credit goes to github.com

Skip to content

@Figure.add_gui and append_gui decorators #849

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
37 changes: 20 additions & 17 deletions examples/guis/imgui_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import fastplotlib as fpl

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

# make some initial data
Expand Down Expand Up @@ -48,41 +48,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
74 changes: 74 additions & 0 deletions examples/guis/imgui_decorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""
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 imageio.v3 as iio
import fastplotlib as fpl
from imgui_bundle import imgui

img_data = iio.imread(f"imageio:camera.png").astype(np.float32)

xs = np.linspace(0, 2 * np.pi, 100)
ys = np.sin(xs)
line_data = np.column_stack([xs, ys])

figure = fpl.Figure(shape=(2, 1), size=(700, 660))
figure[0, 0].add_image(img_data, name="image")
figure[1, 0].add_line(line_data, name="sine")

noise_sigma = 0.0

img_options = ["camera.png", "astronaut.png", "chelsea.png"]
img_index = 0
@figure.add_gui(location="right", title="window", size=300)
def gui(fig_local): # figure is the only argument, so you can use it within the local scope of the GUI function
global img_data
global img_index

global noise_sigma

clicked, img_index = imgui.combo("image", img_index, img_options)
if clicked:
fig_local[0, 0].delete_graphic(fig_local[0, 0]["image"])
img_data = iio.imread(f"imageio:{img_options[img_index]}")
fig_local[0, 0].add_image(img_data, name="image")

change, noise_sigma = imgui.slider_float("noise sigma", v=noise_sigma, v_min=0.0, v_max=100)
if change or clicked:
fig_local[0, 0]["image"].data = img_data + np.random.normal(
loc=0.0,
scale=noise_sigma,
size=img_data.size
).reshape(img_data.shape)


# You can put all the GUI elements within on GUI function
# or split them across multiple functions and use the `append_gui` decorator
freq = 1.0
@figure.append_gui(location="right")
def gui2(fig_local):
global freq
change, freq = imgui.slider_float("freq", v=freq, v_min=0.1, v_max=10)
if change:
ys = np.sin(xs * freq)
fig_local[1, 0]["sine"].data[:, 1] = ys


figure.show()
figure[1, 0].camera.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()
206 changes: 195 additions & 11 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,215 @@ 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 append_gui(self, gui: Callable = None, location: str = None):
"""
Append to an existing GUI. Can also be used as a decorator.

Parameters
----------
gui: Callable
function that creates imgui elements

location: str, "right" or "bottom"
the existing GUI window to append more UI elements to

"""

if location is None:
raise ValueError("Must provide GUI location to append to.")

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 self.guis[location] is None:
raise ValueError(
f"No GUI at given location to append to: {location}"
)

def decorator(_gui: Callable):
gui = self.guis[location]
if isinstance(gui._update_call, list):
gui._update_call.append(_gui)
else:
gui._update_call = [gui._update_call]
gui._update_call.append(partial(_gui, self))

return _gui

if gui is None:
return decorator

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"location not valid, valid locations are: {GUI_EDGES}, you have passed: {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
1 change: 1 addition & 0 deletions fastplotlib/ui/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from ._base import BaseGUI, Window, EdgeWindow, Popup, GUI_EDGES
from ._subplot_toolbar import SubplotToolbar
from .right_click_menus import StandardRightClickMenu, ColormapPicker
from ._utils import ChangeFlag
Loading
Loading