From 494f657deac944ea96c78d48ccb95174d2ed6923 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 15 May 2025 00:09:45 -0400 Subject: [PATCH 1/5] bugfix imgui right click menu --- fastplotlib/ui/right_click_menus/_standard_menu.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fastplotlib/ui/right_click_menus/_standard_menu.py b/fastplotlib/ui/right_click_menus/_standard_menu.py index 53a553f3..4bb59c51 100644 --- a/fastplotlib/ui/right_click_menus/_standard_menu.py +++ b/fastplotlib/ui/right_click_menus/_standard_menu.py @@ -31,7 +31,7 @@ def __init__(self, figure, fa_icons): # whether the right click menu is currently open or not self.is_open: bool = False - def get_subplot(self) -> PlotArea | bool: + def get_subplot(self) -> PlotArea | bool | None: """get the subplot that a click occurred in""" if self._last_right_click_pos is None: return False @@ -40,6 +40,9 @@ def get_subplot(self) -> PlotArea | bool: if subplot.viewport.is_inside(*self._last_right_click_pos): return subplot + # not inside a subplot + return False + def cleanup(self): """called when the popup disappears""" self.is_open = False From 841a9f1b819050efc7cb0e5c5380dc3c1acb45c3 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 15 May 2025 05:21:22 -0400 Subject: [PATCH 2/5] almost fixed rect manager --- examples/guis/many_ui.py | 50 +++++++ examples/guis/sine_cosine_funcs.py | 186 +++++++++++++++++++++++++++ fastplotlib/layouts/_engine.py | 20 ++- fastplotlib/layouts/_figure.py | 11 +- fastplotlib/layouts/_frame.py | 14 +- fastplotlib/layouts/_imgui_figure.py | 6 + fastplotlib/layouts/_rect.py | 95 +++++++++----- fastplotlib/layouts/_subplot.py | 3 +- fastplotlib/ui/_base.py | 12 +- 9 files changed, 345 insertions(+), 52 deletions(-) create mode 100644 examples/guis/many_ui.py create mode 100644 examples/guis/sine_cosine_funcs.py diff --git a/examples/guis/many_ui.py b/examples/guis/many_ui.py new file mode 100644 index 00000000..07bc8bed --- /dev/null +++ b/examples/guis/many_ui.py @@ -0,0 +1,50 @@ +""" +Many UIs surrounding the render area +==================================== + +Mostly a test example with imgui windows on every edge of the Figure. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +# subclass from EdgeWindow to make a custom ImGUI Window to place inside the figure! +from fastplotlib.ui import EdgeWindow +from imgui_bundle import imgui + +# make some initial data +np.random.seed(0) + +xs = np.linspace(0, np.pi * 10, 100) +ys = np.sin(xs) + np.random.normal(scale=0.5, size=100) +data = np.column_stack([xs, ys]) + + +# make a figure +figure = fpl.Figure(shape=(3, 2), size=(700, 560)) + +for subplot in figure: + subplot.add_line(data, thickness=1, colors="r", name="sine-wave", uniform_color=True) + + +class Window1(EdgeWindow): + def __init__(self, figure, size, location, title): + super().__init__(figure=figure, size=size, location=location, title=title) + self._title = title + + def update(self): + if imgui.button("reset data"): + for subplot in self._figure: + subplot["sine-wave"].data[:, 1] = np.sin(xs) + np.random.normal(scale=0.5, size=100) + + +for i, location in enumerate(["left", "right", "top", "bottom"]): + gui = Window1(figure, 100, location, f"ui-{i}") + figure.add_gui(gui) + +figure.show() + +fpl.loop.run() diff --git a/examples/guis/sine_cosine_funcs.py b/examples/guis/sine_cosine_funcs.py new file mode 100644 index 00000000..70d948bf --- /dev/null +++ b/examples/guis/sine_cosine_funcs.py @@ -0,0 +1,186 @@ +""" +Sine and Cosine functions +========================= + +Identical to the Unit Circle example but you can change the angular frequencies using a UI + +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + + +import numpy as np +import fastplotlib as fpl +from fastplotlib.ui import EdgeWindow +from imgui_bundle import imgui + + +# initial frequency coefficients for sine and cosine functions +P = 1 +Q = 1 + + +# helper function to make a circle +def make_circle(center, radius: float, p, q, n_points: int) -> np.ndarray: + theta = np.linspace(0, 2 * np.pi, n_points) + xs = radius * np.cos(theta * p) + ys = radius * np.sin(theta * q) + + return np.column_stack([xs, ys]) + center + + +# we can define this layout using "extents", i.e. min and max ranges on the canvas +# (x_min, x_max, y_min, y_max) +# extents can be defined as fractions as shown here +extents = [ + (0, 0.5, 0, 1), # circle subplot + (0.5, 1, 0, 0.5), # sine subplot + (0.5, 1, 0.5, 1), # cosine subplot +] + +# create a figure with 3 subplots +figure = fpl.Figure( + extents=extents, + names=["circle", "sin", "cos"], + size=(700, 560) +) + +# set more descriptive figure titles +figure["circle"].title = "sin(x*p) over cos(x*q)" +figure["sin"].title = "sin(x * p)" +figure["cos"].title = "cos(x * q)" + +# set the axes to intersect at (0, 0, 0) to better illustrate the unit circle +for subplot in figure: + subplot.axes.intersection = (0, 0, 0) + subplot.toolbar = False # reduce clutter + +figure["sin"].camera.maintain_aspect = False +figure["cos"].camera.maintain_aspect = False + +# create sine and cosine data +xs = np.linspace(0, 2 * np.pi, 360) +sine = np.sin(xs * P) +cosine = np.cos(xs * Q) + +# circle data +circle_data = make_circle(center=(0, 0), p=P, q=Q, radius=1, n_points=360) + +# make the circle line graphic, set the cmap transform using the sine function +circle_graphic = figure["circle"].add_line( + circle_data, thickness=4, cmap="bwr", cmap_transform=sine +) + +# line to show the circle radius +# use it to indicate the current position of the sine and cosine selctors (below) +radius_data = np.array([[0, 0, 0], [*circle_data[0], 0]]) +circle_radius_graphic = figure["circle"].add_line( + radius_data, thickness=6, colors="magenta" +) + +# sine line graphic, cmap transform set from the sine function +sine_graphic = figure["sin"].add_line( + sine, thickness=10, cmap="bwr", cmap_transform=sine +) + +# cosine line graphic, cmap transform set from the sine function +# illustrates the sine function values on the cosine graphic +cosine_graphic = figure["cos"].add_line( + cosine, thickness=10, cmap="bwr", cmap_transform=sine +) + +# add linear selectors to the sine and cosine line graphics +sine_selector = sine_graphic.add_linear_selector() +cosine_selector = cosine_graphic.add_linear_selector() + + +def set_circle_cmap(ev): + # sets the cmap transforms + + cmap_transform = ev.graphic.data[:, 1] # y-val data of the sine or cosine graphic + for g in [sine_graphic, cosine_graphic]: + g.cmap.transform = cmap_transform + + # set circle cmap transform + circle_graphic.cmap.transform = cmap_transform + +# when the sine or cosine graphic is clicked, the cmap_transform +# of the sine, cosine and circle line graphics are all set from +# the y-values of the clicked line +sine_graphic.add_event_handler(set_circle_cmap, "click") +cosine_graphic.add_event_handler(set_circle_cmap, "click") + + +def set_x_val(ev): + # used to sync the two selectors + value = ev.info["value"] + index = ev.get_selected_index() + + sine_selector.selection = value + cosine_selector.selection = value + + circle_radius_graphic.data[1, :-1] = circle_data[index] + +# add same event handler to both graphics +sine_selector.add_event_handler(set_x_val, "selection") +cosine_selector.add_event_handler(set_x_val, "selection") + +# initial selection value +sine_selector.selection = 50 + + +class GUIWindow(EdgeWindow): + def __init__(self, figure, size, location, title): + super().__init__(figure=figure, size=size, location=location, title=title) + + self._p = 1 + self._q = 1 + + def _set_data(self): + global sine_graphic, cosine_graphic, circle_graphic, circle_radius_graphic, circle_data + + # make new data + sine = np.sin(xs * self._p) + cosine = np.cos(xs * self._q) + circle_data = make_circle(center=(0, 0), p=self._p, q=self._q, radius=1, n_points=360) + + + # set the graphics + sine_graphic.data[:, 1] = sine + cosine_graphic.data[:, 1] = cosine + circle_graphic.data[:, :2] = circle_data + circle_radius_graphic.data[1, :-1] = circle_data[sine_selector.get_selected_index()] + + def update(self): + flag_set_data = False + + changed, self._p = imgui.input_int("P", v=self._p, step_fast=2) + if changed: + flag_set_data = True + + changed, self._q = imgui.input_int("Q", v=self._q, step_fast=2) + if changed: + flag_set_data = True + + if flag_set_data: + self._set_data() + + +gui = GUIWindow( + figure=figure, + size=80, + location="top", + title="Freq. coeffs" +) + +figure.add_gui(gui) + +figure.show() + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index 877a7fba..3181d78b 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -31,7 +31,8 @@ def __init__( self, renderer: pygfx.WgpuRenderer, subplots: np.ndarray[Subplot], - canvas_rect: tuple[float, float], + canvas_rect: tuple[float, float, float, float], + render_rect: tuple[float, float, float, float], moveable: bool, resizeable: bool, ): @@ -41,6 +42,7 @@ def __init__( self._renderer = renderer self._subplots: np.ndarray[Subplot] = subplots.ravel() self._canvas_rect = canvas_rect + self._render_rect = render_rect self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array( [np.nan, np.nan] @@ -82,7 +84,7 @@ def _inside_render_rect(self, subplot: Subplot, pos: tuple[int, int]) -> bool: return False - def canvas_resized(self, canvas_rect: tuple): + def canvas_resized(self, canvas_rect: tuple, render_rect: tuple): """ called by figure when canvas is resized @@ -94,8 +96,10 @@ def canvas_resized(self, canvas_rect: tuple): """ self._canvas_rect = canvas_rect + self._render_rect = render_rect + for subplot in self._subplots: - subplot.frame.canvas_resized(canvas_rect) + subplot.frame.canvas_resized(canvas_rect, render_rect) def _highlight_resize_handler(self, subplot: Subplot, ev): if self._active_action == "resize": @@ -132,6 +136,7 @@ def __init__( renderer, subplots: np.ndarray[Subplot], canvas_rect: tuple, + render_rect: tuple, moveable=True, resizeable=True, ): @@ -144,7 +149,7 @@ def __init__( """ - super().__init__(renderer, subplots, canvas_rect, moveable, resizeable) + super().__init__(renderer, subplots, canvas_rect, render_rect, moveable, resizeable) self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array( [np.nan, np.nan] @@ -291,7 +296,7 @@ def set_rect(self, subplot: Subplot, rect: tuple | list | np.ndarray): """ - new_rect = RectManager(*rect, self._canvas_rect) + new_rect = RectManager(*rect, self._canvas_rect, self.render_rect) extent = new_rect.extent # check for overlaps for s in self._subplots: @@ -318,7 +323,7 @@ def set_extent(self, subplot: Subplot, extent: tuple | list | np.ndarray): """ - new_rect = RectManager.from_extent(extent, self._canvas_rect) + new_rect = RectManager.from_extent(extent, self._canvas_rect, self.render_rect) extent = new_rect.extent # check for overlaps for s in self._subplots: @@ -337,6 +342,7 @@ def __init__( renderer, subplots: np.ndarray[Subplot], canvas_rect: tuple[float, float, float, float], + render_rect: tuple[float, float, float, float], shape: tuple[int, int], ): """ @@ -346,7 +352,7 @@ def __init__( """ super().__init__( - renderer, subplots, canvas_rect, moveable=False, resizeable=False + renderer, subplots, canvas_rect, render_rect, moveable=False, resizeable=False ) # {Subplot: (row_ix, col_ix)}, dict mapping subplots to their row and col index in the grid layout diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index a1bae965..a41dce2b 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -398,7 +398,8 @@ def __init__( self._layout = GridLayout( self.renderer, subplots=self._subplots, - canvas_rect=self.get_pygfx_render_area(), + canvas_rect=(0, 0, *canvas.get_logical_size()), + render_rect=self.get_pygfx_render_area(), shape=shape, ) @@ -406,7 +407,8 @@ def __init__( self._layout = WindowLayout( self.renderer, subplots=self._subplots, - canvas_rect=self.get_pygfx_render_area(), + canvas_rect=(0, 0, *canvas.get_logical_size()), + render_rect=self.get_pygfx_render_area() ) self._underlay_camera = UnderlayCamera() @@ -754,7 +756,10 @@ def open_popup(self, *args, **kwargs): def _fpl_reset_layout(self, *ev): """set the viewport rects for all subplots, *ev argument is not used, exists because of renderer resize event""" - self.layout.canvas_resized(self.get_pygfx_render_area()) + self.layout.canvas_resized( + canvas_rect=(0, 0, *self.canvas.get_logical_size()), + render_rect=self.get_pygfx_render_area() + ) def get_pygfx_render_area(self, *args) -> tuple[float, float, float, float]: """ diff --git a/fastplotlib/layouts/_frame.py b/fastplotlib/layouts/_frame.py index cd2a1cbc..9ea7c61f 100644 --- a/fastplotlib/layouts/_frame.py +++ b/fastplotlib/layouts/_frame.py @@ -116,6 +116,7 @@ def __init__( docks, toolbar_visible, canvas_rect, + render_rect, ): """ Manages the plane mesh, resize handle point, and subplot title. @@ -147,7 +148,10 @@ def __init__( toolbar visibility canvas_rect: tuple - figure canvas rect, the render area excluding any areas taken by imgui edge windows + figure canvas rec + + render_rect: tuple + the render area rect excluding any areas taken by imgui edge windows or other things """ @@ -157,9 +161,9 @@ def __init__( # create rect manager to handle all the backend rect calculations if rect is not None: - self._rect_manager = RectManager(*rect, canvas_rect) + self._rect_manager = RectManager(*rect, canvas_rect, render_rect) elif extent is not None: - self._rect_manager = RectManager.from_extent(extent, canvas_rect) + self._rect_manager = RectManager.from_extent(extent, canvas_rect, render_rect) else: raise ValueError("Must provide `rect` or `extent`") @@ -364,8 +368,8 @@ def resize_handle(self) -> pygfx.Points: """resize handler point""" return self._resize_handle - def canvas_resized(self, canvas_rect): + def canvas_resized(self, canvas_rect, render_rect): """called by layout is resized""" - self._rect_manager.canvas_resized(canvas_rect) + self._rect_manager.canvas_resized(canvas_rect, render_rect) self._reset() self.reset_viewport() diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py index 40145fe5..c75b142b 100644 --- a/fastplotlib/layouts/_imgui_figure.py +++ b/fastplotlib/layouts/_imgui_figure.py @@ -177,6 +177,12 @@ def add_gui(self, gui: EdgeWindow): self._fpl_reset_layout() + # reset the rects for all guis to account for any existing guis on the edges to ensure no overlapping windows + for gui in self.guis.values(): + if gui is None: + continue + gui._set_rect() + 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, diff --git a/fastplotlib/layouts/_rect.py b/fastplotlib/layouts/_rect.py index aa84ee8a..d864cbe3 100644 --- a/fastplotlib/layouts/_rect.py +++ b/fastplotlib/layouts/_rect.py @@ -6,13 +6,15 @@ class RectManager: Backend management of a rect. Allows converting between rects and extents, also works with fractional inputs. """ - def __init__(self, x: float, y: float, w: float, h: float, canvas_rect: tuple): + def __init__(self, x: float, y: float, w: float, h: float, canvas_rect: tuple, render_rect: tuple): # initialize rect state arrays # used to store internal state of the rect in both fractional screen space and absolute screen space # the purpose of storing the fractional rect is that it remains constant when the canvas resizes - self._rect_frac = np.zeros(4, dtype=np.float64) + self._rect_frac_canvas_area = np.zeros(4, dtype=np.float64) + self._rect_frac_render_area = np.zeros(4, dtype=np.float64) self._rect_screen_space = np.zeros(4, dtype=np.float64) self._canvas_rect = np.asarray(canvas_rect) + self._render_rect = np.asarray(render_rect) self._set((x, y, w, h)) @@ -36,13 +38,10 @@ def _set(self, rect): else: raise ValueError(f"Invalid rect: {rect}") - def _set_from_fract(self, rect): - """set rect from fractional representation""" - _, _, cw, ch = self._canvas_rect - mult = np.array([cw, ch, cw, ch]) + """set rect from fractional rect representation""" - # check that widths, heights are valid: + # check that widths, heights are valid if rect[0] + rect[2] > 1: raise ValueError( f"invalid fractional rect: {rect}\n x + width > 1: {rect[0]} + {rect[2]} > 1" @@ -52,26 +51,53 @@ def _set_from_fract(self, rect): f"invalid fractional rect: {rect}\n y + height > 1: {rect[1]} + {rect[3]} > 1" ) + cx, cy, cw, ch = self._canvas_rect + rx, ry, rw, rh = self._render_rect + + mult = np.array([cw, ch, cw, ch]) + + # frac rect w.r.t. the render area only + rect_render_area = rect.copy() + + # frac rect w.r.t. the entire canvas + rect_canvas_area = rect.copy() + + render_rect_scaler = np.array([rw, rh, rw, rh]) / mult + + # scale by render rect + rect_canvas_area *= np.concatenate([render_rect_scaler[2:], render_rect_scaler[2:]]) + # add (x0, y0) offsets + rect_canvas_area[:-2] += np.array([rx, ry]) / mult[:-2] + # assign values to the arrays, don't just change the reference - self._rect_frac[:] = rect - self._rect_screen_space[:] = self._rect_frac * mult + self._rect_frac_render_area[:] = rect_render_area + self._rect_frac_canvas_area[:] = rect_canvas_area + self._rect_screen_space[:] = self._rect_frac_canvas_area * mult def _set_from_screen_space(self, rect): - """set rect from screen space representation""" - _, _, cw, ch = self._canvas_rect - mult = np.array([cw, ch, cw, ch]) + """set rect from screen space rect representation""" + cx, cy, cw, ch = self._canvas_rect + rx, ry, rw, rh = self._render_rect + + # multiplier + mult = np.array([rw, rh, rw, rh]) + + # add (x0, y0) offset from render rect + rect[0] += rx + rect[1] += ry + # for screen coords allow (x, y) = 1 or 0, but w, h must be > 1 # check that widths, heights are valid - if rect[0] + rect[2] > cw: + if rect[0] + rect[2] > rw: raise ValueError( - f"invalid rect: {rect}\n x + width > canvas width: {rect[0]} + {rect[2]} > {cw}" + f"invalid rect: {rect}\n x + width > canvas render area width: {rect[0]} + {rect[2]} > {rw}" ) - if rect[1] + rect[3] > ch: + if rect[1] + rect[3] > rh: raise ValueError( - f"invalid rect: {rect}\n y + height > canvas height: {rect[1]} + {rect[3]} >{ch}" + f"invalid rect: {rect}\n y + height > canvas render area height: {rect[1]} + {rect[3]} >{rh}" ) - self._rect_frac[:] = rect / mult + self._rect_frac_canvas_area[:] = rect / mult self._rect_screen_space[:] = rect @property @@ -103,11 +129,12 @@ def rect(self) -> np.ndarray: def rect(self, rect: np.ndarray | tuple): self._set(rect) - def canvas_resized(self, canvas_rect: tuple): + def canvas_resized(self, canvas_rect: tuple, render_rect: tuple): # called by Frame when canvas is resized self._canvas_rect[:] = canvas_rect + self._render_rect[:] = render_rect # set new rect using existing rect_frac since this remains constant regardless of resize - self._set(self._rect_frac) + self._set(self._rect_frac_render_area) @property def x0(self) -> np.float64: @@ -130,10 +157,10 @@ def y1(self) -> np.float64: return self.y + self.h @classmethod - def from_extent(cls, extent, canvas_rect): + def from_extent(cls, extent, canvas_rect, render_rect): """create a RectManager from an extent""" - rect = cls.extent_to_rect(extent, canvas_rect) - return cls(*rect, canvas_rect) + rect = cls.extent_to_rect(extent, render_rect) + return cls(*rect, canvas_rect, render_rect) @property def extent(self) -> np.ndarray: @@ -143,14 +170,14 @@ def extent(self) -> np.ndarray: @extent.setter def extent(self, extent): - rect = RectManager.extent_to_rect(extent, canvas_rect=self._canvas_rect) + rect = RectManager.extent_to_rect(extent, self._canvas_rect) self._set(rect) @staticmethod - def extent_to_rect(extent, canvas_rect): + def extent_to_rect(extent, render_rect): """convert an extent to a rect""" - RectManager.validate_extent(extent, canvas_rect) + RectManager.validate_extent(extent, render_rect) x0, x1, y0, y1 = extent # width and height @@ -160,9 +187,9 @@ def extent_to_rect(extent, canvas_rect): return x0, y0, w, h @staticmethod - def validate_extent(extent: np.ndarray | tuple, canvas_rect: tuple): + def validate_extent(extent: np.ndarray | tuple, render_rect: tuple): extent = np.asarray(extent) - cx0, cy0, cw, ch = canvas_rect + rx0, ry0, rw, rh = render_rect # make sure extent is valid if (extent < 0).any(): @@ -174,7 +201,7 @@ def validate_extent(extent: np.ndarray | tuple, canvas_rect: tuple): raise ValueError( f"if passing a fractional extent, all values must be fractional, you have passed: {extent}" ) - extent *= np.asarray([cw, cw, ch, ch]) + extent *= np.asarray([rw, rw, rh, rh]) x0, x1, y0, y1 = extent @@ -191,15 +218,15 @@ def validate_extent(extent: np.ndarray | tuple, canvas_rect: tuple): raise ValueError(f"extent y-range must be non-negative: {extent}") # calc canvas extent - cx1 = cx0 + cw - cy1 = cy0 + ch - canvas_extent = np.asarray([cx0, cx1, cy0, cy1]) + cx1 = rx0 + rw + cy1 = ry0 + rh + canvas_extent = np.asarray([rx0, cx1, ry0, cy1]) - if x0 < cx0 or x1 < cx0 or x0 > cx1 or x1 > cx1: + if x0 < rx0 or x1 < rx0 or x0 > cx1 or x1 > cx1: raise ValueError( f"extent: {extent} x-range is beyond the bounds of the canvas: {canvas_extent}" ) - if y0 < cy0 or y1 < cy0 or y0 > cy1 or y1 > cy1: + if y0 < ry0 or y1 < ry0 or y0 > cy1 or y1 > cy1: raise ValueError( f"extent: {extent} y-range is beyond the bounds of the canvas: {canvas_extent}" ) @@ -234,6 +261,6 @@ def overlaps(self, extent: np.ndarray) -> bool: ) def __repr__(self): - s = f"{self._rect_frac}\n{self.rect}" + s = f"{self._rect_frac_canvas_area}\n{self.rect}" return s diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index 73f669fe..8c26f935 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -94,7 +94,8 @@ def __init__( title=name, docks=self.docks, toolbar_visible=toolbar_visible, - canvas_rect=parent.get_pygfx_render_area(), + canvas_rect=(0, 0, *canvas.get_logical_size()), + render_rect=parent.get_pygfx_render_area(), ) @property diff --git a/fastplotlib/ui/_base.py b/fastplotlib/ui/_base.py index 6c134d41..8edf91b4 100644 --- a/fastplotlib/ui/_base.py +++ b/fastplotlib/ui/_base.py @@ -179,22 +179,27 @@ def get_rect(self) -> tuple[int, int, int, int]: case "right": x_pos, y_pos = (width_canvas - self.size, 0) + width, height = (self.size, height_canvas) if self._figure.guis["top"]: # if there is a GUI in the top edge, make this one below y_pos += self._figure.guis["top"].size + # reduce height + height -= self._figure.guis["top"].size - width, height = (self.size, height_canvas) if self._figure.guis["bottom"] is not None: height -= self._figure.guis["bottom"].size case "left": x_pos, y_pos = (0, 0) + width, height = (self.size, height_canvas) + if self._figure.guis["top"]: # if there is a GUI in the top edge, make this one below y_pos += self._figure.guis["top"].size + # reduce height + height -= self._figure.guis["top"].size - width, height = (self.size, height_canvas) if self._figure.guis["bottom"] is not None: height -= self._figure.guis["bottom"].size @@ -203,8 +208,11 @@ 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 From 253afeb832300728589a28c1cbef4208b17adfa6 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 15 May 2025 23:42:56 -0400 Subject: [PATCH 3/5] revert, just disallow left and top edges for now --- examples/guis/sine_cosine_funcs.py | 6 +- fastplotlib/layouts/_engine.py | 20 ++---- fastplotlib/layouts/_figure.py | 11 +--- fastplotlib/layouts/_frame.py | 14 ++-- fastplotlib/layouts/_imgui_figure.py | 24 ++----- fastplotlib/layouts/_rect.py | 95 ++++++++++------------------ fastplotlib/layouts/_subplot.py | 3 +- fastplotlib/ui/_base.py | 8 +-- 8 files changed, 61 insertions(+), 120 deletions(-) diff --git a/examples/guis/sine_cosine_funcs.py b/examples/guis/sine_cosine_funcs.py index 70d948bf..c91a3b2e 100644 --- a/examples/guis/sine_cosine_funcs.py +++ b/examples/guis/sine_cosine_funcs.py @@ -9,7 +9,7 @@ # test_example = false # sphinx_gallery_pygfx_docs = 'screenshot' - +import glfw import numpy as np import fastplotlib as fpl from fastplotlib.ui import EdgeWindow @@ -169,8 +169,8 @@ def update(self): gui = GUIWindow( figure=figure, - size=80, - location="top", + size=100, + location="right", title="Freq. coeffs" ) diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index 3181d78b..877a7fba 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -31,8 +31,7 @@ def __init__( self, renderer: pygfx.WgpuRenderer, subplots: np.ndarray[Subplot], - canvas_rect: tuple[float, float, float, float], - render_rect: tuple[float, float, float, float], + canvas_rect: tuple[float, float], moveable: bool, resizeable: bool, ): @@ -42,7 +41,6 @@ def __init__( self._renderer = renderer self._subplots: np.ndarray[Subplot] = subplots.ravel() self._canvas_rect = canvas_rect - self._render_rect = render_rect self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array( [np.nan, np.nan] @@ -84,7 +82,7 @@ def _inside_render_rect(self, subplot: Subplot, pos: tuple[int, int]) -> bool: return False - def canvas_resized(self, canvas_rect: tuple, render_rect: tuple): + def canvas_resized(self, canvas_rect: tuple): """ called by figure when canvas is resized @@ -96,10 +94,8 @@ def canvas_resized(self, canvas_rect: tuple, render_rect: tuple): """ self._canvas_rect = canvas_rect - self._render_rect = render_rect - for subplot in self._subplots: - subplot.frame.canvas_resized(canvas_rect, render_rect) + subplot.frame.canvas_resized(canvas_rect) def _highlight_resize_handler(self, subplot: Subplot, ev): if self._active_action == "resize": @@ -136,7 +132,6 @@ def __init__( renderer, subplots: np.ndarray[Subplot], canvas_rect: tuple, - render_rect: tuple, moveable=True, resizeable=True, ): @@ -149,7 +144,7 @@ def __init__( """ - super().__init__(renderer, subplots, canvas_rect, render_rect, moveable, resizeable) + super().__init__(renderer, subplots, canvas_rect, moveable, resizeable) self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array( [np.nan, np.nan] @@ -296,7 +291,7 @@ def set_rect(self, subplot: Subplot, rect: tuple | list | np.ndarray): """ - new_rect = RectManager(*rect, self._canvas_rect, self.render_rect) + new_rect = RectManager(*rect, self._canvas_rect) extent = new_rect.extent # check for overlaps for s in self._subplots: @@ -323,7 +318,7 @@ def set_extent(self, subplot: Subplot, extent: tuple | list | np.ndarray): """ - new_rect = RectManager.from_extent(extent, self._canvas_rect, self.render_rect) + new_rect = RectManager.from_extent(extent, self._canvas_rect) extent = new_rect.extent # check for overlaps for s in self._subplots: @@ -342,7 +337,6 @@ def __init__( renderer, subplots: np.ndarray[Subplot], canvas_rect: tuple[float, float, float, float], - render_rect: tuple[float, float, float, float], shape: tuple[int, int], ): """ @@ -352,7 +346,7 @@ def __init__( """ super().__init__( - renderer, subplots, canvas_rect, render_rect, moveable=False, resizeable=False + renderer, subplots, canvas_rect, moveable=False, resizeable=False ) # {Subplot: (row_ix, col_ix)}, dict mapping subplots to their row and col index in the grid layout diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index a41dce2b..a1bae965 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -398,8 +398,7 @@ def __init__( self._layout = GridLayout( self.renderer, subplots=self._subplots, - canvas_rect=(0, 0, *canvas.get_logical_size()), - render_rect=self.get_pygfx_render_area(), + canvas_rect=self.get_pygfx_render_area(), shape=shape, ) @@ -407,8 +406,7 @@ def __init__( self._layout = WindowLayout( self.renderer, subplots=self._subplots, - canvas_rect=(0, 0, *canvas.get_logical_size()), - render_rect=self.get_pygfx_render_area() + canvas_rect=self.get_pygfx_render_area(), ) self._underlay_camera = UnderlayCamera() @@ -756,10 +754,7 @@ def open_popup(self, *args, **kwargs): def _fpl_reset_layout(self, *ev): """set the viewport rects for all subplots, *ev argument is not used, exists because of renderer resize event""" - self.layout.canvas_resized( - canvas_rect=(0, 0, *self.canvas.get_logical_size()), - render_rect=self.get_pygfx_render_area() - ) + self.layout.canvas_resized(self.get_pygfx_render_area()) def get_pygfx_render_area(self, *args) -> tuple[float, float, float, float]: """ diff --git a/fastplotlib/layouts/_frame.py b/fastplotlib/layouts/_frame.py index 9ea7c61f..cd2a1cbc 100644 --- a/fastplotlib/layouts/_frame.py +++ b/fastplotlib/layouts/_frame.py @@ -116,7 +116,6 @@ def __init__( docks, toolbar_visible, canvas_rect, - render_rect, ): """ Manages the plane mesh, resize handle point, and subplot title. @@ -148,10 +147,7 @@ def __init__( toolbar visibility canvas_rect: tuple - figure canvas rec - - render_rect: tuple - the render area rect excluding any areas taken by imgui edge windows or other things + figure canvas rect, the render area excluding any areas taken by imgui edge windows """ @@ -161,9 +157,9 @@ def __init__( # create rect manager to handle all the backend rect calculations if rect is not None: - self._rect_manager = RectManager(*rect, canvas_rect, render_rect) + self._rect_manager = RectManager(*rect, canvas_rect) elif extent is not None: - self._rect_manager = RectManager.from_extent(extent, canvas_rect, render_rect) + self._rect_manager = RectManager.from_extent(extent, canvas_rect) else: raise ValueError("Must provide `rect` or `extent`") @@ -368,8 +364,8 @@ def resize_handle(self) -> pygfx.Points: """resize handler point""" return self._resize_handle - def canvas_resized(self, canvas_rect, render_rect): + def canvas_resized(self, canvas_rect): """called by layout is resized""" - self._rect_manager.canvas_resized(canvas_rect, render_rect) + self._rect_manager.canvas_resized(canvas_rect) self._reset() self.reset_viewport() diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py index c75b142b..b0267dc7 100644 --- a/fastplotlib/layouts/_imgui_figure.py +++ b/fastplotlib/layouts/_imgui_figure.py @@ -150,7 +150,7 @@ def _draw_imgui(self) -> imgui.ImDrawData: def add_gui(self, gui: EdgeWindow): """ - Add a GUI to the Figure. GUIs can be added to the top, bottom, left or right edge. + Add a GUI to the Figure. GUIs can be added to the left or bottom edge. Parameters ---------- @@ -177,12 +177,6 @@ def add_gui(self, gui: EdgeWindow): self._fpl_reset_layout() - # reset the rects for all guis to account for any existing guis on the edges to ensure no overlapping windows - for gui in self.guis.values(): - if gui is None: - continue - gui._set_rect() - 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, @@ -197,25 +191,15 @@ def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]: width, height = self.canvas.get_logical_size() - for edge in ["left", "right"]: + for edge in ["right"]: if self.guis[edge]: width -= self._guis[edge].size - for edge in ["top", "bottom"]: + for edge in ["bottom"]: if self.guis[edge]: height -= self._guis[edge].size - if self.guis["left"]: - xpos = self.guis["left"].size - else: - xpos = 0 - - if self.guis["top"]: - ypos = self.guis["top"].size - else: - ypos = 0 - - return xpos, ypos, max(1, width), max(1, height) + return 0, 0, max(1, width), max(1, height) def register_popup(self, popup: Popup.__class__): """ diff --git a/fastplotlib/layouts/_rect.py b/fastplotlib/layouts/_rect.py index d864cbe3..aa84ee8a 100644 --- a/fastplotlib/layouts/_rect.py +++ b/fastplotlib/layouts/_rect.py @@ -6,15 +6,13 @@ class RectManager: Backend management of a rect. Allows converting between rects and extents, also works with fractional inputs. """ - def __init__(self, x: float, y: float, w: float, h: float, canvas_rect: tuple, render_rect: tuple): + def __init__(self, x: float, y: float, w: float, h: float, canvas_rect: tuple): # initialize rect state arrays # used to store internal state of the rect in both fractional screen space and absolute screen space # the purpose of storing the fractional rect is that it remains constant when the canvas resizes - self._rect_frac_canvas_area = np.zeros(4, dtype=np.float64) - self._rect_frac_render_area = np.zeros(4, dtype=np.float64) + self._rect_frac = np.zeros(4, dtype=np.float64) self._rect_screen_space = np.zeros(4, dtype=np.float64) self._canvas_rect = np.asarray(canvas_rect) - self._render_rect = np.asarray(render_rect) self._set((x, y, w, h)) @@ -38,10 +36,13 @@ def _set(self, rect): else: raise ValueError(f"Invalid rect: {rect}") + def _set_from_fract(self, rect): - """set rect from fractional rect representation""" + """set rect from fractional representation""" + _, _, cw, ch = self._canvas_rect + mult = np.array([cw, ch, cw, ch]) - # check that widths, heights are valid + # check that widths, heights are valid: if rect[0] + rect[2] > 1: raise ValueError( f"invalid fractional rect: {rect}\n x + width > 1: {rect[0]} + {rect[2]} > 1" @@ -51,53 +52,26 @@ def _set_from_fract(self, rect): f"invalid fractional rect: {rect}\n y + height > 1: {rect[1]} + {rect[3]} > 1" ) - cx, cy, cw, ch = self._canvas_rect - rx, ry, rw, rh = self._render_rect - - mult = np.array([cw, ch, cw, ch]) - - # frac rect w.r.t. the render area only - rect_render_area = rect.copy() - - # frac rect w.r.t. the entire canvas - rect_canvas_area = rect.copy() - - render_rect_scaler = np.array([rw, rh, rw, rh]) / mult - - # scale by render rect - rect_canvas_area *= np.concatenate([render_rect_scaler[2:], render_rect_scaler[2:]]) - # add (x0, y0) offsets - rect_canvas_area[:-2] += np.array([rx, ry]) / mult[:-2] - # assign values to the arrays, don't just change the reference - self._rect_frac_render_area[:] = rect_render_area - self._rect_frac_canvas_area[:] = rect_canvas_area + self._rect_frac[:] = rect + self._rect_screen_space[:] = self._rect_frac * mult - self._rect_screen_space[:] = self._rect_frac_canvas_area * mult def _set_from_screen_space(self, rect): - """set rect from screen space rect representation""" - cx, cy, cw, ch = self._canvas_rect - rx, ry, rw, rh = self._render_rect - - # multiplier - mult = np.array([rw, rh, rw, rh]) - - # add (x0, y0) offset from render rect - rect[0] += rx - rect[1] += ry - + """set rect from screen space representation""" + _, _, cw, ch = self._canvas_rect + mult = np.array([cw, ch, cw, ch]) # for screen coords allow (x, y) = 1 or 0, but w, h must be > 1 # check that widths, heights are valid - if rect[0] + rect[2] > rw: + if rect[0] + rect[2] > cw: raise ValueError( - f"invalid rect: {rect}\n x + width > canvas render area width: {rect[0]} + {rect[2]} > {rw}" + f"invalid rect: {rect}\n x + width > canvas width: {rect[0]} + {rect[2]} > {cw}" ) - if rect[1] + rect[3] > rh: + if rect[1] + rect[3] > ch: raise ValueError( - f"invalid rect: {rect}\n y + height > canvas render area height: {rect[1]} + {rect[3]} >{rh}" + f"invalid rect: {rect}\n y + height > canvas height: {rect[1]} + {rect[3]} >{ch}" ) - self._rect_frac_canvas_area[:] = rect / mult + self._rect_frac[:] = rect / mult self._rect_screen_space[:] = rect @property @@ -129,12 +103,11 @@ def rect(self) -> np.ndarray: def rect(self, rect: np.ndarray | tuple): self._set(rect) - def canvas_resized(self, canvas_rect: tuple, render_rect: tuple): + def canvas_resized(self, canvas_rect: tuple): # called by Frame when canvas is resized self._canvas_rect[:] = canvas_rect - self._render_rect[:] = render_rect # set new rect using existing rect_frac since this remains constant regardless of resize - self._set(self._rect_frac_render_area) + self._set(self._rect_frac) @property def x0(self) -> np.float64: @@ -157,10 +130,10 @@ def y1(self) -> np.float64: return self.y + self.h @classmethod - def from_extent(cls, extent, canvas_rect, render_rect): + def from_extent(cls, extent, canvas_rect): """create a RectManager from an extent""" - rect = cls.extent_to_rect(extent, render_rect) - return cls(*rect, canvas_rect, render_rect) + rect = cls.extent_to_rect(extent, canvas_rect) + return cls(*rect, canvas_rect) @property def extent(self) -> np.ndarray: @@ -170,14 +143,14 @@ def extent(self) -> np.ndarray: @extent.setter def extent(self, extent): - rect = RectManager.extent_to_rect(extent, self._canvas_rect) + rect = RectManager.extent_to_rect(extent, canvas_rect=self._canvas_rect) self._set(rect) @staticmethod - def extent_to_rect(extent, render_rect): + def extent_to_rect(extent, canvas_rect): """convert an extent to a rect""" - RectManager.validate_extent(extent, render_rect) + RectManager.validate_extent(extent, canvas_rect) x0, x1, y0, y1 = extent # width and height @@ -187,9 +160,9 @@ def extent_to_rect(extent, render_rect): return x0, y0, w, h @staticmethod - def validate_extent(extent: np.ndarray | tuple, render_rect: tuple): + def validate_extent(extent: np.ndarray | tuple, canvas_rect: tuple): extent = np.asarray(extent) - rx0, ry0, rw, rh = render_rect + cx0, cy0, cw, ch = canvas_rect # make sure extent is valid if (extent < 0).any(): @@ -201,7 +174,7 @@ def validate_extent(extent: np.ndarray | tuple, render_rect: tuple): raise ValueError( f"if passing a fractional extent, all values must be fractional, you have passed: {extent}" ) - extent *= np.asarray([rw, rw, rh, rh]) + extent *= np.asarray([cw, cw, ch, ch]) x0, x1, y0, y1 = extent @@ -218,15 +191,15 @@ def validate_extent(extent: np.ndarray | tuple, render_rect: tuple): raise ValueError(f"extent y-range must be non-negative: {extent}") # calc canvas extent - cx1 = rx0 + rw - cy1 = ry0 + rh - canvas_extent = np.asarray([rx0, cx1, ry0, cy1]) + cx1 = cx0 + cw + cy1 = cy0 + ch + canvas_extent = np.asarray([cx0, cx1, cy0, cy1]) - if x0 < rx0 or x1 < rx0 or x0 > cx1 or x1 > cx1: + if x0 < cx0 or x1 < cx0 or x0 > cx1 or x1 > cx1: raise ValueError( f"extent: {extent} x-range is beyond the bounds of the canvas: {canvas_extent}" ) - if y0 < ry0 or y1 < ry0 or y0 > cy1 or y1 > cy1: + if y0 < cy0 or y1 < cy0 or y0 > cy1 or y1 > cy1: raise ValueError( f"extent: {extent} y-range is beyond the bounds of the canvas: {canvas_extent}" ) @@ -261,6 +234,6 @@ def overlaps(self, extent: np.ndarray) -> bool: ) def __repr__(self): - s = f"{self._rect_frac_canvas_area}\n{self.rect}" + s = f"{self._rect_frac}\n{self.rect}" return s diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index 8c26f935..73f669fe 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -94,8 +94,7 @@ def __init__( title=name, docks=self.docks, toolbar_visible=toolbar_visible, - canvas_rect=(0, 0, *canvas.get_logical_size()), - render_rect=parent.get_pygfx_render_area(), + canvas_rect=parent.get_pygfx_render_area(), ) @property diff --git a/fastplotlib/ui/_base.py b/fastplotlib/ui/_base.py index 8edf91b4..86c26950 100644 --- a/fastplotlib/ui/_base.py +++ b/fastplotlib/ui/_base.py @@ -6,7 +6,7 @@ from ..layouts._figure import Figure -GUI_EDGES = ["top", "right", "bottom", "left"] +GUI_EDGES = ["right", "bottom"] class BaseGUI: @@ -40,7 +40,7 @@ def __init__( self, figure: Figure, size: int, - location: Literal["top", "bottom", "left", "right"], + location: Literal["bottom", "right"], title: str, window_flags: int = imgui.WindowFlags_.no_collapse | imgui.WindowFlags_.no_resize, @@ -48,7 +48,7 @@ def __init__( **kwargs, ): """ - A base class for imgui windows displayed at one of the four edges of a Figure + A base class for imgui windows displayed at the bottom or top edge of a Figure Parameters ---------- @@ -58,7 +58,7 @@ def __init__( size: int width or height of the window, depending on its location - location: str, "top" | "bottom" | "left" | "right" + location: str, "bottom" | "right" location of the window title: str From 429decdda00b2715ab4554ff0296444a43f99e51 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 15 May 2025 23:47:44 -0400 Subject: [PATCH 4/5] cleanup docs guide --- docs/source/user_guide/guide.rst | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/docs/source/user_guide/guide.rst b/docs/source/user_guide/guide.rst index 1bdb3377..073fa806 100644 --- a/docs/source/user_guide/guide.rst +++ b/docs/source/user_guide/guide.rst @@ -549,10 +549,15 @@ between imgui and ipywidgets, Qt, and wx is the imgui UI can be rendered directl i.e. it will work in jupyter, Qt, glfw and wx windows! The programming model is different from Qt and ipywidgets, there are no callbacks, but it is easy to learn if you see a few examples. +.. image:: ../_static/guide_imgui.png + We specifically use `imgui-bundle `_ for the python bindings in fastplotlib. There is large community and many resources out there on building UIs using imgui. -For examples on integrating imgui with a fastplotlib Figure please see the examples gallery. +To install ``fastplotlib`` with ``imgui`` use the ``imgui`` extras option, i.e. ``pip install fastplotlib[imgui]``, or ``pip install imgui_bundle`` if you've already installed fastplotlib. + +Fastplotlib comes built-in with imgui UIs for subplot toolbars and a standard right-click menu with a number of options. +You can also make custom GUIs and embed them within the canvas, see the examples gallery for detailed examples. **Some tips:** @@ -662,21 +667,6 @@ There are several spaces to consider when using ``fastplotlib``: For more information on the various spaces used by rendering engines please see this `article `_ -Imgui ------ - -Fastplotlib uses `imgui_bundle `_ to provide within-canvas UI elemenents if you -installed ``fastplotlib`` using the ``imgui`` toggle, i.e. ``fastplotlib[imgui]``, or installed ``imgui_bundle`` afterwards. - -Fastplotlib comes built-in with imgui UIs for subplot toolbars and a standard right-click menu with a number of options. -You can also make custom GUIs and embed them within the canvas, see the examples gallery for detailed examples. - -.. note:: - Imgui is optional, you can use other GUI frameworks such at Qt or ipywidgets with fastplotlib. You can also of course - use imgui and Qt or ipywidgets. - -.. image:: ../_static/guide_imgui.png - Using ``fastplotlib`` in an interactive shell --------------------------------------------- From de02acbee5c87a13dab7762bac04872604532bd0 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 16 May 2025 00:14:38 -0400 Subject: [PATCH 5/5] remove file, fix --- examples/guis/many_ui.py | 50 ---------------------------------------- fastplotlib/ui/_base.py | 23 ------------------ 2 files changed, 73 deletions(-) delete mode 100644 examples/guis/many_ui.py diff --git a/examples/guis/many_ui.py b/examples/guis/many_ui.py deleted file mode 100644 index 07bc8bed..00000000 --- a/examples/guis/many_ui.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Many UIs surrounding the render area -==================================== - -Mostly a test example with imgui windows on every edge of the Figure. -""" - -# test_example = true -# sphinx_gallery_pygfx_docs = 'screenshot' - -import numpy as np -import fastplotlib as fpl - -# subclass from EdgeWindow to make a custom ImGUI Window to place inside the figure! -from fastplotlib.ui import EdgeWindow -from imgui_bundle import imgui - -# make some initial data -np.random.seed(0) - -xs = np.linspace(0, np.pi * 10, 100) -ys = np.sin(xs) + np.random.normal(scale=0.5, size=100) -data = np.column_stack([xs, ys]) - - -# make a figure -figure = fpl.Figure(shape=(3, 2), size=(700, 560)) - -for subplot in figure: - subplot.add_line(data, thickness=1, colors="r", name="sine-wave", uniform_color=True) - - -class Window1(EdgeWindow): - def __init__(self, figure, size, location, title): - super().__init__(figure=figure, size=size, location=location, title=title) - self._title = title - - def update(self): - if imgui.button("reset data"): - for subplot in self._figure: - subplot["sine-wave"].data[:, 1] = np.sin(xs) + np.random.normal(scale=0.5, size=100) - - -for i, location in enumerate(["left", "right", "top", "bottom"]): - gui = Window1(figure, 100, location, f"ui-{i}") - figure.add_gui(gui) - -figure.show() - -fpl.loop.run() diff --git a/fastplotlib/ui/_base.py b/fastplotlib/ui/_base.py index 86c26950..e31dd8d4 100644 --- a/fastplotlib/ui/_base.py +++ b/fastplotlib/ui/_base.py @@ -168,10 +168,6 @@ def get_rect(self) -> tuple[int, int, int, int]: width_canvas, height_canvas = self._figure.canvas.get_logical_size() match self._location: - case "top": - x_pos, y_pos = (0, 0) - width, height = (width_canvas, self.size) - case "bottom": x_pos = 0 y_pos = height_canvas - self.size @@ -181,25 +177,6 @@ def get_rect(self) -> tuple[int, int, int, int]: x_pos, y_pos = (width_canvas - self.size, 0) width, height = (self.size, height_canvas) - if self._figure.guis["top"]: - # if there is a GUI in the top edge, make this one below - y_pos += self._figure.guis["top"].size - # reduce height - height -= self._figure.guis["top"].size - - if self._figure.guis["bottom"] is not None: - height -= self._figure.guis["bottom"].size - - case "left": - x_pos, y_pos = (0, 0) - width, height = (self.size, height_canvas) - - if self._figure.guis["top"]: - # if there is a GUI in the top edge, make this one below - y_pos += self._figure.guis["top"].size - # reduce height - height -= self._figure.guis["top"].size - if self._figure.guis["bottom"] is not None: height -= self._figure.guis["bottom"].size pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy