Skip to content

Commit

Permalink
fix wx
Browse files Browse the repository at this point in the history
  • Loading branch information
almarklein committed Dec 6, 2024
1 parent e3c9650 commit 1401a35
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 23 deletions.
4 changes: 2 additions & 2 deletions examples/cube_wx.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Run a wgpu example on the wx backend.
"""

from rendercanvas.wx import RenderCanvas, run
from rendercanvas.wx import RenderCanvas, loop

from rendercanvas.utils.cube import setup_drawing_sync

Expand All @@ -18,4 +18,4 @@


if __name__ == "__main__":
run()
loop.run()
26 changes: 17 additions & 9 deletions rendercanvas/_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ class BaseLoop:
* off (0): the initial state, the subclass should probably not even import dependencies yet.
* ready (1): the first canvas is created, ``_rc_init()`` is called to get the loop ready for running.
* active (2): the loop is active, but not running via our entrypoints.
* running (3): the loop is running via ``_rc_run()`` or ``_rc_run_async()``.
* active (3): the loop is inter-active in e.g. an IDE.
* running (4): the loop is running via ``_rc_run()`` or ``_rc_run_async()``.
Notes:
Expand All @@ -44,17 +45,21 @@ class BaseLoop:
def __init__(self):
self.__tasks = set()
self.__canvas_groups = set()
self.__state = 0 # 0: idle, 1: ready, 2: active, 3: running via our entrypoint
self.__should_stop = 0
self.__state = (
0 # 0: off, 1: ready, 2: detected-active, 3: inter-active, 4: running
)

def __repr__(self):
state = ["off", "ready", "active", "running"][self.__state]
return f"<{self.__class__.__module__}.{self.__class__.__name__} '{state}' at {hex(id(self))}>"
full_class_name = f"{self.__class__.__module__}.{self.__class__.__name__}"
state = self.__state
state_str = ["off", "ready", "active", "active", "running"][state]
return f"<{full_class_name} '{state_str}' ({state}) at {hex(id(self))}>"

def _mark_as_interactive(self):
"""For subclasses to set active from ``_rc_init()``"""
if self.__state == 1:
self.__state = 2
if self.__state in (1, 2):
self.__state = 3

def _register_canvas_group(self, canvas_group):
# A CanvasGroup will call this every time that a new canvas is created for this loop.
Expand Down Expand Up @@ -201,7 +206,10 @@ def run(self):
# Yes we can
pass
elif self.__state == 2:
# No, already active (interactive mode)
# We look active, but have not been marked interactive
pass
elif self.__state == 3:
# No, already marked active (interactive mode)
return
else:
# No, what are you doing??
Expand Down Expand Up @@ -245,7 +253,7 @@ def stop(self):
"""Close all windows and stop the currently running event-loop.
If the loop is active but not running via our ``run()`` method, the loop
moves back to its "off" state, but the underlying loop is not stopped.
moves back to its off-state, but the underlying loop is not stopped.
"""
# Only take action when we're inside the run() method
self.__should_stop += 1
Expand All @@ -254,7 +262,7 @@ def stop(self):
self._stop()

def _stop(self):
"""Move to our off state."""
"""Move to the off-state."""
# If we used the async adapter, cancel any tasks
while self.__tasks:
task = self.__tasks.pop()
Expand Down
3 changes: 3 additions & 0 deletions rendercanvas/asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ def _rc_init(self):
def _rc_run(self):
import asyncio

if self._interactive_loop is not None:
return

asyncio.run(self._rc_run_async())

async def _rc_run_async(self):
Expand Down
4 changes: 3 additions & 1 deletion rendercanvas/qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ def _rc_init(self):
app = QtWidgets.QApplication.instance()
if app is None:
self._app = QtWidgets.QApplication([])
if already_had_app_on_import:
self._mark_as_interactive()

def _rc_run(self):
# Note: we could detect if asyncio is running (interactive session) and wheter
Expand All @@ -178,7 +180,7 @@ def _rc_run(self):
# or not.

if already_had_app_on_import:
return # Likely in an interactive session or larger application that will start the Qt app.
return

self._we_run_the_loop = True
try:
Expand Down
33 changes: 22 additions & 11 deletions rendercanvas/wx.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,10 @@ class WxLoop(BaseLoop):

def _rc_init(self):
if self._app is None:
app = wx.App.GetInstance()
if app is None:
self._app = wx.App.GetInstance()
if self._app is None:
self._app = wx.App()
wx.App.SetInstance(app)
wx.App.SetInstance(self._app)

def _rc_run(self):
self._app.MainLoop()
Expand All @@ -166,7 +166,15 @@ def _rc_add_task(self, async_func, name):
return super()._rc_add_task(async_func, name)

def _rc_call_later(self, delay, callback):
raise NotImplementedError() # todo: wx.CallSoon(callback, args)
wx.CallLater(int(delay * 1000), callback)

def process_wx_events(self):
old_loop = wx.GUIEventLoop.GetActive()
event_loop = wx.GUIEventLoop()
wx.EventLoop.SetActive(event_loop)
while event_loop.Pending():
event_loop.Dispatch()
wx.EventLoop.SetActive(old_loop)


loop = WxLoop()
Expand Down Expand Up @@ -257,13 +265,7 @@ def _rc_gui_poll(self):
if isinstance(self._rc_canvas_group.get_loop(), WxLoop):
pass # all is well
else:
old_loop = wx.EventLoop.GetActive()
event_loop = wx.EventLoop()
wx.EventLoop.SetActive(event_loop)
while event_loop.Pending():
event_loop.Dispatch()
self.app.ProcessIdle()
wx.EventLoop.SetActive(old_loop)
loop.process_wx_events()

def _rc_get_present_methods(self):
if self._surface_ids is None:
Expand Down Expand Up @@ -510,6 +512,15 @@ def Destroy(self): # noqa: N802 - this is a wx method
self._subwidget._is_closed = True
super().Destroy()

# wx stops running its loop as soon as the last canvas closes.
# So when that happens, we manually run the loop for a short while
# so that we can clean up properly
if not self._subwidget._rc_canvas_group.get_canvases():
etime = time.perf_counter() + 0.15
while time.perf_counter() < etime:
time.sleep(0.01)
loop.process_wx_events()


# Make available under a name that is the same for all gui backends
RenderWidget = WxRenderWidget
Expand Down

0 comments on commit 1401a35

Please sign in to comment.