pyglet 3.0.dev7__py3-none-any.whl → 3.0.dev8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. pyglet/__init__.py +22 -13
  2. pyglet/__init__.pyi +1 -1
  3. pyglet/app/__init__.py +2 -11
  4. pyglet/app/async_app.py +196 -121
  5. pyglet/app/base.py +128 -15
  6. pyglet/clock.py +155 -55
  7. pyglet/config/gl/egl.py +1 -1
  8. pyglet/display/__init__.py +1 -1
  9. pyglet/display/base.py +1 -1
  10. pyglet/display/headless.py +29 -6
  11. pyglet/display/wayland.py +1 -1
  12. pyglet/enums.py +14 -0
  13. pyglet/experimental/flaccodec.py +2 -2
  14. pyglet/experimental/geoshader_sprite.py +14 -12
  15. pyglet/experimental/particles.py +11 -10
  16. pyglet/font/__init__.py +30 -8
  17. pyglet/font/base.py +40 -0
  18. pyglet/font/dwrite/__init__.py +251 -78
  19. pyglet/font/dwrite/d2d1_lib.py +145 -9
  20. pyglet/font/dwrite/d2d1_types_lib.py +43 -0
  21. pyglet/font/dwrite/dwrite_lib.py +1 -1
  22. pyglet/font/fontconfig.py +37 -1
  23. pyglet/font/freetype.py +160 -9
  24. pyglet/font/freetype_lib.py +47 -1
  25. pyglet/font/group.py +120 -83
  26. pyglet/font/pyodide_js.py +112 -37
  27. pyglet/font/quartz.py +141 -20
  28. pyglet/font/ttf.py +17 -1
  29. pyglet/font/user.py +4 -0
  30. pyglet/font/win32.py +18 -0
  31. pyglet/graphics/__init__.py +6 -3
  32. pyglet/graphics/api/base.py +75 -2
  33. pyglet/graphics/api/gl/context.py +13 -15
  34. pyglet/graphics/api/gl/egl/context.py +1 -1
  35. pyglet/graphics/api/gl/enums.py +2 -1
  36. pyglet/graphics/api/gl/gl_info.py +109 -1
  37. pyglet/graphics/api/gl/shader.py +25 -96
  38. pyglet/graphics/api/gl/shapes.py +1 -1
  39. pyglet/graphics/api/gl/sprite.py +3 -3
  40. pyglet/graphics/api/gl/text.py +22 -5
  41. pyglet/graphics/api/gl/texture.py +314 -115
  42. pyglet/graphics/api/gl/vertexdomain.py +2 -3
  43. pyglet/graphics/api/gl2/shader.py +2 -1
  44. pyglet/graphics/api/gl2/shapes.py +2 -2
  45. pyglet/graphics/api/gl2/sprite.py +2 -2
  46. pyglet/graphics/api/gl2/text.py +22 -5
  47. pyglet/graphics/api/gl2/vertexdomain.py +0 -2
  48. pyglet/graphics/api/webgl/buffer.py +51 -12
  49. pyglet/graphics/api/webgl/enums.py +2 -1
  50. pyglet/graphics/api/webgl/gl_info.py +47 -1
  51. pyglet/graphics/api/webgl/shader.py +5 -86
  52. pyglet/graphics/api/webgl/shapes.py +1 -1
  53. pyglet/graphics/api/webgl/sprite.py +3 -3
  54. pyglet/graphics/api/webgl/text.py +22 -5
  55. pyglet/graphics/api/webgl/texture.py +156 -203
  56. pyglet/graphics/buffer.py +8 -0
  57. pyglet/graphics/draw.py +31 -10
  58. pyglet/graphics/shader.py +287 -27
  59. pyglet/graphics/texture.py +120 -7
  60. pyglet/graphics/vertexdomain.py +18 -1
  61. pyglet/gui/ninepatch.py +6 -6
  62. pyglet/gui/widgets.py +9 -8
  63. pyglet/image/__init__.py +27 -1
  64. pyglet/image/codecs/__init__.py +8 -0
  65. pyglet/image/codecs/js_image.py +61 -76
  66. pyglet/image/codecs/pil.py +2 -2
  67. pyglet/image/codecs/wic.py +106 -84
  68. pyglet/info.py +402 -47
  69. pyglet/libs/darwin/cocoapy/cocoalibs.py +24 -0
  70. pyglet/libs/egl/__init__.py +2 -0
  71. pyglet/libs/{linux/egl → egl}/eglext.py +2 -2
  72. pyglet/libs/emscripten/__init__.py +72 -0
  73. pyglet/libs/emscripten/files.py +45 -0
  74. pyglet/libs/emscripten/filesystem.py +63 -0
  75. pyglet/libs/emscripten/proxies.py +55 -0
  76. pyglet/libs/emscripten/pyglet_emscripten.js +79 -0
  77. pyglet/libs/win32/__init__.py +2 -0
  78. pyglet/libs/win32/com.py +16 -4
  79. pyglet/media/codecs/base.py +88 -110
  80. pyglet/media/codecs/coreaudio.py +2 -2
  81. pyglet/media/codecs/ffmpeg.py +17 -59
  82. pyglet/media/codecs/ffmpeg_lib/libswresample.py +0 -3
  83. pyglet/media/codecs/gstreamer.py +2 -5
  84. pyglet/media/codecs/pyogg.py +4 -4
  85. pyglet/media/codecs/wave.py +2 -4
  86. pyglet/media/codecs/webaudio_pyodide.py +1 -1
  87. pyglet/media/codecs/wmf.py +5 -10
  88. pyglet/media/devices/base.py +37 -29
  89. pyglet/media/drivers/base.py +98 -142
  90. pyglet/media/drivers/directsound/adaptation.py +2 -5
  91. pyglet/media/drivers/openal/adaptation.py +2 -7
  92. pyglet/media/drivers/pulse/adaptation.py +3 -5
  93. pyglet/media/drivers/pyodide_js/adaptation.py +1 -34
  94. pyglet/media/drivers/silent/adaptation.py +2 -6
  95. pyglet/media/drivers/xaudio2/adaptation.py +222 -89
  96. pyglet/media/drivers/xaudio2/interface.py +469 -293
  97. pyglet/media/player.py +28 -2
  98. pyglet/media/player_worker_thread.py +4 -3
  99. pyglet/media/synthesis.py +2 -4
  100. pyglet/model/__init__.py +24 -23
  101. pyglet/model/codecs/obj.py +15 -9
  102. pyglet/resource.py +1 -1
  103. pyglet/shapes.py +56 -56
  104. pyglet/sprite.py +12 -12
  105. pyglet/storage.py +398 -0
  106. pyglet/text/__init__.py +105 -25
  107. pyglet/text/caret.py +20 -20
  108. pyglet/text/document.py +23 -1
  109. pyglet/text/effects.py +62 -0
  110. pyglet/text/formats/html.py +26 -3
  111. pyglet/text/formats/structured.py +67 -7
  112. pyglet/text/layout/__init__.py +9 -1
  113. pyglet/text/layout/base.py +191 -986
  114. pyglet/text/layout/boxes.py +1038 -0
  115. pyglet/text/layout/flow.py +510 -0
  116. pyglet/text/layout/incremental.py +32 -4
  117. pyglet/text/layout/scrolling.py +13 -3
  118. pyglet/window/__init__.py +24 -10
  119. pyglet/window/camera/base.py +7 -3
  120. pyglet/window/camera/camera2d.py +3 -2
  121. pyglet/window/camera/camera3d.py +3 -2
  122. pyglet/window/dialog/__init__.py +14 -4
  123. pyglet/window/dialog/base.py +34 -3
  124. pyglet/window/dialog/darwin.py +4 -4
  125. pyglet/window/dialog/emscripten.py +74 -0
  126. pyglet/window/dialog/linux.py +6 -4
  127. pyglet/window/dialog/windows.py +2 -2
  128. pyglet/window/emscripten/__init__.py +114 -21
  129. pyglet/window/headless/__init__.py +42 -18
  130. pyglet/window/wayland/__init__.py +1 -1
  131. {pyglet-3.0.dev7.dist-info → pyglet-3.0.dev8.dist-info}/METADATA +23 -3
  132. {pyglet-3.0.dev7.dist-info → pyglet-3.0.dev8.dist-info}/RECORD +136 -126
  133. pyglet/libs/linux/egl/__init__.py +0 -2
  134. /pyglet/libs/{linux/egl → egl}/egl.py +0 -0
  135. /pyglet/libs/{linux/egl → egl}/lib.py +0 -0
  136. {pyglet-3.0.dev7.dist-info → pyglet-3.0.dev8.dist-info}/WHEEL +0 -0
  137. {pyglet-3.0.dev7.dist-info → pyglet-3.0.dev8.dist-info}/licenses/LICENSE +0 -0
pyglet/__init__.py CHANGED
@@ -18,12 +18,15 @@ if TYPE_CHECKING:
18
18
  from typing import Any, Callable, ItemsView, Sized
19
19
 
20
20
  #: The release version
21
- version = '3.0.dev7'
21
+ version = '3.0.dev8'
22
22
  __version__ = version
23
23
 
24
24
  MIN_PYTHON_VERSION = 3, 10
25
25
  MIN_PYTHON_VERSION_STR = ".".join([str(v) for v in MIN_PYTHON_VERSION])
26
26
 
27
+ #: The Pyodide release used to develop and test pyglet's browser support.
28
+ PYODIDE_VERSION = "0.29.4"
29
+
27
30
  if sys.version_info < MIN_PYTHON_VERSION:
28
31
  msg = f"pyglet {version} requires Python {MIN_PYTHON_VERSION_STR} or newer."
29
32
  raise Exception(msg)
@@ -56,7 +59,6 @@ class PyodideOptions:
56
59
  page, and do not want to alter your code, then modify this option.
57
60
  """
58
61
 
59
-
60
62
  @dataclass
61
63
  class Options:
62
64
  """Dataclass for global pyglet options."""
@@ -208,25 +210,30 @@ class Options:
208
210
  .. versionadded:: 2.0
209
211
  """
210
212
 
211
- dw_legacy_naming: bool = False
212
- """If ``True``, will enable legacy naming support for the default Windows font renderer (``DirectWrite``).
213
- Attempt to parse fonts by the passed name, to best match legacy RBIZ naming.
213
+ font_name_compatibility: bool = False
214
+ """If ``True``, pyglet performs additional compatibility lookup for font names.
214
215
 
215
- :see: https://learn.microsoft.com/en-us/windows/win32/directwrite/font-selection#rbiz-font-family-model
216
+ The portable spelling of a font is its family name with explicit ``weight``, ``style``, and ``stretch``. This
217
+ option additionally accepts OpenType full names and platform compatibility aliases where the backend supports
218
+ them. For example, it allows ``"Arial Narrow"`` rather than ``"Arial"`` with a ``"condensed"`` stretch, or
219
+ ``"Arial Black"`` instead of ``"Arial"`` with a weight of ``"black"``.
216
220
 
217
- For example, this allows specifying ``"Arial Narrow"`` rather than ``"Arial"`` with a ``"condensed"`` stretch or
218
- ``"Arial Black"`` instead of ``"Arial"`` with a weight of ``black``. This may enhance naming compatibility
219
- cross-platform for select fonts as older font renderers went by this naming scheme.
221
+ This option does not reject full names when disabled: a platform font API may still accept them natively. It only
222
+ enables pyglet's extra compatibility lookup and mapping, which can improve cross-platform behavior at the cost of
223
+ slower font resolution.
220
224
 
221
- Starts by parsing the string for any known style names, and searches all font collections for a matching RBIZ name.
222
- If a perfect match is not found, it will choose a second best match.
225
+ On Windows with DirectWrite, GDI/RBIZ aliases are not indexed as family names. Resolving one requires enumerating
226
+ installed font families and faces, and can be noticeably slow on systems with many installed fonts. Results are
227
+ cached until a custom font is added.
228
+
229
+ :see: https://learn.microsoft.com/en-us/windows/win32/directwrite/font-selection#rbiz-font-family-model
223
230
 
224
231
  .. note:: Due to the high variation of styles and limited capability of some fonts, there is no guarantee the
225
- second closest match will be exactly what the user wants.
232
+ selected face will be exactly what the user wants.
226
233
 
227
234
  .. note:: The ``debug_font`` option can provide information on what settings are being selected.
228
235
 
229
- .. versionadded:: 2.0.3
236
+ .. versionadded:: 3.0
230
237
  """
231
238
 
232
239
  win32_disable_xinput: bool = False
@@ -502,6 +509,7 @@ if TYPE_CHECKING:
502
509
  resource,
503
510
  shapes,
504
511
  sprite,
512
+ storage,
505
513
  text,
506
514
  window,
507
515
  )
@@ -525,6 +533,7 @@ else:
525
533
  resource = _ModuleProxy("resource") # type: ignore
526
534
  sprite = _ModuleProxy("sprite") # type: ignore
527
535
  shapes = _ModuleProxy("shapes") # type: ignore
536
+ storage = _ModuleProxy("storage") # type: ignore
528
537
  text = _ModuleProxy("text") # type: ignore
529
538
  window = _ModuleProxy("window") # type: ignore
530
539
 
pyglet/__init__.pyi CHANGED
@@ -64,7 +64,7 @@ class Options:
64
64
  headless: bool
65
65
  headless_device: int
66
66
  text_shaping: Literal["platform", "harfbuzz", False]
67
- dw_legacy_naming: bool
67
+ font_name_compatibility: bool
68
68
  win32_disable_xinput: bool
69
69
  com_mta: bool
70
70
  osx_alt_loop: bool
pyglet/app/__init__.py CHANGED
@@ -80,11 +80,7 @@ def run(interval: float | None = 1 / 60) -> None:
80
80
  pyglet.app.event_loop.run(interval)
81
81
 
82
82
  """
83
- if pyglet.compat_platform == "emscripten":
84
- import asyncio
85
- asyncio.create_task(event_loop.run(interval)) # noqa: RUF006
86
- else:
87
- event_loop.run(interval)
83
+ event_loop.run(interval)
88
84
 
89
85
 
90
86
  def exit() -> None:
@@ -99,12 +95,7 @@ def exit() -> None:
99
95
  pyglet.app.event_loop.exit()
100
96
 
101
97
  """
102
- if pyglet.compat_platform == "emscripten":
103
- import asyncio
104
-
105
- asyncio.create_task(event_loop.exit())
106
- else:
107
- event_loop.exit()
98
+ event_loop.exit()
108
99
 
109
100
 
110
101
  #: The global event loop. Applications can replace this
pyglet/app/async_app.py CHANGED
@@ -1,39 +1,40 @@
1
+ """Asyncio event-loop support for the Emscripten platform."""
1
2
  from __future__ import annotations
2
3
 
3
4
  import asyncio
5
+ from contextlib import suppress
4
6
  import sys
5
7
  from typing import Any, Callable, TYPE_CHECKING
6
8
 
7
9
  import pyglet
8
10
  from pyglet import app, clock, event
11
+ from pyglet.app.base import ClockWindowDrawSource, WindowDrawSource
9
12
 
10
13
  if TYPE_CHECKING:
11
14
  from pyglet.event import EventDispatcher
12
15
  from pyglet.window import BaseWindow
13
16
 
14
- _is_pyglet_doc_run = hasattr(sys, "is_pyglet_doc_run") and sys.is_pyglet_doc_run
15
17
  try:
16
- from js import requestAnimationFrame, performance # noqa: F821
17
- from pyodide.ffi import create_proxy # noqa: F821
18
- except ImportError:
19
- raise ImportError('Pyodide not available.')
18
+ from js import cancelAnimationFrame, requestAnimationFrame, performance
19
+ except ImportError as exc:
20
+ raise ImportError('Pyodide not available.') from exc
21
+
22
+ from pyglet.libs.emscripten.proxies import ProxyRegistry
20
23
 
21
24
 
22
25
  class AsyncEventLoop(event.EventDispatcher):
23
- """Asyncio-based event loop for Pyglet."""
26
+ """Browser event loop which preserves pyglet's normal clock semantics."""
24
27
 
25
28
  def __init__(self) -> None:
29
+ """Create the browser clock and callback state."""
26
30
  self._has_exit_event = asyncio.Event()
27
- # Change the default clock to use the javascript performance timer.
28
31
  self.clock = clock.Clock(self._get_js_time)
29
32
  pyglet.clock.set_default(self.clock)
30
33
  self.is_running = False
31
- self._interval = None
32
- self._last_ts = 0.0
33
- self._frame_dt = 0.0
34
-
35
- self._frame_future = None
36
- self._frame_proxy = create_proxy(self._frame_cb)
34
+ self._interval: float | None = None
35
+ self._run_task: asyncio.Task[None] | None = None
36
+ self._window_draw_source: WindowDrawSource | None = None
37
+ self._window_draw_exception: tuple[type[BaseException], BaseException, Any] | None = None
37
38
 
38
39
  def _get_js_time(self) -> float:
39
40
  """Use Javascripts performance.now for the clock accuracy.
@@ -42,171 +43,245 @@ class AsyncEventLoop(event.EventDispatcher):
42
43
  """
43
44
  return performance.now() / 1000
44
45
 
45
- @staticmethod
46
- def _redraw_windows(dt: float) -> None:
47
- # Redraw all windows
48
- for window in app.windows:
46
+ def _redraw_windows(self, dt: float) -> None:
47
+ for window in tuple(app.windows):
48
+ self._dispatch_platform_draw(window, dt)
49
+
50
+ def _dispatch_platform_draw(self, window: BaseWindow, dt: float) -> None:
51
+ try:
49
52
  window.draw(dt)
53
+ except BaseException: # noqa: BLE001 - JS callbacks cannot unwind Python exceptions.
54
+ self._window_draw_exception = sys.exc_info()
55
+ self.exit()
50
56
 
51
- def _frame_cb(self, ts: float) -> None:
52
- """Callback for the requestAnimationFrame."""
53
- now = ts / 1000.0
54
- _frame_dt = now - self._last_ts
57
+ def _raise_window_draw_exception(self) -> None:
58
+ if self._window_draw_exception is None:
59
+ return
55
60
 
56
- self._last_ts = now
61
+ _, exception, traceback = self._window_draw_exception
62
+ self._window_draw_exception = None
63
+ raise exception.with_traceback(traceback)
57
64
 
58
- if self._frame_future:
59
- self._frame_future.set_result(_frame_dt)
60
- self._frame_future = None
65
+ def _schedule_window_draw(self, interval: float | None) -> None:
66
+ self._interval = interval
67
+ if interval is None:
68
+ return
61
69
 
62
- async def _next_animation_frame(self) -> float:
63
- self._frame_future = asyncio.Future()
64
- requestAnimationFrame(self._frame_proxy)
65
- return await self._frame_future
70
+ draw_source = app.platform_event_loop.create_window_draw_source(self)
71
+ if draw_source is None:
72
+ draw_source = ClockWindowDrawSource(self.clock, self._redraw_windows)
66
73
 
67
- async def draw_frame(self) -> None:
68
- frame_dt = await self._next_animation_frame()
69
- self._redraw_windows(frame_dt)
74
+ self._window_draw_source = draw_source
75
+ if not draw_source.start(interval):
76
+ draw_source.stop()
77
+ self._window_draw_source = ClockWindowDrawSource(self.clock, self._redraw_windows)
78
+ self._window_draw_source.start(interval)
70
79
 
71
- async def run(self, interval: float | None = 1/60) -> None:
72
- try:
73
- """Begin processing events asynchronously."""
74
- self._interval = interval
80
+ def _unschedule_window_draw(self) -> None:
81
+ if self._window_draw_source is not None:
82
+ self._window_draw_source.stop()
83
+ self._window_draw_source = None
75
84
 
76
- self._has_exit_event.clear()
77
- platform_event_loop = app.platform_event_loop
85
+ def run(self, interval: float | None = 1 / 60) -> None:
86
+ """Start processing browser events, scheduled functions, and window draws."""
87
+ if self._run_task is not None and not self._run_task.done():
88
+ raise RuntimeError("The pyglet event loop is already running.")
78
89
 
79
- await platform_event_loop.start()
80
- self.dispatch_event('on_enter')
81
- self.is_running = True
90
+ task = asyncio.create_task(self._run(interval))
91
+ self._run_task = task
82
92
 
83
- # Use the browser's requestAnimationFrame for the loop if None is used.
84
- if self._interval is None:
85
- while not self._has_exit_event.is_set():
86
- await self.draw_frame()
87
- self.idle() # Ticks clock and runs scheduled functions.
88
- await platform_event_loop.dispatch_posted_events()
89
- else:
90
- _schedule_rate = self._interval / 1000.0
93
+ def clear_task(completed_task: asyncio.Task[None]) -> None:
94
+ if self._run_task is completed_task:
95
+ self._run_task = None
91
96
 
92
- # Separate draw loop so clock timeout doesn't block rendering if it's slower.
93
- async def _draw_loop() -> None:
94
- while not self._has_exit_event.is_set():
95
- await self.draw_frame()
97
+ task.add_done_callback(clear_task)
96
98
 
97
- asyncio.create_task(_draw_loop()) # noqa: RUF006
99
+ async def _run(self, interval: float | None = 1 / 60) -> None:
100
+ """Process pyglet clock callbacks and browser events."""
101
+ if self.is_running:
102
+ raise RuntimeError("The pyglet event loop is already running.")
98
103
 
99
- while not self._has_exit_event.is_set():
100
- timeout = self.idle()
101
- await platform_event_loop.dispatch_posted_events()
102
- await asyncio.sleep(0 if timeout is None else timeout)
104
+ self._interval = interval
105
+ self._has_exit_event.clear()
106
+ platform_event_loop = app.platform_event_loop
103
107
 
108
+ try:
109
+ platform_event_loop.start()
110
+ self._schedule_window_draw(interval)
111
+ self.dispatch_event('on_enter')
112
+ self.is_running = True
113
+
114
+ while not self._has_exit_event.is_set():
115
+ timeout = self.idle()
116
+ await platform_event_loop.wait(timeout)
117
+ platform_event_loop.dispatch_posted_events()
118
+ finally:
104
119
  self.is_running = False
120
+ platform_event_loop.stop()
121
+ self._unschedule_window_draw()
105
122
  self.dispatch_event('on_exit')
106
- await platform_event_loop.stop()
107
- except Exception as err:
108
- import traceback
109
- print("Error in wrapped task", err)
110
- traceback.print_exc()
111
-
112
- async def enter_blocking(self) -> None:
113
- """Ensure `idle()` continues to be called during blocking operations."""
123
+
124
+ self._raise_window_draw_exception()
125
+
126
+ def enter_blocking(self) -> None:
114
127
  timeout = self.idle()
115
- await app.platform_event_loop.set_timer(self._blocking_timer, timeout)
128
+ app.platform_event_loop.set_timer(self._blocking_timer, timeout)
116
129
 
117
- async def exit_blocking(self) -> None:
118
- """Stop the blocking timer."""
119
- await app.platform_event_loop.set_timer(None, None)
130
+ def exit_blocking(self) -> None:
131
+ app.platform_event_loop.set_timer(None, None)
120
132
 
121
- async def _blocking_timer(self) -> None:
122
- """Called to process scheduled events during blocking operations."""
133
+ def _blocking_timer(self) -> None:
123
134
  dt = self.clock.update_time()
124
135
  self.clock.call_scheduled_functions(dt)
