pyglet 3.0.dev9__py3-none-any.whl → 3.0.dev11__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 (189) hide show
  1. pyglet/__init__.py +13 -2
  2. pyglet/__init__.pyi +3 -0
  3. pyglet/app/__init__.py +10 -3
  4. pyglet/app/base.py +2 -3
  5. pyglet/app/cocoa.py +88 -39
  6. pyglet/app/ios.py +168 -0
  7. pyglet/config/__init__.py +10 -2
  8. pyglet/config/gl/__init__.py +21 -12
  9. pyglet/config/gl/ios.py +45 -0
  10. pyglet/config/gl/macos.py +1 -1
  11. pyglet/config/gl/windows.py +17 -17
  12. pyglet/display/__init__.py +5 -3
  13. pyglet/display/base.py +23 -12
  14. pyglet/display/cocoa.py +4 -0
  15. pyglet/display/emscripten.py +11 -1
  16. pyglet/display/headless.py +5 -1
  17. pyglet/display/ios.py +53 -0
  18. pyglet/display/wayland.py +15 -8
  19. pyglet/display/win32.py +13 -15
  20. pyglet/display/xlib.py +17 -14
  21. pyglet/enums.py +38 -0
  22. pyglet/event.py +5 -5
  23. pyglet/experimental/geoshader_sprite.py +158 -129
  24. pyglet/experimental/particles.py +226 -42
  25. pyglet/font/__init__.py +3 -4
  26. pyglet/font/base.py +27 -22
  27. pyglet/font/dwrite/__init__.py +3 -3
  28. pyglet/font/freetype.py +51 -76
  29. pyglet/font/freetype_lib.py +1 -1
  30. pyglet/font/group.py +18 -4
  31. pyglet/font/harfbuzz.py +3 -3
  32. pyglet/font/quartz.py +48 -48
  33. pyglet/font/user.py +26 -12
  34. pyglet/graphics/__init__.py +9 -2
  35. pyglet/graphics/allocation.py +109 -2
  36. pyglet/graphics/api/__init__.py +16 -6
  37. pyglet/graphics/api/base.py +96 -2
  38. pyglet/graphics/api/gl/__init__.py +0 -3
  39. pyglet/graphics/api/gl/buffer.py +5 -5
  40. pyglet/graphics/api/gl/cocoa/context.py +1 -1
  41. pyglet/graphics/api/gl/context.py +28 -1
  42. pyglet/graphics/api/gl/framebuffer.py +164 -28
  43. pyglet/graphics/api/gl/gl.py +3051 -2622
  44. pyglet/graphics/api/gl/gl_compat.py +7795 -0
  45. pyglet/graphics/api/gl/gl_fallback.py +98 -0
  46. pyglet/graphics/api/gl/gl_info.py +4 -0
  47. pyglet/graphics/api/gl/gles.py +3693 -0
  48. pyglet/graphics/api/gl/global_opengl.py +19 -11
  49. pyglet/graphics/api/gl/ios/__init__.py +3 -0
  50. pyglet/graphics/api/gl/ios/context.py +131 -0
  51. pyglet/graphics/api/gl/lib.py +2 -18
  52. pyglet/graphics/api/gl/shader.py +18 -41
  53. pyglet/graphics/api/gl/shapes.py +3 -2
  54. pyglet/graphics/api/gl/sprite.py +9 -8
  55. pyglet/graphics/api/gl/state.py +4 -0
  56. pyglet/graphics/api/gl/text.py +185 -54
  57. pyglet/graphics/api/gl/texture.py +3 -13
  58. pyglet/graphics/api/gl/vertexdomain.py +155 -267
  59. pyglet/graphics/api/gl/win32/context.py +7 -7
  60. pyglet/graphics/api/gl/xlib/context.py +15 -15
  61. pyglet/graphics/api/gl2/buffer.py +3 -3
  62. pyglet/graphics/api/gl2/framebuffer.py +7 -2
  63. pyglet/graphics/api/gl2/global_opengl.py +13 -9
  64. pyglet/graphics/api/gl2/shader.py +5 -2
  65. pyglet/graphics/api/gl2/shapes.py +3 -2
  66. pyglet/graphics/api/gl2/sprite.py +6 -6
  67. pyglet/graphics/api/gl2/text.py +171 -49
  68. pyglet/graphics/api/gl2/vertexdomain.py +93 -49
  69. pyglet/graphics/api/webgl/__init__.py +22 -14
  70. pyglet/graphics/api/webgl/buffer.py +17 -17
  71. pyglet/graphics/api/webgl/context.py +3 -3
  72. pyglet/graphics/api/webgl/draw.py +2 -2
  73. pyglet/graphics/api/webgl/framebuffer.py +39 -13
  74. pyglet/graphics/api/webgl/gl.py +1 -1
  75. pyglet/graphics/api/webgl/shader.py +22 -57
  76. pyglet/graphics/api/webgl/shapes.py +3 -2
  77. pyglet/graphics/api/webgl/sprite.py +9 -8
  78. pyglet/graphics/api/webgl/state.py +4 -0
  79. pyglet/graphics/api/webgl/text.py +116 -98
  80. pyglet/graphics/api/webgl/texture.py +18 -28
  81. pyglet/graphics/api/webgl/vertexarray.py +3 -3
  82. pyglet/graphics/api/webgl/vertexdomain.py +141 -62
  83. pyglet/graphics/api/webgl/webgl_js.pyi +3 -0
  84. pyglet/graphics/atlas.py +8 -7
  85. pyglet/graphics/attributes.py +221 -0
  86. pyglet/graphics/draw.py +684 -204
  87. pyglet/graphics/framebuffer.py +163 -5
  88. pyglet/graphics/instance.py +310 -2
  89. pyglet/graphics/resource.py +2 -2
  90. pyglet/graphics/shader.py +181 -267
  91. pyglet/graphics/state.py +5 -1
  92. pyglet/graphics/texture.py +26 -36
  93. pyglet/graphics/vertexdomain.py +251 -333
  94. pyglet/graphics/vertexstorage.py +555 -0
  95. pyglet/gui/__init__.py +6 -2
  96. pyglet/gui/layout.py +806 -0
  97. pyglet/gui/manager.py +348 -0
  98. pyglet/gui/ninepatch.py +91 -54
  99. pyglet/gui/styles.py +130 -0
  100. pyglet/gui/widgets.py +610 -115
  101. pyglet/image/__init__.py +9 -14
  102. pyglet/image/animation.py +5 -10
  103. pyglet/image/base.py +52 -43
  104. pyglet/image/codecs/__init__.py +8 -0
  105. pyglet/image/codecs/qoi.py +275 -0
  106. pyglet/image/codecs/quartz.py +14 -14
  107. pyglet/input/__init__.py +2 -3
  108. pyglet/input/base.py +10 -11
  109. pyglet/input/macos/darwin_gc.py +11 -23
  110. pyglet/lib.py +13 -18
  111. pyglet/libs/__init__.py +25 -0
  112. pyglet/libs/darwin/__init__.py +18 -1
  113. pyglet/libs/darwin/agl.py +3 -1
  114. pyglet/libs/darwin/cocoapy/__init__.py +19 -7
  115. pyglet/libs/darwin/cocoapy/cocoatypes.py +17 -6
  116. pyglet/libs/darwin/cocoapy/lib_appkit.py +148 -0
  117. pyglet/libs/darwin/cocoapy/lib_cocoa.py +76 -0
  118. pyglet/libs/darwin/cocoapy/lib_corefoundation.py +253 -0
  119. pyglet/libs/darwin/cocoapy/lib_coregraphics.py +182 -0
  120. pyglet/libs/darwin/cocoapy/lib_coretext.py +123 -0
  121. pyglet/libs/darwin/cocoapy/lib_quartz.py +74 -0
  122. pyglet/libs/darwin/cocoapy/lib_uikit.py +7 -0
  123. pyglet/libs/darwin/cocoapy/runtime.py +227 -64
  124. pyglet/libs/darwin/coreaudio.py +3 -7
  125. pyglet/libs/darwin/lib_agl.py +13 -3
  126. pyglet/libs/egl/__init__.py +1 -2
  127. pyglet/libs/egl/egl.py +728 -565
  128. pyglet/libs/egl/lib.py +15 -8
  129. pyglet/libs/linux/glx/glx.py +439 -568
  130. pyglet/libs/linux/glx/lib_glx.py +3 -2
  131. pyglet/libs/win32/lib_wgl.py +3 -2
  132. pyglet/libs/win32/types.py +66 -0
  133. pyglet/libs/win32/wgl.py +359 -258
  134. pyglet/math.py +8 -8
  135. pyglet/media/__init__.py +64 -5
  136. pyglet/media/codecs/__init__.py +1 -3
  137. pyglet/media/codecs/base.py +15 -14
  138. pyglet/media/codecs/ffmpeg_lib/compat.py +1 -1
  139. pyglet/media/drivers/__init__.py +1 -3
  140. pyglet/media/player.py +161 -103
  141. pyglet/media/synthesis.py +64 -44
  142. pyglet/model/__init__.py +2 -2
  143. pyglet/model/codecs/obj.py +3 -4
  144. pyglet/resource.py +148 -40
  145. pyglet/shapes.py +255 -79
  146. pyglet/sprite.py +133 -56
  147. pyglet/text/__init__.py +33 -13
  148. pyglet/text/caret.py +17 -8
  149. pyglet/text/document.py +7 -5
  150. pyglet/text/formats/attributed.py +3 -2
  151. pyglet/text/formats/html.py +18 -14
  152. pyglet/text/formats/structured.py +62 -31
  153. pyglet/text/layout/base.py +157 -41
  154. pyglet/text/layout/boxes.py +63 -32
  155. pyglet/text/layout/flow.py +81 -61
  156. pyglet/text/layout/incremental.py +42 -23
  157. pyglet/text/layout/scrolling.py +40 -15
  158. pyglet/text/runlist.py +8 -7
  159. pyglet/util.py +2 -2
  160. pyglet/window/__init__.py +75 -50
  161. pyglet/window/{cocoa → apple/cocoa}/__init__.py +72 -9
  162. pyglet/window/{cocoa → apple/cocoa}/pyglet_delegate.py +17 -21
  163. pyglet/window/{cocoa → apple/cocoa}/pyglet_textview.py +41 -40
  164. pyglet/window/{cocoa → apple/cocoa}/pyglet_view.py +37 -35
  165. pyglet/window/{cocoa → apple/cocoa}/pyglet_window.py +10 -15
  166. pyglet/window/apple/ios.py +212 -0
  167. pyglet/window/camera/base.py +41 -11
  168. pyglet/window/camera/camera2d.py +8 -7
  169. pyglet/window/camera/camera3d.py +4 -3
  170. pyglet/window/dialog/__init__.py +3 -4
  171. pyglet/window/emscripten/__init__.py +1 -0
  172. pyglet/window/key.py +1 -0
  173. pyglet/window/wayland/__init__.py +1 -0
  174. pyglet/window/win32/__init__.py +1 -0
  175. pyglet/window/xlib/__init__.py +5 -4
  176. {pyglet-3.0.dev9.dist-info → pyglet-3.0.dev11.dist-info}/METADATA +3 -2
  177. {pyglet-3.0.dev9.dist-info → pyglet-3.0.dev11.dist-info}/RECORD +378 -364
  178. {pyglet-3.0.dev9.dist-info → pyglet-3.0.dev11.dist-info}/WHEEL +1 -1
  179. pyglet/graphics/api/gl1/gl_compat.py +0 -6182
  180. pyglet/gui/frame.py +0 -205
  181. pyglet/libs/darwin/cocoapy/cocoalibs.py +0 -827
  182. pyglet/libs/egl/eglext.py +0 -43
  183. pyglet/libs/linux/glx/glxext_arb.py +0 -1168
  184. pyglet/libs/linux/glx/glxext_mesa.py +0 -8
  185. pyglet/libs/linux/glx/glxext_nv.py +0 -980
  186. pyglet/libs/win32/wglext_arb.py +0 -1606
  187. /pyglet/{graphics/api/gl1 → window/apple}/__init__.py +0 -0
  188. /pyglet/window/{cocoa → apple/cocoa}/systemcursor.py +0 -0
  189. {pyglet-3.0.dev9.dist-info → pyglet-3.0.dev11.dist-info}/licenses/LICENSE +0 -0
pyglet/__init__.py CHANGED
@@ -8,6 +8,8 @@ import os
8
8
  import sys
9
9
  import warnings
10
10
 
11
+ IS_DOC_BUILD = os.environ.get("PYGLET_DOC_BUILD") == "1"
12
+
11
13
  from dataclasses import dataclass, field
12
14
  from typing import TYPE_CHECKING, Literal, Sequence
13
15
 
@@ -18,7 +20,7 @@ if TYPE_CHECKING:
18
20
  from typing import Any, Callable, ItemsView, Sized
19
21
 
20
22
  #: The release version
21
- version = '3.0.dev9'
23
+ version = '3.0.dev11'
22
24
  __version__ = version
23
25
 
24
26
  MIN_PYTHON_VERSION = 3, 10
@@ -264,6 +266,15 @@ class Options:
264
266
 
265
267
  .. versionadded:: 2.0.5"""
266
268
 
269
+ osx_displaylink: bool = True
270
+ """If ``True`` (the default), macOS 14 and later uses ``CADisplayLink`` to schedule window draws.
271
+
272
+ Set this to ``False`` before calling :func:`pyglet.app.run` to retain the clock-driven redraw behavior used by
273
+ earlier pyglet versions. This is useful for applications that need a timer defined frame cadence, such as a
274
+ deterministic capture pipeline or an application that coordinates drawing with an external scheduler.
275
+
276
+ .. versionadded:: 3.0.0"""
277
+
267
278
  dpi_scaling: Literal["platform", "stretch"] = "platform"
268
279
  """For 'HiDPI' displays, Window behavior can differ between operating systems. Defaults to `'platform'`.
269
280
 
@@ -354,7 +365,7 @@ for _option_name, _type_str in options.__annotations__.items():
354
365
 
355
366
 
356
367
  if (__debug__ is False) or getattr(sys, "frozen", False):
357
- options.debug_gl = False
368
+ options.debug_api = False
358
369
 
359
370
 
360
371
  # Call tracing
pyglet/__init__.pyi CHANGED
@@ -24,6 +24,8 @@ from . import sprite as sprite
24
24
  from . import text as text
25
25
  from . import window as window
26
26
 
27
+ IS_DOC_BUILD: bool
28
+
27
29
  version: str
28
30
  MIN_PYTHON_VERSION: tuple[int, int]
29
31
  MIN_PYTHON_VERSION_STR: str
@@ -68,6 +70,7 @@ class Options:
68
70
  win32_disable_xinput: bool
69
71
  com_mta: bool
70
72
  osx_alt_loop: bool
73
+ osx_displaylink: bool
71
74
  dpi_scaling: Literal["platform", "stretch"]
72
75
  shader_bind_management: bool
73
76
  wayland: bool
pyglet/app/__init__.py CHANGED
@@ -31,17 +31,17 @@ default policy is to wait until all windows are closed)::
31
31
 
32
32
  from __future__ import annotations
33
33
 
34
- import platform
35
34
  import sys
35
+ import signal
36
+ import platform
36
37
  import weakref
37
38
 
38
39
  import pyglet
39
40
  from pyglet import compat_platform
40
41
  from pyglet.app.base import EventLoop
41
42
 
42
- _is_pyglet_doc_run = hasattr(sys, "is_pyglet_doc_run") and sys.is_pyglet_doc_run
43
43
 
44
- if _is_pyglet_doc_run:
44
+ if pyglet.IS_DOC_BUILD:
45
45
  from pyglet.app.base import PlatformEventLoop
46
46
  else:
47
47
  if compat_platform == 'darwin':
@@ -51,6 +51,9 @@ else:
51
51
  # Use alternate loop only if forced, or using an M1 chip.
52
52
  if (platform.machine() == 'arm64' and "M1" in get_chip_model()) or pyglet.options.osx_alt_loop:
53
53
  from pyglet.app.cocoa import CocoaAlternateEventLoop as EventLoop
54
+ elif compat_platform == 'ios':
55
+ from pyglet.app.ios import IOSEventLoop as EventLoop
56
+ from pyglet.app.ios import IOSPlatformEventLoop as PlatformEventLoop
54
57
  elif compat_platform in ('win32', 'cygwin'):
55
58
  from pyglet.app.win32 import Win32EventLoop as PlatformEventLoop
56
59
  elif compat_platform == 'linux':
@@ -79,7 +82,11 @@ def run(interval: float | None = 1 / 60) -> None:
79
82
 
80
83
  pyglet.app.event_loop.run(interval)
81
84
 
85
+ ..note: This convenience function also adds a SIGINT signal handler
86
+ to automatically call ``pyglet.app.exit`` when this signal
87
+ is detected.
82
88
  """
89
+ signal.signal(signal.SIGINT, lambda _s, _f: event_loop.exit())
83
90
  event_loop.run(interval)
84
91
 
85
92
 
pyglet/app/base.py CHANGED
@@ -7,13 +7,12 @@ from abc import ABC, abstractmethod
7
7
  from queue import Queue
8
8
  from typing import TYPE_CHECKING, Any, Callable
9
9
 
10
- from pyglet import app, clock, event
10
+ from pyglet import app, clock, event, IS_DOC_BUILD
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  from pyglet.event import EventDispatcher
14
14
  from pyglet.window import BaseWindow
15
15
 
16
- _is_pyglet_doc_run = hasattr(sys, "is_pyglet_doc_run") and sys.is_pyglet_doc_run
17
16
 
18
17
 
19
18
  class WindowDrawSource(ABC):
@@ -406,7 +405,7 @@ class EventLoop(event.EventDispatcher):
406
405
  if len(app.windows) == 0:
407
406
  self.exit()
408
407
 
409
- if _is_pyglet_doc_run:
408
+ if IS_DOC_BUILD:
410
409
  # Events
411
410
 
412
411
  def on_window_close(self, window: BaseWindow) -> None:
pyglet/app/cocoa.py CHANGED
@@ -2,13 +2,16 @@ from __future__ import annotations
2
2
 
3
3
  import signal
4
4
  import time
5
+ from typing import TYPE_CHECKING
5
6
 
7
+ import pyglet
6
8
  from pyglet import app
7
- from pyglet.app.base import EventLoop, PlatformEventLoop
8
- from pyglet.libs.darwin import cocoapy, AutoReleasePool, ObjCSubclass, PyObjectEncoding, ObjCInstance, send_super, \
9
- ObjCClass, get_selector, set_realtime_thread_policy
9
+ from pyglet.app.base import EventLoop, PlatformEventLoop, WindowDrawSource
10
+ from pyglet.libs.darwin import cocoapy, AutoReleasePool, PyObjectEncoding, ObjCInstance, send_super, \
11
+ ObjCClass, get_selector, objc_method, set_realtime_thread_policy
10
12
 
11
13
  NSApplication = cocoapy.ObjCClass('NSApplication')
14
+ NSObject = cocoapy.ObjCClass('NSObject')
12
15
  NSMenu = cocoapy.ObjCClass('NSMenu')
13
16
  NSMenuItem = cocoapy.ObjCClass('NSMenuItem')
14
17
  NSDate = cocoapy.ObjCClass('NSDate')
@@ -17,6 +20,9 @@ NSUserDefaults = cocoapy.ObjCClass('NSUserDefaults')
17
20
  NSTimer = cocoapy.ObjCClass('NSTimer')
18
21
  NSRunningApplication = cocoapy.ObjCClass('NSRunningApplication')
19
22
 
23
+ if TYPE_CHECKING:
24
+ from pyglet.window.apple.cocoa import CocoaWindow
25
+
20
26
  def add_menu_item(menu, title, action, key):
21
27
  with AutoReleasePool():
22
28
  title = cocoapy.CFSTR(title)
@@ -54,25 +60,24 @@ def create_menu():
54
60
  appMenuItem.release()
55
61
 
56
62
 
57
- class _AppDelegate_Implementation:
58
- _AppDelegate = ObjCSubclass('NSObject', '_AppDelegate')
63
+ class _AppDelegate(NSObject):
59
64
 
60
- @_AppDelegate.method(b'@' + PyObjectEncoding)
65
+ @objc_method(b'@' + PyObjectEncoding)
61
66
  def init(self, pyglet_loop):
62
67
  objc = ObjCInstance(send_super(self, 'init'))
63
68
  self._pyglet_loop = pyglet_loop
64
69
  return objc # objc is self
65
70
 
66
- @_AppDelegate.method('v')
71
+ @objc_method('v')
67
72
  def updatePyglet_(self):
68
73
  self._pyglet_loop.nsapp_step()
69
74
 
70
- @_AppDelegate.method('v@')
75
+ @objc_method('v@')
71
76
  def applicationWillTerminate_(self, notification):
72
77
  self._pyglet_loop.is_running = False
73
78
  self._pyglet_loop.has_exit = True
74
79
 
75
- @_AppDelegate.method('v@')
80
+ @objc_method('v@')
76
81
  def applicationDidFinishLaunching_(self, notification):
77
82
  self._pyglet_loop._finished_launching = True
78
83
 
@@ -96,38 +101,32 @@ class _AppDelegate_Implementation:
96
101
 
97
102
  NSApp.activateIgnoringOtherApps_(True)
98
103
 
99
- @_AppDelegate.method('v@')
104
+ @objc_method('v@')
100
105
  def applicationWillFinishLaunching_(self, notification):
101
106
  pass
102
107
 
103
- @_AppDelegate.method('B')
108
+ @objc_method('B')
104
109
  def applicationSupportsSecureRestorableState_(self):
105
110
  return True
106
111
 
107
- _AppDelegate = ObjCClass('_AppDelegate') # the actual class
108
-
109
112
  class CocoaAlternateEventLoop(EventLoop):
110
113
  """This is an alternate loop developed mainly for ARM64 variants of macOS.
111
- nextEventMatchingMask_untilDate_inMode_dequeue_ is very broken with ctypes calls. Events eventually stop
112
- working properly after X returns. This event loop differs in that it uses the built-in NSApplication event
114
+
115
+ nextEventMatchingMask_untilDate_inMode_dequeue_ is very broken with ctypes calls on M1.
116
+
117
+ Events eventually stop working properly after X returns. This event loop
118
+ differs in that it uses the built-in NSApplication event
113
119
  loop. We tie our schedule into it via timer.
114
120
  """
115
121
  def __init__(self):
116
122
  super().__init__()
117
123
  self.platform_event_loop = None
118
124
 
119
- def run(self, interval: float | None = 1/60):
120
- if interval is None:
121
- pass # do not schedule redraws
122
- elif not interval:
123
- self.clock.schedule(self._redraw_windows)
124
- else:
125
- self.clock.schedule_interval(self._redraw_windows, interval)
126
-
125
+ def run(self, interval: float | None = 1/60) -> None:
127
126
  self.has_exit = False
128
127
 
129
- from pyglet.window import Window
130
- Window._enable_event_queue = False
128
+ from pyglet.window import Window # noqa: PLC0415
129
+ Window._enable_event_queue = False # noqa: SLF001
131
130
 
132
131
  # Dispatch pending events
133
132
  for window in app.windows:
@@ -135,13 +134,18 @@ class CocoaAlternateEventLoop(EventLoop):
135
134
  window.dispatch_pending_events()
136
135
 
137
136
  self.platform_event_loop = app.platform_event_loop
137
+ self._schedule_window_draw(interval)
138
138
 
139
139
  self.dispatch_event('on_enter')
140
140
  self.is_running = True
141
141
 
142
- self.platform_event_loop.nsapp_start(interval or 0)
142
+ try:
143
+ self.platform_event_loop.nsapp_start(interval or 0)
144
+ finally:
145
+ self._unschedule_window_draw()
146
+ self._raise_window_draw_exception()
143
147
 
144
- def exit(self):
148
+ def exit(self) -> None:
145
149
  """Safely exit the event loop at the end of the current iteration.
146
150
 
147
151
  This method is a thread-safe equivalent for setting
@@ -159,12 +163,52 @@ class CocoaAlternateEventLoop(EventLoop):
159
163
  self.platform_event_loop.nsapp_stop()
160
164
 
161
165
 
166
+ class CocoaWindowDrawSource(WindowDrawSource):
167
+ """Display driven drawing backed by one CADisplayLink."""
168
+
169
+ def __init__(self) -> None:
170
+ self._active = False
171
+ self._interval = None
172
+
173
+ def start(self, interval: float) -> bool:
174
+ started_windows = []
175
+ for window in app.windows:
176
+ if not window._start_display_link(interval): # noqa: SLF001
177
+ for started_window in started_windows:
178
+ started_window._stop_display_link() # noqa: SLF001
179
+ return False
180
+ started_windows.append(window)
181
+
182
+ self._interval = interval
183
+ self._active = True
184
+ return True
185
+
186
+ def stop(self) -> None:
187
+ if not self._active:
188
+ return
189
+
190
+ self._active = False
191
+ self._interval = None
192
+ for window in app.windows:
193
+ window._stop_display_link() # noqa: SLF001
194
+
195
+ def add_window(self, window: CocoaWindow) -> bool:
196
+ return not self._active or window._start_display_link(self._interval) # noqa: SLF001
197
+
198
+ def remove_window(self, window: CocoaWindow) -> None:
199
+ window._stop_display_link() # noqa: SLF001
200
+
201
+
162
202
  class CocoaPlatformEventLoop(PlatformEventLoop):
163
203
 
164
204
  def __init__(self):
165
205
  super().__init__()
166
206
  set_realtime_thread_policy()
167
207
  self._timer = None
208
+ nsview = ObjCClass('NSView')
209
+ self._display_link_supported = bool(
210
+ nsview.instancesRespondToSelector_(get_selector('displayLinkWithTarget:selector:'))
211
+ )
168
212
 
169
213
  with AutoReleasePool():
170
214
  # Prepare the default application.
@@ -198,7 +242,7 @@ class CocoaPlatformEventLoop(PlatformEventLoop):
198
242
  signal.signal(signal.SIGINT, term_received)
199
243
  signal.signal(signal.SIGTERM, term_received)
200
244
 
201
- def start(self):
245
+ def start(self) -> None:
202
246
  with AutoReleasePool():
203
247
  if not self.NSApp.isRunning() and not self._finished_launching:
204
248
  # finishLaunching should be called only once. However isRunning will not
@@ -207,9 +251,14 @@ class CocoaPlatformEventLoop(PlatformEventLoop):
207
251
  self.NSApp.activateIgnoringOtherApps_(True)
208
252
  self._finished_launching = True
209
253
 
210
- def nsapp_start(self, interval):
211
- """Used only for CocoaAlternateEventLoop"""
212
- from pyglet.app import event_loop
254
+ def create_window_draw_source(self, _event_loop: EventLoop) -> WindowDrawSource | None:
255
+ if pyglet.options.osx_displaylink and self._display_link_supported:
256
+ return CocoaWindowDrawSource()
257
+ return None
258
+
259
+ def nsapp_start(self, interval: float) -> None:
260
+ """Used only for CocoaAlternateEventLoop."""
261
+ from pyglet.app import event_loop # noqa: PLC0415
213
262
  self._event_loop = event_loop
214
263
 
215
264
  assert self._timer is None
@@ -219,26 +268,26 @@ class CocoaPlatformEventLoop(PlatformEventLoop):
219
268
  self.appdelegate,
220
269
  get_selector('updatePyglet:'),
221
270
  False,
222
- True
271
+ True,
223
272
  )
224
273
 
225
274
  self.NSApp.run()
226
275
 
227
- def nsapp_step(self):
228
- """Used only for CocoaAlternateEventLoop"""
276
+ def nsapp_step(self) -> None:
277
+ """Used only for CocoaAlternateEventLoop."""
229
278
  self._event_loop.idle()
230
279
  with AutoReleasePool():
231
280
  self.dispatch_posted_events()
232
281
 
233
- def nsapp_stop(self):
234
- """Used only for CocoaAlternateEventLoop"""
282
+ def nsapp_stop(self) -> None:
283
+ """Used only for CocoaAlternateEventLoop."""
235
284
  self.NSApp.stop_(None)
236
285
 
237
286
  if self._timer:
238
287
  self._timer.invalidate()
239
288
  self._timer = None
240
289
 
241
- def step(self, timeout=None):
290
+ def step(self, timeout: float | None=None) -> bool: # noqa: ANN201
242
291
  with AutoReleasePool():
243
292
  self.dispatch_posted_events()
244
293
 
@@ -274,10 +323,10 @@ class CocoaPlatformEventLoop(PlatformEventLoop):
274
323
 
275
324
  return did_time_out
276
325
 
277
- def stop(self):
326
+ def stop(self) -> None:
278
327
  pass
279
328
 
280
- def notify(self):
329
+ def notify(self) -> None:
281
330
  with AutoReleasePool():
282
331
  notifyEvent = NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_(
283
332
  cocoapy.NSApplicationDefined, # type
pyglet/app/ios.py ADDED
@@ -0,0 +1,168 @@
1
+ """UIKit app loop integration on iOS."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+
6
+ from pyglet import app
7
+ from pyglet.app.base import EventLoop, PlatformEventLoop, WindowDrawSource
8
+ from pyglet.libs.darwin import get_display_link_dt
9
+ from pyglet.libs.darwin.cocoapy import (
10
+ CAFrameRateRange,
11
+ NSRunLoopCommonModes,
12
+ ObjCClass,
13
+ ObjCInstance,
14
+ PyObjectEncoding,
15
+ get_selector,
16
+ objc_method,
17
+ send_super,
18
+ )
19
+
20
+ _XCTEST_ENVIRONMENT = (
21
+ "XCTestBundlePath",
22
+ "XCTestConfigurationFilePath",
23
+ "XCTestSessionIdentifier",
24
+ )
25
+
26
+ def _is_xctest() -> bool:
27
+ return any(name in os.environ for name in _XCTEST_ENVIRONMENT)
28
+
29
+ NSObject = ObjCClass('NSObject')
30
+ NSRunLoop = ObjCClass('NSRunLoop')
31
+ CADisplayLink = ObjCClass('CADisplayLink')
32
+
33
+
34
+ class _PygletIOSDisplayLinkTarget(NSObject):
35
+
36
+ @objc_method(b'@' + PyObjectEncoding)
37
+ def initWithDrawSource_(self, draw_source: IOSDisplayLinkDrawSource) -> ObjCInstance | None:
38
+ self = ObjCInstance(send_super(self, 'init'))
39
+ if not self:
40
+ return None
41
+ self._draw_source = draw_source
42
+ return self
43
+
44
+ @objc_method('v@')
45
+ def displayLinkFired_(self, display_link: ObjCInstance) -> None:
46
+ self._draw_source.tick(display_link)
47
+
48
+
49
+ class IOSDisplayLinkDrawSource(WindowDrawSource):
50
+ """One UIKit display link that paces all pyglet window drawing."""
51
+
52
+ def __init__(self, event_loop: IOSEventLoop) -> None:
53
+ self._event_loop = event_loop
54
+ self._display_link = None
55
+ self._target = None
56
+ self._last_timestamp: float | None = None
57
+
58
+ def start(self, interval: float) -> bool:
59
+ if self._display_link is not None:
60
+ return True
61
+
62
+ self._target = _PygletIOSDisplayLinkTarget.alloc().initWithDrawSource_(self)
63
+ display_link = CADisplayLink.displayLinkWithTarget_selector_(
64
+ self._target,
65
+ get_selector('displayLinkFired:'),
66
+ )
67
+ if display_link is None:
68
+ self._target.release()
69
+ self._target = None
70
+ return False
71
+
72
+ if interval:
73
+ frame_rate = 1 / interval
74
+ if CADisplayLink.instancesRespondToSelector_(get_selector('setPreferredFrameRateRange:')):
75
+ display_link.setPreferredFrameRateRange_(
76
+ CAFrameRateRange(frame_rate, frame_rate, frame_rate),
77
+ )
78
+ else:
79
+ display_link.setPreferredFramesPerSecond_(round(frame_rate))
80
+
81
+ self._display_link = display_link
82
+ self._last_timestamp = None
83
+ display_link.addToRunLoop_forMode_(NSRunLoop.mainRunLoop(), NSRunLoopCommonModes)
84
+ return True
85
+
86
+ def stop(self) -> None:
87
+ if self._display_link is not None:
88
+ self._display_link.invalidate()
89
+ self._display_link = None
90
+ if self._target is not None:
91
+ self._target.release()
92
+ self._target = None
93
+ self._last_timestamp = None
94
+
95
+ def tick(self, display_link: ObjCInstance) -> None:
96
+ if self._display_link is None or not self._event_loop.is_running:
97
+ return
98
+ self._last_timestamp, dt = get_display_link_dt(display_link, self._last_timestamp)
99
+ self._event_loop._tick_app(dt) # noqa: SLF001
100
+
101
+ class IOSPlatformEventLoop(PlatformEventLoop):
102
+ """Schedules pyglet work from UIKit's already-running main run loop."""
103
+
104
+ def __init__(self) -> None:
105
+ super().__init__()
106
+
107
+ def notify(self) -> None:
108
+ return None
109
+
110
+ def start(self) -> None:
111
+ self._is_running.set()
112
+
113
+ def step(self, timeout: float | None = None) -> None:
114
+ self.dispatch_posted_events()
115
+
116
+ def stop(self) -> None:
117
+ self._is_running.clear()
118
+
119
+ def create_window_draw_source(self, event_loop: EventLoop) -> WindowDrawSource:
120
+ return IOSDisplayLinkDrawSource(event_loop)
121
+
122
+
123
+ class IOSEventLoop(EventLoop):
124
+ """Event loop similar to what we did for alt macos loop.."""
125
+
126
+ def run(self, interval: float | None = 1 / 60) -> None:
127
+ # Check for XCode environment as it owns the app lifetime and finalizes its embedded
128
+ # interpreter after the test returns. It must not receive native
129
+ # callbacks from a pyglet timer after that point.
130
+ if _is_xctest():
131
+ # Tick app once during test to ensure at least one draw.
132
+ self._tick_app()
133
+ self.has_exit = True
134
+ return
135
+
136
+ self.has_exit = False
137
+ from pyglet.window import Window # noqa: PLC0415
138
+ Window._enable_event_queue = False # noqa: SLF001
139
+ for window in app.windows:
140
+ window.dispatch_pending_events()
141
+
142
+ self._interval = interval
143
+ self.dispatch_event('on_enter')
144
+ self.is_running = True
145
+ app.platform_event_loop.start()
146
+ self._schedule_display_link(interval)
147
+
148
+ def _schedule_display_link(self, interval: float | None) -> None:
149
+ self._window_draw_source = app.platform_event_loop.create_window_draw_source(self)
150
+ if not self._window_draw_source.start(1 / 60 if interval is None else interval):
151
+ raise RuntimeError("Unable to create the iOS CADisplayLink.")
152
+
153
+ def _tick_app(self, draw_dt: float | None = None) -> None:
154
+ app.platform_event_loop.dispatch_posted_events()
155
+ clock_dt = self.clock.update_time()
156
+ self.clock.call_scheduled_functions(clock_dt)
157
+ if self._interval is not None:
158
+ for window in app.windows:
159
+ self._dispatch_platform_draw(window, clock_dt if draw_dt is None else draw_dt)
160
+
161
+ def exit(self) -> None:
162
+ if not self.is_running:
163
+ return
164
+ self.has_exit = True
165
+ self.is_running = False
166
+ self._unschedule_window_draw()
167
+ app.platform_event_loop.stop()
168
+ self.dispatch_event('on_exit')
pyglet/config/__init__.py CHANGED
@@ -4,6 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  from typing import TYPE_CHECKING, NamedTuple
6
6
 
7
+ import pyglet
8
+
7
9
  from pyglet.enums import GraphicsAPI
8
10
 
9
11
  from .base import SurfaceConfig, UserConfig # noqa: TC001
@@ -23,7 +25,7 @@ class Config:
23
25
 
24
26
  __slots__ = 'gl2', 'gles2', 'gles3', 'opengl', 'vulkan', 'webgl'
25
27
 
26
- def __init__(self):
28
+ def __init__(self) -> None: # noqa: D107
27
29
  self.opengl: OpenGLUserConfig = OpenGLUserConfig(major_version=3, minor_version=3, api=GraphicsAPI.OPENGL)
28
30
  self.gl2: OpenGLUserConfig = OpenGLUserConfig(major_version=2, minor_version=0, api=GraphicsAPI.OPENGL_2)
29
31
  self.gles2: OpenGLUserConfig = OpenGLUserConfig(major_version=2, minor_version=0, api=GraphicsAPI.OPENGL_ES_2)
@@ -33,9 +35,15 @@ class Config:
33
35
 
34
36
 
35
37
  def match_surface_config(config: UserConfig, surface: Window) -> SurfaceConfig | None:
36
- if isinstance(config, (OpenGLUserConfig, WebGLUserConfig)):
38
+ if isinstance(config, OpenGLUserConfig):
37
39
  from pyglet.config.gl import get_surface_config # noqa: PLC0415
38
40
  return get_surface_config(config, surface)
39
41
 
42
+ if isinstance(config, WebGLUserConfig):
43
+ if pyglet.compat_platform == 'emscripten':
44
+ from pyglet.config.gl.webgl import match # noqa: PLC0415
45
+ return match(config, surface)
46
+ return None
47
+
40
48
  msg = f"Matching for '{config.__class__}' is not yet implemented."
41
49
  raise NotImplementedError(msg)
@@ -1,12 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, cast
4
5
 
5
6
  import pyglet
6
7
 
7
8
  from pyglet.enums import GraphicsAPI
8
9
  from pyglet.config.base import UserConfig, SurfaceConfig
9
10
 
11
+ if TYPE_CHECKING:
12
+ from pyglet.window.apple.cocoa import CocoaWindow
13
+ from pyglet.window.apple.ios import IOSWindow
14
+ from pyglet.window.headless import HeadlessWindow
15
+ from pyglet.window.wayland import WaylandWindow
16
+ from pyglet.window.win32 import Win32Window
17
+ from pyglet.window.xlib import XlibWindow
18
+
10
19
 
11
20
  class GLSurfaceConfig(SurfaceConfig):
12
21
  config: OpenGLUserConfig
@@ -98,31 +107,31 @@ class WebGLUserConfig(UserConfig):
98
107
  return False
99
108
 
100
109
 
101
- def get_surface_config(user_config: UserConfig, surface: pyglet.window.Window) -> SurfaceConfig | None:
110
+ def get_surface_config(user_config: OpenGLUserConfig, surface: pyglet.window.Window) -> SurfaceConfig | None:
102
111
 
103
112
  if pyglet.options.headless or pyglet.options.wayland:
104
- from pyglet.config.gl.egl import match
113
+ from pyglet.config.gl.egl import match as match_egl # noqa: PLC0415
105
114
 
106
- return match(user_config, surface)
115
+ return match_egl(user_config, cast("HeadlessWindow | WaylandWindow", surface))
107
116
 
108
117
  if pyglet.compat_platform == "win32":
109
- from pyglet.config.gl.windows import match # noqa: PLC0415
118
+ from pyglet.config.gl.windows import match as match_windows # noqa: PLC0415
110
119
 
111
- return match(user_config, surface)
120
+ return match_windows(user_config, cast("Win32Window", surface))
112
121
 
113
122
  if pyglet.compat_platform.startswith("linux"):
114
- from pyglet.config.gl.x11 import match # noqa: PLC0415
123
+ from pyglet.config.gl.x11 import match as match_x11 # noqa: PLC0415
115
124
 
116
- return match(user_config, surface)
125
+ return match_x11(user_config, cast("XlibWindow", surface))
117
126
 
118
127
  if pyglet.compat_platform == "darwin":
119
- from pyglet.config.gl.macos import match # noqa: PLC0415
128
+ from pyglet.config.gl.macos import match as match_macos # noqa: PLC0415
120
129
 
121
- return match(user_config, surface)
130
+ return match_macos(user_config, cast("CocoaWindow", surface))
122
131
 
123
- if pyglet.compat_platform == "emscripten":
124
- from pyglet.config.gl.webgl import match # noqa: PLC0415
132
+ if pyglet.compat_platform == "ios":
133
+ from pyglet.config.gl.ios import match as match_ios # noqa: PLC0415
125
134
 
126
- return match(user_config, surface)
135
+ return match_ios(user_config, cast("IOSWindow", surface))
127
136
 
128
137
  return None