2
2
import sys
3
3
import time
4
4
import logging
5
+ from contextlib import contextmanager
5
6
import ctypes .util
6
7
from collections import defaultdict
7
8
8
9
9
10
logger = logging .getLogger ("wgpu" )
10
11
12
+ err_hashes = {}
13
+
14
+
15
+ @contextmanager
16
+ def log_exception (kind ):
17
+ """Context manager to log any exceptions, but only log a one-liner
18
+ for subsequent occurances of the same error to avoid spamming by
19
+ repeating errors in e.g. a draw function or event callback.
20
+ """
21
+ try :
22
+ yield
23
+ except Exception as err :
24
+ # Store exc info for postmortem debugging
25
+ exc_info = list (sys .exc_info ())
26
+ exc_info [2 ] = exc_info [2 ].tb_next # skip *this* function
27
+ sys .last_type , sys .last_value , sys .last_traceback = exc_info
28
+ # Show traceback, or a one-line summary
29
+ msg = str (err )
30
+ msgh = hash (msg )
31
+ if msgh not in err_hashes :
32
+ # Provide the exception, so the default logger prints a stacktrace.
33
+ # IDE's can get the exception from the root logger for PM debugging.
34
+ err_hashes [msgh ] = 1
35
+ logger .error (kind , exc_info = err )
36
+ else :
37
+ # We've seen this message before, return a one-liner instead.
38
+ err_hashes [msgh ] = count = err_hashes [msgh ] + 1
39
+ msg = kind + ": " + msg .split ("\n " )[0 ].strip ()
40
+ msg = msg if len (msg ) <= 70 else msg [:69 ] + "…"
41
+ logger .error (msg + f" ({ count } )" )
42
+
11
43
12
44
class WgpuCanvasInterface :
13
45
"""This is the interface that a canvas object must implement in order
@@ -109,41 +141,18 @@ def _draw_frame_and_present(self):
109
141
# Perform the user-defined drawing code. When this errors,
110
142
# we should report the error and then continue, otherwise we crash.
111
143
# Returns the result of the context's present() call or None.
112
- try :
144
+ with log_exception ( "Draw error" ) :
113
145
self .draw_frame ()
114
- except Exception as err :
115
- self ._log_exception ("Draw error" , err )
116
- try :
146
+ with log_exception ("Present error" ):
117
147
if self ._canvas_context :
118
148
return self ._canvas_context .present ()
119
- except Exception as err :
120
- self ._log_exception ("Present error" , err )
121
149
122
150
def _get_draw_wait_time (self ):
123
151
"""Get time (in seconds) to wait until the next draw in order to honour max_fps."""
124
152
now = time .perf_counter ()
125
153
target_time = self ._last_draw_time + 1.0 / self ._max_fps
126
154
return max (0 , target_time - now )
127
155
128
- def _log_exception (self , kind , err ):
129
- """Log the given exception instance, but only log a one-liner for
130
- subsequent occurances of the same error to avoid spamming (which
131
- can happen easily with errors in the drawing code).
132
- """
133
- msg = str (err )
134
- msgh = hash (msg )
135
- if msgh not in self ._err_hashes :
136
- # Provide the exception, so the default logger prints a stacktrace.
137
- # IDE's can get the exception from the root logger for PM debugging.
138
- self ._err_hashes [msgh ] = 1
139
- logger .error (kind , exc_info = err )
140
- else :
141
- # We've seen this message before, return a one-liner instead.
142
- self ._err_hashes [msgh ] = count = self ._err_hashes [msgh ] + 1
143
- msg = kind + ": " + msg .split ("\n " )[0 ].strip ()
144
- msg = msg if len (msg ) <= 70 else msg [:69 ] + "…"
145
- logger .error (msg + f" ({ count } )" )
146
-
147
156
# Methods that must be overloaded
148
157
149
158
def get_pixel_ratio (self ):
@@ -184,8 +193,45 @@ class WgpuAutoGui:
184
193
185
194
def __init__ (self , * args , ** kwargs ):
186
195
super ().__init__ (* args , ** kwargs )
196
+ self ._last_event_time = 0
197
+ self ._pending_events = {}
187
198
self ._event_handlers = defaultdict (set )
188
199
200
+ def _get_event_wait_time (self ):
201
+ """Calculate the time to wait for the next event dispatching
202
+ (for rate-limited events)."""
203
+ rate = 75 # events per second
204
+ now = time .perf_counter ()
205
+ target_time = self ._last_event_time + 1.0 / rate
206
+ return max (0 , target_time - now )
207
+
208
+ def _handle_event_rate_limited (self , ev , call_later_func , match_keys , accum_keys ):
209
+ """Alternative `to handle_event()` for events that must be rate-limted.
210
+ If any of the `match_keys` keys of the new event differ from the currently
211
+ pending event, the old event is dispatched now. The `accum_keys` keys of
212
+ the current and new event are added together (e.g. to accumulate wheel delta).
213
+
214
+ This method is called in the following cases:
215
+ * When the timer runs out.
216
+ * When a non-rate-limited event is dispatched.
217
+ * When a rate-limited event of the same type is scheduled
218
+ that has different match_keys (e.g. modifiers changes).
219
+ """
220
+ event_type = ev ["event_type" ]
221
+ # We may need to emit the old event. Otherwise, we need to update the new one.
222
+ old = self ._pending_events .get (event_type , None )
223
+ if old :
224
+ if any (ev [key ] != old [key ] for key in match_keys ):
225
+ self ._dispatch_event (old )
226
+ else :
227
+ for key in accum_keys :
228
+ ev [key ] = old [key ] + ev [key ]
229
+ # Make sure that we have scheduled a moment to handle events
230
+ if not self ._pending_events :
231
+ call_later_func (self ._get_event_wait_time (), self ._dispatch_pending_events )
232
+ # Store the event object
233
+ self ._pending_events [event_type ] = ev
234
+
189
235
def handle_event (self , event ):
190
236
"""Handle an incoming event.
191
237
@@ -194,9 +240,25 @@ def handle_event(self, event):
194
240
is a dict with at least the key event_type. For details, see
195
241
https://jupyter-rfb.readthedocs.io/en/latest/events.html
196
242
"""
243
+ # On any not-rate-limited event, we dispatch any pending events.
244
+ # This is to make sure that the original order of events is preserved.
245
+ self ._dispatch_pending_events ()
246
+ self ._dispatch_event (event )
247
+
248
+ def _dispatch_pending_events (self ):
249
+ """Handle any pending rate-limited events."""
250
+ events = self ._pending_events .values ()
251
+ self ._last_event_time = time .perf_counter ()
252
+ self ._pending_events = {}
253
+ for ev in events :
254
+ self ._dispatch_event (ev )
255
+
256
+ def _dispatch_event (self , event ):
257
+ """Dispatch event to the event handlers."""
197
258
event_type = event .get ("event_type" )
198
259
for callback in self ._event_handlers [event_type ]:
199
- callback (event )
260
+ with log_exception (f"Error during handling { event ['event_type' ]} event" ):
261
+ callback (event )
200
262
201
263
def add_event_handler (self , * args ):
202
264
"""Register an event handler.
0 commit comments