Content-Length: 36725 | pFad | http://github.com/fastplotlib/fastplotlib/pull/849.patch
thub.com
From 18383fcfb89bd7dd56d7f6b4c37b52c8ab2ad86c Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Sat, 31 May 2025 20:19:17 -0400
Subject: [PATCH 01/10] basic prototype working :D
---
examples/guis/imgui_decorator.py | 30 ++++++++++
fastplotlib/layouts/_imgui_figure.py | 85 +++++++++++++++++++++++-----
fastplotlib/ui/_base.py | 24 ++++----
3 files changed, 114 insertions(+), 25 deletions(-)
create mode 100644 examples/guis/imgui_decorator.py
diff --git a/examples/guis/imgui_decorator.py b/examples/guis/imgui_decorator.py
new file mode 100644
index 00000000..ab1c8c26
--- /dev/null
+++ b/examples/guis/imgui_decorator.py
@@ -0,0 +1,30 @@
+"""
+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()
+figure[0, 0].add_line(np.random.rand(100))
+
+
+@figure.add_gui(location="right", title="yay", size=100)
+def gui():
+ if imgui.button("reset data"):
+ figure[0, 0].graphics[0].data[:, 1] = np.random.rand(100)
+
+
+figure.show(maintain_aspect=False)
+
+fpl.loop.run()
diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py
index c5489023..cfb148a1 100644
--- a/fastplotlib/layouts/_imgui_figure.py
+++ b/fastplotlib/layouts/_imgui_figure.py
@@ -150,34 +150,91 @@ 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.
+
Parameters
----------
gui: EdgeWindow
A GUI EdgeWindow instance
+ location: str, "right" | "bottom"
+ window location
+
+ title: str
+ window title
+
+ size: int
+ width or height of the window depending on location
+
+ window_flags: imgui.WindowFlags_, default imgui.WindowFlags_.no_collapse | imgui.WindowFlags_.no_resize,
+ imgui.WindowFlags_ enum
+
+ 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():
+ if imgui.button("reset data"):
+ figure[0, 0].graphics[0].data[:, 1] = np.random.rand(100)
+
+ figure.show(maintain_aspect=False)
+
"""
- 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_func):
+ 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 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):
+ ew = EdgeWindow(
+ figure=self,
+ size=size,
+ location=location,
+ title=title,
+ update_call=_gui_func,
+ window_flags=window_flags
+ )
+ window_location = location
+ else:
+ ew = _gui_func
+ window_location = location
+
+ self.guis[window_location] = ew
- if self.guis[location] is not None:
- raise ValueError(f"GUI already exists in the desired location: {location}")
+ self._fpl_reset_layout()
- self.guis[location] = gui
+ return _gui_func
+
+ if not isinstance(gui, EdgeWindow):
+ return decorator
- self._fpl_reset_layout()
+ decorator(gui)
def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]:
"""
diff --git a/fastplotlib/ui/_base.py b/fastplotlib/ui/_base.py
index e31dd8d4..784eca4a 100644
--- a/fastplotlib/ui/_base.py
+++ b/fastplotlib/ui/_base.py
@@ -1,5 +1,5 @@
+from collections.abc import Callable
from typing import Literal
-import numpy as np
from imgui_bundle import imgui
@@ -31,7 +31,6 @@ def update(self):
class Window(BaseGUI):
"""Base class for imgui windows drawn within Figures"""
-
pass
@@ -42,8 +41,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,
):
@@ -64,7 +64,7 @@ def __init__(
title: str
window title
- window_flags: int
+ window_flags: imgui.WindowFlags_
window flag enum, valid flags are:
.. code-block:: py
@@ -94,10 +94,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__()
@@ -115,6 +115,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"""
@@ -185,11 +190,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
@@ -198,8 +200,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()
From 8214849cf5ee0f2e5c0eb61d853b5313f54ea4d2 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Sun, 1 Jun 2025 00:21:35 -0400
Subject: [PATCH 02/10] add ChangeFlag
---
fastplotlib/ui/utils.py | 46 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 46 insertions(+)
create mode 100644 fastplotlib/ui/utils.py
diff --git a/fastplotlib/ui/utils.py b/fastplotlib/ui/utils.py
new file mode 100644
index 00000000..620e2e8d
--- /dev/null
+++ b/fastplotlib/ui/utils.py
@@ -0,0 +1,46 @@
+class ChangeFlag:
+ """
+ A flag that helps detect whether an imgui UI has been changed by the user.
+ Basically, once True, always True.
+
+ Example::
+
+ changed = ChangeFlag(False)
+
+ changed.value, bah = (False, False)
+
+ print(changed.value)
+
+ changed.value, bah = (True, False)
+
+ print(changed.value)
+
+ changed.value, bah = (False, False)
+
+ print(changed.value)
+
+ """
+ def __init__(self, value: bool):
+ self._value = bool(value)
+
+ @property
+ def value(self) -> bool:
+ return self._value
+
+ @value.setter
+ def value(self, value: bool):
+ if value:
+ self._value = True
+
+ def __bool__(self):
+ return self.value
+
+ def __or__(self, other):
+ if other:
+ self._value = True
+
+ def __eq__(self, other):
+ return self.value == other
+
+ def force_value(self, value):
+ self._value = value
From 3c998ee54cec527e8584a4c0d680c374336b0406 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Sun, 1 Jun 2025 00:21:54 -0400
Subject: [PATCH 03/10] docstring, comments
---
fastplotlib/layouts/_imgui_figure.py | 61 +++++++++++++++++++++++-----
1 file changed, 50 insertions(+), 11 deletions(-)
diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py
index cfb148a1..9d8e9d83 100644
--- a/fastplotlib/layouts/_imgui_figure.py
+++ b/fastplotlib/layouts/_imgui_figure.py
@@ -161,24 +161,30 @@ def add_gui(
"""
Add a GUI to the Figure. GUIs can be added to the left or bottom edge.
- Can also be used as a decorator.
+ 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
+ window location, ignored if not decorating
title: str
- window title
+ window title, ignored if not decorating
size: int
- width or height of the window depending on location
+ width or height of the window depending on location, ignored if not decorating
window_flags: imgui.WindowFlags_, default imgui.WindowFlags_.no_collapse | imgui.WindowFlags_.no_resize,
- imgui.WindowFlags_ enum
+ imgui.WindowFlags_ enum, ignored if not decorating
Examples
--------
@@ -200,6 +206,33 @@ def gui():
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()
+
"""
def decorator(_gui_func):
@@ -212,7 +245,8 @@ def decorator(_gui_func):
raise ValueError(f"GUI already exists in the desired location: {location}")
if not isinstance(gui, EdgeWindow):
- ew = EdgeWindow(
+ # being used as a decorator, create an EdgeWindow
+ edge_window = EdgeWindow(
figure=self,
size=size,
location=location,
@@ -220,20 +254,25 @@ def decorator(_gui_func):
update_call=_gui_func,
window_flags=window_flags
)
- window_location = location
+ window_location = location # creating this reference is required
else:
- ew = _gui_func
- window_location = location
+ edge_window = _gui_func # creating this reference is required
+ window_location = location # creating this reference is required
- self.guis[window_location] = ew
+ # store the gui
+ self.guis[window_location] = edge_window
+ # redo the layout
self._fpl_reset_layout()
+ # return function being decorated
return _gui_func
if not isinstance(gui, EdgeWindow):
+ # if decorating
return decorator
+ # if not decorating
decorator(gui)
def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]:
From eae0fa07be17bf4d4bb85551e5f9dae20eaab9c7 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Sun, 1 Jun 2025 00:24:07 -0400
Subject: [PATCH 04/10] fix
---
fastplotlib/ui/utils.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/fastplotlib/ui/utils.py b/fastplotlib/ui/utils.py
index 620e2e8d..e4288b12 100644
--- a/fastplotlib/ui/utils.py
+++ b/fastplotlib/ui/utils.py
@@ -36,8 +36,7 @@ def __bool__(self):
return self.value
def __or__(self, other):
- if other:
- self._value = True
+ return self._value | other
def __eq__(self, other):
return self.value == other
From 832f6fdeee295742aeaea0fb2ec36f367dcf8de4 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Sun, 1 Jun 2025 20:55:22 -0400
Subject: [PATCH 05/10] fig as argument to add_gui() decorated function,
comments
---
examples/guis/imgui_decorator.py | 14 +++---
fastplotlib/layouts/_imgui_figure.py | 50 +++++++++++++--------
fastplotlib/ui/_base.py | 1 +
fastplotlib/{ui/utils.py => utils/imgui.py} | 1 +
4 files changed, 42 insertions(+), 24 deletions(-)
rename fastplotlib/{ui/utils.py => utils/imgui.py} (99%)
diff --git a/examples/guis/imgui_decorator.py b/examples/guis/imgui_decorator.py
index ab1c8c26..f4efe995 100644
--- a/examples/guis/imgui_decorator.py
+++ b/examples/guis/imgui_decorator.py
@@ -15,16 +15,20 @@
import fastplotlib as fpl
from imgui_bundle import imgui
-figure = fpl.Figure()
+figure = fpl.Figure(size=(700, 560))
figure[0, 0].add_line(np.random.rand(100))
-@figure.add_gui(location="right", title="yay", size=100)
-def gui():
+@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"):
- figure[0, 0].graphics[0].data[:, 1] = np.random.rand(100)
+ fig_local[0, 0].graphics[0].data[:, 1] = np.random.rand(100)
figure.show(maintain_aspect=False)
-fpl.loop.run()
+# 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()
diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py
index 9d8e9d83..5a96e729 100644
--- a/fastplotlib/layouts/_imgui_figure.py
+++ b/fastplotlib/layouts/_imgui_figure.py
@@ -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
@@ -151,12 +154,13 @@ def _draw_imgui(self) -> imgui.ImDrawData:
return imgui.get_draw_data()
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,
+ 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.
@@ -175,16 +179,16 @@ def add_gui(
A GUI EdgeWindow instance, if not decorating
location: str, "right" | "bottom"
- window location, ignored if not decorating
+ window location, used if decorating
title: str
- window title, ignored if not decorating
+ window title, used if decorating
size: int
- width or height of the window depending on location, ignored if not decorating
+ 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, ignored if not decorating
+ imgui.WindowFlags_ enum, used if decorating
Examples
--------
@@ -200,9 +204,9 @@ def add_gui(
@figure.add_gui(location="right", title="yay", size=100)
- def gui():
+ 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"):
- figure[0, 0].graphics[0].data[:, 1] = np.random.rand(100)
+ fig[0, 0].graphics[0].data[:, 1] = np.random.rand(100)
figure.show(maintain_aspect=False)
@@ -233,16 +237,22 @@ def update(self):
self._amplitude = amplitude
self.compute_data()
+ # create GUI instance and add to the figure
+ gui = GUI(figure)
+ figure.add_gui(gui)
+
"""
- def decorator(_gui_func):
+ def decorator(_gui: EdgeWindow | Callable):
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}")
+ raise ValueError(
+ f"GUI already exists in the desired location: {location}"
+ )
if not isinstance(gui, EdgeWindow):
# being used as a decorator, create an EdgeWindow
@@ -251,12 +261,14 @@ def decorator(_gui_func):
size=size,
location=location,
title=title,
- update_call=_gui_func,
- window_flags=window_flags
+ 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_func # creating this reference is required
+ edge_window = _gui # creating this reference is required
window_location = location # creating this reference is required
# store the gui
@@ -266,14 +278,14 @@ def decorator(_gui_func):
self._fpl_reset_layout()
# return function being decorated
- return _gui_func
+ return _gui
if not isinstance(gui, EdgeWindow):
# if decorating
return decorator
# if not decorating
- decorator(gui)
+ decorator(gui, self)
def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]:
"""
diff --git a/fastplotlib/ui/_base.py b/fastplotlib/ui/_base.py
index 784eca4a..d37bda09 100644
--- a/fastplotlib/ui/_base.py
+++ b/fastplotlib/ui/_base.py
@@ -31,6 +31,7 @@ def update(self):
class Window(BaseGUI):
"""Base class for imgui windows drawn within Figures"""
+
pass
diff --git a/fastplotlib/ui/utils.py b/fastplotlib/utils/imgui.py
similarity index 99%
rename from fastplotlib/ui/utils.py
rename to fastplotlib/utils/imgui.py
index e4288b12..32c6f9f6 100644
--- a/fastplotlib/ui/utils.py
+++ b/fastplotlib/utils/imgui.py
@@ -20,6 +20,7 @@ class ChangeFlag:
print(changed.value)
"""
+
def __init__(self, value: bool):
self._value = bool(value)
From c7b92e161bf028e41a0d47af4f547dab3259f052 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Sun, 1 Jun 2025 21:10:29 -0400
Subject: [PATCH 06/10] update basic example to use changeflag
---
examples/guis/imgui_basic.py | 36 +++++++++++++++-------------
fastplotlib/layouts/_imgui_figure.py | 2 +-
2 files changed, 21 insertions(+), 17 deletions(-)
diff --git a/examples/guis/imgui_basic.py b/examples/guis/imgui_basic.py
index eac39121..e803255e 100644
--- a/examples/guis/imgui_basic.py
+++ b/examples/guis/imgui_basic.py
@@ -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
@@ -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
diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py
index 5a96e729..4637a9c7 100644
--- a/fastplotlib/layouts/_imgui_figure.py
+++ b/fastplotlib/layouts/_imgui_figure.py
@@ -285,7 +285,7 @@ def decorator(_gui: EdgeWindow | Callable):
return decorator
# if not decorating
- decorator(gui, self)
+ decorator(gui)
def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]:
"""
From 15baeb76c36621d4d56899257d3e3d9a78b3ee37 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Sun, 1 Jun 2025 21:43:03 -0400
Subject: [PATCH 07/10] add figure.remove_gui(), better handling in add_gui
---
fastplotlib/layouts/_imgui_figure.py | 40 +++++++++++++++++++++++++---
1 file changed, 37 insertions(+), 3 deletions(-)
diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py
index 4637a9c7..c787f4a6 100644
--- a/fastplotlib/layouts/_imgui_figure.py
+++ b/fastplotlib/layouts/_imgui_figure.py
@@ -244,6 +244,11 @@ def update(self):
"""
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}"
@@ -280,13 +285,42 @@ def decorator(_gui: EdgeWindow | Callable):
# return function being decorated
return _gui
- if not isinstance(gui, EdgeWindow):
- # if decorating
+ if gui is None:
+ # assume decorating
return decorator
- # if not decorating
+ # 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"location not valid, valid locations are: {GUI_EDGES}, you have passed: {location}"
+ )
+
+ gui = self.guis.pop(location)
+
+ # reset to None for this location
+ self.guis[location] = None
+
+ # return EdgeWindow instance, it can be added again later
+ return gui
+
def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]:
"""
Get rect for the portion of the canvas that the pygfx renderer draws to,
From 3a93189abedddbbe0020b4cc7461fdaa67bbb194 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Sun, 1 Jun 2025 23:33:31 -0400
Subject: [PATCH 08/10] bugfix
---
fastplotlib/layouts/_imgui_figure.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py
index c787f4a6..e3809f20 100644
--- a/fastplotlib/layouts/_imgui_figure.py
+++ b/fastplotlib/layouts/_imgui_figure.py
@@ -274,7 +274,7 @@ def decorator(_gui: EdgeWindow | Callable):
window_location = location # creating this reference is required
else:
edge_window = _gui # creating this reference is required
- window_location = location # creating this reference is required
+ window_location = _gui.location # creating this reference is required
# store the gui
self.guis[window_location] = edge_window
From 69053358b37129540589fb95384673415c8089be Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Sun, 29 Jun 2025 01:30:30 -0400
Subject: [PATCH 09/10] @Figure.append_gui decorator
---
examples/guis/imgui_basic.py | 3 +-
examples/guis/imgui_decorator.py | 53 +++++++++++++++++---
fastplotlib/layouts/_imgui_figure.py | 29 +++++++++++
fastplotlib/ui/__init__.py | 1 +
fastplotlib/ui/_base.py | 6 ++-
fastplotlib/{utils/imgui.py => ui/_utils.py} | 0
6 files changed, 83 insertions(+), 9 deletions(-)
rename fastplotlib/{utils/imgui.py => ui/_utils.py} (100%)
diff --git a/examples/guis/imgui_basic.py b/examples/guis/imgui_basic.py
index e803255e..9511a1b0 100644
--- a/examples/guis/imgui_basic.py
+++ b/examples/guis/imgui_basic.py
@@ -14,8 +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.utils.imgui import ChangeFlag
+from fastplotlib.ui import EdgeWindow, ChangeFlag
from imgui_bundle import imgui
# make some initial data
diff --git a/examples/guis/imgui_decorator.py b/examples/guis/imgui_decorator.py
index f4efe995..3118e137 100644
--- a/examples/guis/imgui_decorator.py
+++ b/examples/guis/imgui_decorator.py
@@ -12,20 +12,61 @@
import numpy as np
+import imageio.v3 as iio
import fastplotlib as fpl
from imgui_bundle import imgui
-figure = fpl.Figure(size=(700, 560))
-figure[0, 0].add_line(np.random.rand(100))
+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.add_gui(location="right", title="window", size=200)
+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
- if imgui.button("reset data"):
- fig_local[0, 0].graphics[0].data[:, 1] = np.random.rand(100)
+ 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(maintain_aspect=False)
+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
diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py
index e3809f20..96adc197 100644
--- a/fastplotlib/layouts/_imgui_figure.py
+++ b/fastplotlib/layouts/_imgui_figure.py
@@ -292,6 +292,35 @@ def decorator(_gui: EdgeWindow | Callable):
# EdgeWindow instance passed
decorator(gui)
+ def append_gui(self, gui: Callable = None, location: str = None):
+ 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 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
diff --git a/fastplotlib/ui/__init__.py b/fastplotlib/ui/__init__.py
index a1e57a9c..0ff1d513 100644
--- a/fastplotlib/ui/__init__.py
+++ b/fastplotlib/ui/__init__.py
@@ -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
\ No newline at end of file
diff --git a/fastplotlib/ui/_base.py b/fastplotlib/ui/_base.py
index d37bda09..f6e43544 100644
--- a/fastplotlib/ui/_base.py
+++ b/fastplotlib/ui/_base.py
@@ -202,7 +202,11 @@ def draw_window(self):
imgui.push_id(self._id_counter)
# draw imgui UI elements into window
- self._update_call()
+ if isinstance(self._update_call, list):
+ for update_call in self._update_call:
+ update_call()
+ else:
+ self._update_call()
# pop ID
imgui.pop_id()
diff --git a/fastplotlib/utils/imgui.py b/fastplotlib/ui/_utils.py
similarity index 100%
rename from fastplotlib/utils/imgui.py
rename to fastplotlib/ui/_utils.py
From b3dda823de1ee9368e0bf71323d794462acbbad0 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Sun, 29 Jun 2025 02:04:43 -0400
Subject: [PATCH 10/10] docstring
---
examples/guis/imgui_decorator.py | 1 -
fastplotlib/layouts/_imgui_figure.py | 13 +++++++++++++
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/examples/guis/imgui_decorator.py b/examples/guis/imgui_decorator.py
index 3118e137..8e51e9a0 100644
--- a/examples/guis/imgui_decorator.py
+++ b/examples/guis/imgui_decorator.py
@@ -37,7 +37,6 @@ def gui(fig_local): # figure is the only argument, so you can use it within the
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"])
diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py
index 96adc197..406d1f0d 100644
--- a/fastplotlib/layouts/_imgui_figure.py
+++ b/fastplotlib/layouts/_imgui_figure.py
@@ -293,6 +293,19 @@ def decorator(_gui: EdgeWindow | Callable):
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.")
--- a PPN by Garber Painting Akron. With Image Size Reduction included!Fetched URL: http://github.com/fastplotlib/fastplotlib/pull/849.patch
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy