Skip to content

imgui prototype #552

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

Closed
wants to merge 9 commits into from
Closed
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
139 changes: 91 additions & 48 deletions fastplotlib/layouts/_figure.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import os
from itertools import product, chain
from multiprocessing import Queue
from pathlib import Path
Expand All @@ -9,42 +8,47 @@
from inspect import getfullargspec
from warnings import warn

import imgui_bundle
from imgui_bundle import imgui, icons_fontawesome_6 as fa

import pygfx

from wgpu.gui import WgpuCanvasBase
from wgpu.utils.imgui import ImguiRenderer

from ._video_writer import VideoWriterAV
from ._utils import make_canvas_and_renderer, create_controller, create_camera
from ._utils import controller_types as valid_controller_types
from ._subplot import Subplot
from .ui._right_click_menu import RightClickMenu
from .. import ImageGraphic


class Figure:
def __init__(
self,
shape: tuple[int, int] = (1, 1),
cameras: (
Literal["2d", "3d"]
| Iterable[Iterable[Literal["2d", "3d"]]]
| pygfx.PerspectiveCamera
| Iterable[Iterable[pygfx.PerspectiveCamera]]
) = "2d",
controller_types: (
Iterable[Iterable[Literal["panzoom", "fly", "trackball", "orbit"]]]
| Iterable[Literal["panzoom", "fly", "trackball", "orbit"]]
) = None,
controller_ids: (
Literal["sync"]
| Iterable[int]
| Iterable[Iterable[int]]
| Iterable[Iterable[str]]
) = None,
controllers: pygfx.Controller | Iterable[Iterable[pygfx.Controller]] = None,
canvas: str | WgpuCanvasBase | pygfx.Texture = None,
renderer: pygfx.WgpuRenderer = None,
size: tuple[int, int] = (500, 300),
names: list | np.ndarray = None,
self,
shape: tuple[int, int] = (1, 1),
cameras: (
Literal["2d", "3d"]
| Iterable[Iterable[Literal["2d", "3d"]]]
| pygfx.PerspectiveCamera
| Iterable[Iterable[pygfx.PerspectiveCamera]]
) = "2d",
controller_types: (
Iterable[Iterable[Literal["panzoom", "fly", "trackball", "orbit"]]]
| Iterable[Literal["panzoom", "fly", "trackball", "orbit"]]
) = None,
controller_ids: (
Literal["sync"]
| Iterable[int]
| Iterable[Iterable[int]]
| Iterable[Iterable[str]]
) = None,
controllers: pygfx.Controller | Iterable[Iterable[pygfx.Controller]] = None,
canvas: str | WgpuCanvasBase | pygfx.Texture = None,
renderer: pygfx.WgpuRenderer = None,
size: tuple[int, int] = (500, 300),
names: list | np.ndarray = None,
):
"""
A grid of subplots.
Expand Down Expand Up @@ -111,6 +115,25 @@ def __init__(

canvas, renderer = make_canvas_and_renderer(canvas, renderer)

self.imgui_renderer = ImguiRenderer(renderer.device, canvas)

path = str(Path(imgui_bundle.__file__).parent.joinpath("assets", "fonts", "Font_Awesome_6_Free-Solid-900.otf"))

io = imgui.get_io()

self._imgui_icons = io.fonts.add_font_from_file_ttf(
path,
24,
glyph_ranges_as_int_list=[fa.ICON_MIN_FA, fa.ICON_MAX_FA]
)

io.fonts.build()
self.imgui_renderer.backend.create_fonts_texture()

# number of pixels in the width, height we reserve for imgui at the right and bottom edges of the canvas
# used for imagewidget sliders or other imgui stuff
self.imgui_reserved_canvas = [0, 0]

if isinstance(cameras, str):
# create the array representing the views for each subplot in the grid
cameras = np.array([cameras] * len(self)).reshape(self.shape)
Expand Down Expand Up @@ -211,7 +234,7 @@ def __init__(
for i, sublist in enumerate(controller_ids):
for name in sublist:
ids_init[subplot_names == name] = -(
i + 1
i + 1
) # use negative numbers because why not

controller_ids = ids_init
Expand Down Expand Up @@ -331,6 +354,11 @@ def __init__(
else:
self.recorder = None

self._imgui_updaters: list[callable] = list()

self._right_click_menu = RightClickMenu(self)
self.imgui_renderer.set_gui(self.update_imgui)

@property
def toolbar(self):
"""ipywidget or QToolbar instance"""
Expand Down Expand Up @@ -390,6 +418,9 @@ def __getitem__(self, index: tuple[int, int] | str) -> Subplot:
else:
return self._subplots[index[0], index[1]]

def add_imgui_ui(self, func: callable):
self._imgui_updaters.append(func)

def render(self):
# call the animation functions before render
self._call_animate_functions(self._animate_funcs_pre)
Expand All @@ -398,24 +429,47 @@ def render(self):
subplot.render()

self.renderer.flush()

self.imgui_renderer.render()

self.canvas.request_draw()

# call post-render animate functions
self._call_animate_functions(self._animate_funcs_post)

def update_imgui(self):
imgui.new_frame()

# update the toolbars
for subplot in self:
subplot.toolbar.update()

# call any other imgui updaters
for func in self._imgui_updaters:
func()

# make right click menu
self._right_click_menu.update()

# render new UI frame
imgui.end_frame()

imgui.render()

return imgui.get_draw_data()

def start_render(self):
"""start render cycle"""
self.canvas.request_draw(self.render)
self.canvas.set_logical_size(*self._starting_size)

def show(
self,
autoscale: bool = True,
maintain_aspect: bool = None,
toolbar: bool = True,
sidecar: bool = False,
sidecar_kwargs: dict = None,
add_widgets: list = None,
self,
autoscale: bool = True,
maintain_aspect: bool = None,
toolbar: bool = True,
sidecar: bool = False,
sidecar_kwargs: dict = None,
):
"""
Begins the rendering event loop and shows the plot in the desired output context (jupyter, qt or glfw).
Expand All @@ -438,9 +492,6 @@ def show(
kwargs for sidecar instance to display plot
i.e. title, layout

add_widgets: list of widgets
a list of ipywidgets or QWidget that are vertically stacked below the plot

Returns
-------
OutputContext
Expand All @@ -455,12 +506,6 @@ def show(

self.start_render()

if sidecar_kwargs is None:
sidecar_kwargs = dict()

if add_widgets is None:
add_widgets = list()

# flip y-axis if ImageGraphics are present
for subplot in self:
for g in subplot.graphics:
Expand All @@ -484,17 +529,15 @@ def show(

self._output = JupyterOutputContext(
frame=self,
make_toolbar=toolbar,
use_sidecar=sidecar,
sidecar_kwargs=sidecar_kwargs,
add_widgets=add_widgets,
)

elif self.canvas.__class__.__name__ == "QWgpuCanvas":
from .output.qt_output import QOutputContext # noqa - inline import

self._output = QOutputContext(
frame=self, make_toolbar=toolbar, add_widgets=add_widgets
frame=self,
)

else: # assume GLFW, the output context is just the canvas
Expand All @@ -521,10 +564,10 @@ def _call_animate_functions(self, funcs: list[callable]):
fn()

def add_animations(
self,
*funcs: callable,
pre_render: bool = True,
post_render: bool = False,
self,
*funcs: callable,
pre_render: bool = True,
post_render: bool = False,
):
"""
Add function(s) that are called on every render cycle.
Expand Down
34 changes: 31 additions & 3 deletions fastplotlib/layouts/_subplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from ._utils import make_canvas_and_renderer, create_camera, create_controller
from ._plot_area import PlotArea
from ._graphic_methods_mixin import GraphicMethodsMixin
from .ui._toolbar import SubplotToolbar


class Subplot(PlotArea, GraphicMethodsMixin):
Expand Down Expand Up @@ -116,6 +117,13 @@ def __init__(
if self.name is not None:
self.set_title(self.name)

self.toolbar = SubplotToolbar(self, self.parent._imgui_icons)

# self.add_animations(self.render_imgui)
#
# def render_imgui(self):
# self.parent.imgui_renderer.render(self.toolbar.update())

@property
def name(self) -> str:
return self._name
Expand Down Expand Up @@ -171,14 +179,25 @@ def get_rect(self):
row_ix, col_ix = self.position
width_canvas, height_canvas = self.renderer.logical_size

width_canvas -= self.parent.imgui_reserved_canvas[0]
height_canvas -= self.parent.imgui_reserved_canvas[1]

# spacings for imgui toolbar
height_canvas -= 50
if row_ix > 0:
top_spacing = 50
else:
top_spacing = 0

x_pos = (
(width_canvas / self.ncols) + ((col_ix - 1) * (width_canvas / self.ncols))
) + self.spacing
y_pos = (
(height_canvas / self.nrows) + ((row_ix - 1) * (height_canvas / self.nrows))
) + self.spacing
) + self.spacing + top_spacing

width_subplot = (width_canvas / self.ncols) - self.spacing
height_subplot = (height_canvas / self.nrows) - self.spacing
height_subplot = (height_canvas / self.nrows) - self.spacing - top_spacing

rect = np.array([x_pos, y_pos, width_subplot, height_subplot])

Expand Down Expand Up @@ -247,6 +266,15 @@ def get_rect(self, *args):
row_ix_parent, col_ix_parent = self.parent.position
width_canvas, height_canvas = self.parent.renderer.logical_size

width_canvas -= self.parent.parent.imgui_reserved_canvas[0]
height_canvas -= self.parent.parent.imgui_reserved_canvas[1]

height_canvas -= 50
if row_ix_parent > 0:
top_spacing = 50
else:
top_spacing = 0

spacing = 2 # spacing in pixels

if self.position == "right":
Expand Down Expand Up @@ -283,7 +311,7 @@ def get_rect(self, *args):
y_pos = (
(height_canvas / self.parent.nrows)
+ ((row_ix_parent - 1) * (height_canvas / self.parent.nrows))
) + spacing
) + spacing + top_spacing
width_viewport = (width_canvas / self.parent.ncols) - spacing
height_viewport = self.size

Expand Down
24 changes: 23 additions & 1 deletion fastplotlib/layouts/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,33 @@

import pygfx
from pygfx import WgpuRenderer, Texture, Renderer
from pygfx.renderers.wgpu.engine.renderer import EVENT_TYPE_MAP, PointerEvent
from wgpu.gui import WgpuCanvasBase

from ..utils import gui


# temporary until https://github.com/pygfx/pygfx/issues/495
class WgpuRendererWithEventFilters(WgpuRenderer):
def __init__(self, target, *args, **kwargs):
super().__init__(target, *args, **kwargs)
self._event_filters = {}

def convert_event(self, event: dict):
event_type = event["event_type"]

if EVENT_TYPE_MAP[event_type] is PointerEvent:
for filt in self.event_filters.values():
if filt[0, 0] < event["x"] < filt[1, 0] and filt[0, 1] < event["y"] < filt[1, 1]:
return

super().convert_event(event)

@property
def event_filters(self) -> dict:
return self._event_filters


def make_canvas_and_renderer(
canvas: str | WgpuCanvasBase | Texture | None, renderer: Renderer | None
):
Expand All @@ -27,7 +49,7 @@ def make_canvas_and_renderer(
)

if renderer is None:
renderer = WgpuRenderer(canvas)
renderer = WgpuRendererWithEventFilters(canvas)
elif not isinstance(renderer, Renderer):
raise TypeError(
f"renderer option must be a pygfx.Renderer instance such as pygfx.WgpuRenderer"
Expand Down
Loading