125
- if self._interval is None:
126
- self._redraw_windows(dt)
127
-
128
136
  timeout = self.clock.get_sleep_time(True)
129
- await app.platform_event_loop.set_timer(self._blocking_timer, timeout)
137
+ app.platform_event_loop.set_timer(self._blocking_timer, timeout)
130
138
 
131
139
  def idle(self) -> float | None:
132
- """Called each iteration to process events."""
133
140
  dt = self.clock.update_time()
134
141
  self.clock.call_scheduled_functions(dt)
135
142
  return self.clock.get_sleep_time(True)
136
143
 
137
- async def exit(self) -> None:
138
- """Exit the event loop asynchronously."""
144
+ def exit(self) -> None:
139
145
  self._has_exit_event.set()
140
146
  app.platform_event_loop.notify()
141
147
 
142
148
  async def sleep(self, timeout: float) -> bool:
143
- """Pause execution for a given time unless `exit()` is called."""
144
149
  try:
145
150
  await asyncio.wait_for(self._has_exit_event.wait(), timeout)
146
151
  return True
147
152
  except asyncio.TimeoutError:
148
153
  return False
149
154
 
150
- async def on_window_close(self, _window: BaseWindow) -> None:
151
- """Exit when the last window is closed."""
155
+ def on_window_close(self, _window: BaseWindow) -> None:
152
156
  if len(app.windows) == 0:
153
- await self.exit()
157
+ self.exit()
158
+
154
159
 
155
160
  AsyncEventLoop.register_event_type('on_window_close')
156
161
  AsyncEventLoop.register_event_type('on_enter')
157
162
  AsyncEventLoop.register_event_type('on_exit')
158
163
 
164
+
159
165
  class AsyncPlatformEventLoop:
160
- """An asyncio-based platform event loop."""
166
+ """An asyncio-based platform event loop, currently for supporting Pyodide."""
161
167
 
162
168
  def __init__(self) -> None:
163
- """Initialize the event loop with an asyncio queue and event."""
164
- self._event_queue = asyncio.Queue()
165
- self._is_running = asyncio.Event()
169
+ """Create the browser event queue and wakeup state."""
170
+ self._event_queue: asyncio.Queue[tuple[EventDispatcher, str, tuple[Any, ...]]] = asyncio.Queue()
171
+ self._is_running = False
172
+ self._wake_event = asyncio.Event()
173
+ self._timer_task: asyncio.Task[None] | None = None
166
174
 
167
175
  def is_running(self) -> bool:
168
- """Check if the event loop is currently processing."""
169
- return self._is_running.is_set()
176
+ return self._is_running
170
177
 
171
- async def post_event(self, dispatcher: EventDispatcher, event: str, *args: Any) -> None:
172
- """Post an event into the main application thread asynchronously."""
173
- await self._event_queue.put((dispatcher, event, args))
178
+ def post_event(self, dispatcher: EventDispatcher, event: str, *args: Any) -> None:
179
+ self._event_queue.put_nowait((dispatcher, event, args))
174
180
  self.notify()
175
181
 
176
- async def dispatch_posted_events(self) -> None:
177
- """Dispatch all pending events asynchronously."""
178
- while not self._event_queue.empty():
182
+ def dispatch_posted_events(self) -> None:
183
+ while True:
179
184
  try:
180
- dispatcher, evnt, args = await self._event_queue.get()
181
- dispatcher.dispatch_event(evnt, *args)
182
- except asyncio.CancelledError:
183
- break
184
- except ReferenceError:
185
- # weakly-referenced object no longer exists
186
- pass
185
+ dispatcher, event, args = self._event_queue.get_nowait()
186
+ except asyncio.QueueEmpty:
187
+ return
188
+ with suppress(ReferenceError):
189
+ dispatcher.dispatch_event(event, *args)
187
190
 
188
191
  def notify(self) -> None:
189
- """Trigger an iteration of the asyncio event loop."""
190
- if self.is_running():
191
- asyncio.create_task(self.dispatch_posted_events())
192
+ self._wake_event.set()
193
+
194
+ def start(self) -> None:
195
+ self._is_running = True
196
+
197
+ async def wait(self, timeout: float | None) -> None:
198
+ if not self._event_queue.empty():
199
+ return
192
200
 
