diff --git a/pyproject.toml b/pyproject.toml index d738fc36..c586070f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ wxpython = ["pyconify>=0.2.1", "wxpython >=4.2.2"] # Supported Canavs backends vispy = ["vispy>=0.14.3", "pyopengl >=3.1"] -pygfx = ["pygfx==0.9.0", "rendercanvas ==2.0.1", "wgpu==0.19.3"] +pygfx = ["pygfx~=0.16", "rendercanvas~=2.6", "wgpu~=0.31"] # ready to go bundles with pygfx qt = ["ndv[pygfx,pyqt]", "imageio[tifffile] >=2.20"] diff --git a/src/ndv/views/_app.py b/src/ndv/views/_app.py index 934b5e7d..90975ee1 100644 --- a/src/ndv/views/_app.py +++ b/src/ndv/views/_app.py @@ -346,6 +346,7 @@ def process_events() -> None: def run_app() -> None: """Start the active GUI application event loop.""" + print("running") ndv_app().run() diff --git a/src/ndv/views/_pygfx/_array_canvas.py b/src/ndv/views/_pygfx/_array_canvas.py index 498aefc7..a374e77d 100755 --- a/src/ndv/views/_pygfx/_array_canvas.py +++ b/src/ndv/views/_pygfx/_array_canvas.py @@ -387,7 +387,6 @@ def __init__(self, viewer_model: ArrayViewerModel) -> None: self._disconnect_mouse_events = filter_mouse_events(self._canvas, self) self._renderer = pygfx.renderers.WgpuRenderer(self._canvas) - self._renderer.blend_mode = "additive" self._scene = pygfx.Scene() self._camera: pygfx.Camera | None = None @@ -451,7 +450,7 @@ def add_image(self, data: np.ndarray | None = None) -> PyGFXImageHandle: image = pygfx.Image( pygfx.Geometry(grid=tex), # depth_test=False for additive-like blending - pygfx.ImageBasicMaterial(depth_test=False), + pygfx.ImageBasicMaterial(depth_test=False, alpha_mode="add"), ) self._scene.add(image) @@ -476,7 +475,9 @@ def add_volume(self, data: np.ndarray | None = None) -> PyGFXImageHandle: vol = pygfx.Volume( pygfx.Geometry(grid=tex), # depth_test=False for additive-like blending - pygfx.VolumeRayMaterial(interpolation="nearest", depth_test=False), + pygfx.VolumeRayMaterial( + interpolation="nearest", depth_test=False, alpha_mode="add" + ), ) self._scene.add(vol) diff --git a/src/ndv/views/_pygfx/_histogram.py b/src/ndv/views/_pygfx/_histogram.py index a71026d1..e18386aa 100644 --- a/src/ndv/views/_pygfx/_histogram.py +++ b/src/ndv/views/_pygfx/_histogram.py @@ -126,8 +126,6 @@ def __init__(self, *, vertical: bool = False) -> None: self._renderer = pygfx.renderers.WgpuRenderer(self._canvas) - self._renderer.blend_mode = "ordered1" - # Note that we split the view up into multiple scenes, each with their # own camera and renderer. # diff --git a/src/ndv/views/_pygfx/_util.py b/src/ndv/views/_pygfx/_util.py index 9dabde2e..01f78cbd 100644 --- a/src/ndv/views/_pygfx/_util.py +++ b/src/ndv/views/_pygfx/_util.py @@ -23,11 +23,19 @@ def sizeHint(self) -> QSize: return rendercanvas.jupyter.JupyterRenderCanvas # type: ignore[no-any-return] if frontend == GuiFrontend.WX: - # ...still not working - # import rendercanvas.wx - # return rendercanvas.wx.WxRenderWidget - from wgpu.gui.wx import WxWgpuCanvas + import rendercanvas.wx + import wx - return WxWgpuCanvas # type: ignore[no-any-return] + class WxRenderWidget(rendercanvas.wx.WxRenderWidget): + """Ensure the widget always has a parent to avoid segfaults.""" + + def __init__(self, *args: object, **kwargs: object) -> None: + if "parent" not in kwargs and (not args or args[0] is None): + # wx.Window segfaults on Reparent if created without + # a parent, so use a temporary hidden frame. + kwargs["parent"] = wx.Frame(None) + super().__init__(*args, **kwargs) + + return WxRenderWidget raise ValueError(f"Unsupported frontend: {frontend}") # pragma: no cover diff --git a/tests/conftest.py b/tests/conftest.py index 3c923111..03eb6bcc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -91,15 +91,22 @@ def _catch_qt_leaks(request: FixtureRequest, qapp: QApplication) -> Iterator[Non # if the test failed, don't worry about checking widgets if request.session.testsfailed - failures_before: return + allow: list[type] = [] try: from vispy.app.backends._qt import CanvasBackendDesktop - allow: tuple[type, ...] = (CanvasBackendDesktop,) + allow.append(CanvasBackendDesktop) except (ImportError, RuntimeError): - allow = () + pass + try: + import rendercanvas.qt + + allow.append(rendercanvas.qt.QRenderWidget) + except (ImportError, RuntimeError): + pass # This is a known widget that is not cleaned up properly - remaining = [w for w in qapp.topLevelWidgets() if not isinstance(w, allow)] + remaining = [w for w in qapp.topLevelWidgets() if not isinstance(w, tuple(allow))] if len(remaining) > nbefore: test_node = request.node diff --git a/tests/views/_pygfx/test_histogram.py b/tests/views/_pygfx/test_histogram.py index 8e329237..3283d761 100644 --- a/tests/views/_pygfx/test_histogram.py +++ b/tests/views/_pygfx/test_histogram.py @@ -85,6 +85,13 @@ def test_interaction() -> None: # gamma=2, ) histogram = PyGFXHistogramCanvas() + # Ensure the canvas has a real size (rendercanvas >=2.6 returns (1, 1) + # before the widget is shown). Qt's show() triggers a resize, but for + # other backends we force it via _size_info directly. + if hasattr(histogram._canvas, "show"): + histogram._canvas.show() + else: + histogram._canvas._size_info.set_physical_size(640, 480, 1.0) histogram.set_range(x=(0, 10), y=(0, 1)) histogram.model = model left, right = 0, 10