193
- async def start(self) -> None:
194
- """Start the asyncio-based event loop."""
195
- self._is_running.set()
201
+ self._wake_event.clear()
202
+ if not self._event_queue.empty():
203
+ return
204
+
205
+ if timeout is None:
206
+ await self._wake_event.wait()
207
+ elif timeout > 0:
208
+ with suppress(asyncio.TimeoutError):
209
+ await asyncio.wait_for(self._wake_event.wait(), timeout)
210
+ else:
211
+ await asyncio.sleep(0)
196
212
 
197
213
  async def step(self, timeout: float | None = None) -> None:
198
- """Run one iteration of the asyncio event loop with a timeout."""
199
- try:
200
- await asyncio.wait_for(self.dispatch_posted_events(), timeout)
201
- except asyncio.TimeoutError:
202
- pass
214
+ await self.wait(timeout)
215
+ self.dispatch_posted_events()
216
+
217
+ def set_timer(self, func: Callable | None, interval: float | None) -> None:
218
+ if self._timer_task is not None:
219
+ self._timer_task.cancel()
220
+ self._timer_task = None
221
+
222
+ if func is None or interval is None or not self.is_running():
223
+ return
224
+
225
+ async def timer() -> None:
226
+ while self.is_running():
227
+ await asyncio.sleep(interval)
228
+ if self.is_running():
229
+ func()
230
+
231
+ self._timer_task = asyncio.create_task(timer())
232
+
233
+ def create_window_draw_source(self, event_loop: AsyncEventLoop) -> WindowDrawSource:
234
+ return RequestAnimationFrameDrawSource(event_loop)
235
+
236
+ def stop(self) -> None:
237
+ self._is_running = False
238
+ self.set_timer(None, None)
239
+ self.notify()
203
240
 
204
- async def set_timer(self, func: Callable, interval: float) -> None:
205
- """Schedule a timer function asynchronously."""
206
- while self.is_running():
207
- await asyncio.sleep(interval)
208
- func()
209
241
 
210
- async def stop(self) -> None:
211
- """Stop the asyncio-based event loop."""
212
- self._is_running.clear()
242
+ class RequestAnimationFrameDrawSource(WindowDrawSource):
243
+ """Draw browser windows when the browser requests an animation frame."""
244
+
245
+ def __init__(self, event_loop: AsyncEventLoop) -> None:
246
+ """Attaches to the browser event loop."""
247
+ self._event_loop = event_loop
248
+ self._last_timestamp: float | None = None
249
+ self._request_id: int | None = None
250
+ self._running = False
251
+ self._callback = None
252
+ self._proxies = ProxyRegistry()
253
+
254
+ def start(self, _interval: float) -> bool:
255
+ self._running = True
256
+ self._last_timestamp = None
257
+ if self._callback is None:
258
+ self._callback = self._proxies.create(self._on_animation_frame)
259
+ self._request_next_frame()
260
+ return True
261
+
262
+ def stop(self) -> None:
263
+ if not self._running:
264
+ return
265
+
266
+ self._running = False
267
+ if self._request_id is not None:
268
+ cancelAnimationFrame(self._request_id)
269
+ self._request_id = None
270
+ self._proxies.destroy()
271
+ self._callback = None
272
+
273
+ def _request_next_frame(self) -> None:
274
+ self._request_id = requestAnimationFrame(self._callback)
275
+
276
+ def _on_animation_frame(self, timestamp: float) -> None:
277
+ if not self._running:
278
+ return
279
+
280
+ now = timestamp / 1000.0
281
+ dt = 0.0 if self._last_timestamp is None else now - self._last_timestamp
282
+ self._last_timestamp = now
283
+ self._request_id = None
284
+ self._event_loop._redraw_windows(dt) # noqa: SLF001
285
+
286
+ if self._running:
287
+ self._request_next_frame()