pyglet 2.1.5__py3-none-any.whl → 2.1.8__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 (71) hide show
  1. pyglet/__init__.py +27 -42
  2. pyglet/app/base.py +2 -2
  3. pyglet/clock.py +1 -1
  4. pyglet/display/base.py +31 -21
  5. pyglet/display/cocoa.py +25 -1
  6. pyglet/display/headless.py +1 -1
  7. pyglet/display/win32.py +134 -18
  8. pyglet/display/xlib.py +285 -70
  9. pyglet/event.py +17 -1
  10. pyglet/experimental/README.md +1 -1
  11. pyglet/experimental/jobs.py +1 -1
  12. pyglet/experimental/multitexture_sprite.py +2 -2
  13. pyglet/font/__init__.py +1 -1
  14. pyglet/font/base.py +8 -5
  15. pyglet/font/dwrite/__init__.py +13 -8
  16. pyglet/font/dwrite/dwrite_lib.py +1 -1
  17. pyglet/font/user.py +1 -1
  18. pyglet/gl/base.py +8 -4
  19. pyglet/gl/cocoa.py +4 -0
  20. pyglet/gl/gl.py +4 -3
  21. pyglet/gl/gl.pyi +2320 -0
  22. pyglet/gl/gl_compat.py +7 -18
  23. pyglet/gl/gl_compat.pyi +3097 -0
  24. pyglet/gl/xlib.py +24 -0
  25. pyglet/graphics/shader.py +34 -20
  26. pyglet/graphics/vertexbuffer.py +1 -1
  27. pyglet/gui/frame.py +2 -2
  28. pyglet/gui/widgets.py +1 -1
  29. pyglet/image/__init__.py +3 -3
  30. pyglet/image/buffer.py +3 -3
  31. pyglet/input/base.py +8 -8
  32. pyglet/input/linux/evdev.py +1 -1
  33. pyglet/libs/darwin/cocoapy/cocoalibs.py +3 -1
  34. pyglet/libs/win32/__init__.py +12 -0
  35. pyglet/libs/win32/constants.py +4 -0
  36. pyglet/libs/win32/types.py +97 -0
  37. pyglet/libs/x11/xrandr.py +166 -0
  38. pyglet/libs/x11/xrender.py +43 -0
  39. pyglet/libs/x11/xsync.py +43 -0
  40. pyglet/math.py +40 -49
  41. pyglet/media/buffered_logger.py +1 -1
  42. pyglet/media/codecs/ffmpeg.py +18 -34
  43. pyglet/media/codecs/gstreamer.py +3 -3
  44. pyglet/media/codecs/pyogg.py +1 -1
  45. pyglet/media/codecs/wave.py +6 -0
  46. pyglet/media/codecs/wmf.py +33 -7
  47. pyglet/media/devices/win32.py +1 -1
  48. pyglet/media/drivers/base.py +1 -1
  49. pyglet/media/drivers/directsound/interface.py +4 -0
  50. pyglet/media/drivers/listener.py +2 -2
  51. pyglet/media/drivers/xaudio2/interface.py +6 -2
  52. pyglet/media/drivers/xaudio2/lib_xaudio2.py +1 -1
  53. pyglet/media/instrumentation.py +2 -2
  54. pyglet/media/player.py +2 -2
  55. pyglet/media/player_worker_thread.py +1 -1
  56. pyglet/media/synthesis.py +1 -1
  57. pyglet/model/codecs/gltf.py +1 -1
  58. pyglet/shapes.py +25 -24
  59. pyglet/sprite.py +1 -1
  60. pyglet/text/caret.py +44 -5
  61. pyglet/text/layout/base.py +3 -3
  62. pyglet/util.py +1 -1
  63. pyglet/window/__init__.py +54 -14
  64. pyglet/window/cocoa/__init__.py +27 -0
  65. pyglet/window/mouse.py +11 -1
  66. pyglet/window/win32/__init__.py +40 -14
  67. pyglet/window/xlib/__init__.py +21 -7
  68. {pyglet-2.1.5.dist-info → pyglet-2.1.8.dist-info}/METADATA +1 -1
  69. {pyglet-2.1.5.dist-info → pyglet-2.1.8.dist-info}/RECORD +71 -67
  70. {pyglet-2.1.5.dist-info → pyglet-2.1.8.dist-info}/LICENSE +0 -0
  71. {pyglet-2.1.5.dist-info → pyglet-2.1.8.dist-info}/WHEEL +0 -0
pyglet/__init__.py CHANGED
@@ -6,16 +6,16 @@ from __future__ import annotations
6
6
 
7
7
  import os
8
8
  import sys
9
- from collections.abc import ItemsView, Sequence
9
+ import warnings
10
+ from collections.abc import Sequence
10
11
  from dataclasses import dataclass
11
12
  from typing import TYPE_CHECKING, Literal
12
-
13
13
  if TYPE_CHECKING:
14
14
  from types import FrameType
15
15
  from typing import Any, Callable, ItemsView, Sized
16
16
 
17
17
  #: The release version
18
- version = '2.1.5'
18
+ version = '2.1.8'
19
19
  __version__ = version
20
20
 
21
21
  MIN_PYTHON_VERSION = 3, 8
@@ -30,20 +30,16 @@ compat_platform = sys.platform
30
30
  if "bsd" in compat_platform:
31
31
  compat_platform = "linux-compat"
32
32
 
33
- _enable_optimisations = not __debug__
34
- if getattr(sys, "frozen", None):
35
- _enable_optimisations = True
36
-
37
-
38
- _SPECIAL_OPTION_VALIDATORS = {
39
- "audio": lambda x: isinstance(x, Sequence),
40
- "vsync": lambda x: x is None or isinstance(x, bool),
41
- }
33
+ if compat_platform == "cygwin":
34
+ # This hack pretends that the posix-like ctypes provides windows
35
+ # functionality. COM does not work with this hack, so there is no
36
+ # DirectSound support.
37
+ import ctypes
42
38
 
43
- _OPTION_TYPE_VALIDATORS = {
44
- "bool": lambda x: isinstance(x, bool),
45
- "int": lambda x: isinstance(x, int),
46
- }
39
+ ctypes.windll = ctypes.cdll
40
+ ctypes.oledll = ctypes.cdll
41
+ ctypes.WINFUNCTYPE = ctypes.CFUNCTYPE
42
+ ctypes.HRESULT = ctypes.c_long
47
43
 
48
44
 
49
45
  @dataclass
@@ -115,7 +111,7 @@ class Options:
115
111
  that utilize COM calls. Only applies to the Windows platform."""
116
112
 
117
113
  debug_win32: bool = False
118
- """If ``True``, prints error messages related to Windows library calls. Usually get's information from
114
+ """If ``True``, prints error messages related to Windows library calls. Usually gets information from
119
115
  ``Kernel32.GetLastError``. This information is output to a file called ``debug_win32.log``."""
120
116
 
121
117
  debug_input: bool = False
@@ -311,42 +307,31 @@ class Options:
311
307
  return self.__dict__[item]
312
308
 
313
309
  def __setitem__(self, key: str, value: Any) -> None:
314
- assert key in self.__annotations__, f"Invalid option name: '{key}'"
315
- assert (_SPECIAL_OPTION_VALIDATORS.get(key) or _OPTION_TYPE_VALIDATORS[self.__annotations__[key]])(value), \
316
- f"Invalid type: '{type(value)}' for '{key}'"
317
310
  self.__dict__[key] = value
318
311
 
319
312
 
320
313
  #: Instance of :py:class:`~pyglet.Options` used to set runtime options.
321
314
  options: Options = Options()
322
315
 
323
- _OPTION_TYPE_REMAPS = {
324
- "audio": "sequence",
325
- "vsync": "bool",
326
- }
327
316
 
328
- for _key, _type in options.__annotations__.items():
317
+ for _option_name, _type_str in options.__annotations__.items():
329
318
  """Check Environment Variables for pyglet options"""
330
- if _value := os.environ.get(f"PYGLET_{_key.upper()}"):
331
- _type = _OPTION_TYPE_REMAPS.get(_key, _type)
332
- if _type == 'sequence':
333
- options[_key] = _value.split(",")
334
- elif _type == 'bool':
335
- options[_key] = _value in ("true", "TRUE", "True", "1")
336
- elif _type == 'int':
337
- options[_key] = int(_value)
319
+ if _value := os.environ.get(f"PYGLET_{_option_name.upper()}"):
320
+ if 'Sequence' in _type_str:
321
+ setattr(options, _option_name, _value.split(","))
322
+ elif 'bool' in _type_str:
323
+ setattr(options, _option_name, _value in ("true", "TRUE", "True", "1"))
324
+ elif 'int' in _type_str:
325
+ setattr(options, _option_name, int(_value))
326
+ elif 'Literal' in _type_str and _value in _type_str:
327
+ setattr(options, _option_name, _value)
328
+ else:
329
+ warnings.warn(f"Invalid value '{_value}' for {_option_name}. Expecting {_type_str}")
338
330
 
339
331
 
340
- if compat_platform == "cygwin":
341
- # This hack pretends that the posix-like ctypes provides windows
342
- # functionality. COM does not work with this hack, so there is no
343
- # DirectSound support.
344
- import ctypes
332
+ if (__debug__ is False) or getattr(sys, "frozen", False):
333
+ options.debug_gl = False
345
334
 
346
- ctypes.windll = ctypes.cdll
347
- ctypes.oledll = ctypes.cdll
348
- ctypes.WINFUNCTYPE = ctypes.CFUNCTYPE
349
- ctypes.HRESULT = ctypes.c_long
350
335
 
351
336
  # Call tracing
352
337
  # ------------
pyglet/app/base.py CHANGED
@@ -199,7 +199,7 @@ class EventLoop(event.EventDispatcher):
199
199
  if self._interval is None:
200
200
  self._redraw_windows(dt)
201
201
 
202
- # Update timout
202
+ # Update timeout
203
203
  timeout = self.clock.get_sleep_time(True)
204
204
  app.platform_event_loop.set_timer(self._blocking_timer, timeout)
205
205
 
@@ -231,7 +231,7 @@ class EventLoop(event.EventDispatcher):
231
231
  dt = self.clock.update_time()
232
232
  self.clock.call_scheduled_functions(dt)
233
233
 
234
- # Update timout
234
+ # Update timeout
235
235
  return self.clock.get_sleep_time(True)
236
236
 
237
237
  @property
pyglet/clock.py CHANGED
@@ -303,7 +303,7 @@ class Clock:
303
303
 
304
304
  The result is the average of a sliding window of the last "n" updates,
305
305
  where "n" is some number designed to cover approximately 1 second.
306
- This is the clock frequence, **not** the Window redraw rate (fps).
306
+ This is the clock frequency, **not** the Window redraw rate (fps).
307
307
  """
308
308
  if not self.cumulative_time:
309
309
  return 0
pyglet/display/base.py CHANGED
@@ -1,11 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ import abc
4
+ from typing import TYPE_CHECKING, Literal
4
5
 
5
- from pyglet import gl
6
- from pyglet import app
7
- from pyglet import window
8
- from pyglet import display
6
+ from pyglet import app, display, gl, window
9
7
 
10
8
  if TYPE_CHECKING:
11
9
  from pyglet.gl import Config
@@ -55,9 +53,7 @@ class Display:
55
53
  raise NotImplementedError('abstract')
56
54
 
57
55
  def get_default_screen(self) -> Screen:
58
- """Get the default (primary) screen as specified by the user's operating system
59
- preferences.
60
- """
56
+ """Get the default (primary) screen as specified by the user's operating system preferences."""
61
57
  screens = self.get_screens()
62
58
  for screen in screens:
63
59
  if screen.x == 0 and screen.y == 0:
@@ -71,7 +67,7 @@ class Display:
71
67
  return [win for win in app.windows if win.display is self]
72
68
 
73
69
 
74
- class Screen:
70
+ class Screen(abc.ABC):
75
71
  """A virtual monitor that supports fullscreen windows.
76
72
 
77
73
  Screens typically map onto a physical display such as a
@@ -84,12 +80,12 @@ class Screen:
84
80
  give the global location of the top-left corner of the screen. This is
85
81
  useful for determining if screens are arranged above or next to one
86
82
  another.
87
-
83
+
88
84
  Use :func:`~Display.get_screens` or :func:`~Display.get_default_screen`
89
85
  to obtain an instance of this class.
90
86
  """
91
87
 
92
- def __init__(self, display: Display, x: int, y: int, width: int, height: int):
88
+ def __init__(self, display: Display, x: int, y: int, width: int, height: int) -> None:
93
89
  self.display = display
94
90
  """Display this screen belongs to."""
95
91
  self.x = x
@@ -208,29 +204,43 @@ class Screen:
208
204
  def set_mode(self, mode: ScreenMode) -> None:
209
205
  """Set the display mode for this screen.
210
206
 
211
- The mode must be one previously returned by :meth:`get_mode` or
207
+ The mode must be one previously returned by :meth:`get_mode` or
212
208
  :meth:`get_modes`.
213
209
  """
214
210
  raise NotImplementedError('abstract')
215
211
 
216
212
  def restore_mode(self) -> None:
217
- """Restore the screen mode to the user's default.
218
- """
213
+ """Restore the screen mode to the user's default."""
219
214
  raise NotImplementedError('abstract')
220
215
 
221
216
  def get_dpi(self):
222
- """Get the DPI of the screen.
217
+ """Get the DPI of the screen."""
218
+ raise NotImplementedError('abstract')
223
219
 
224
- :rtype: int
225
- """
220
+ def get_scale(self) -> float:
221
+ """Get the pixel scale ratio of the screen."""
226
222
  raise NotImplementedError('abstract')
227
223
 
228
- def get_scale(self):
229
- """Get the pixel scale ratio of the screen.
224
+ @abc.abstractmethod
225
+ def get_display_id(self) -> str | int:
226
+ """Get a unique identifier for the screen.
230
227
 
231
- :rtype: float
228
+ .. versionadded: 2.1.8
229
+ """
230
+
231
+ @abc.abstractmethod
232
+ def get_monitor_name(self) -> str | Literal["Unknown"]:
233
+ """Get a friendly name for the screen if available.
234
+
235
+ Windows and Mac OSX report what the system will see the screen as.
236
+
237
+ On Linux, there is no API for retrieving the monitor name, other than manually decoding the EDID information.
238
+ Instead, it will return the connection name.
239
+
240
+ If no name can be queried, a default name of Unknown will be returned.
241
+
242
+ .. versionadded: 2.1.8
232
243
  """
233
- raise NotImplementedError('abstract')
234
244
 
235
245
 
236
246
  class ScreenMode:
pyglet/display/cocoa.py CHANGED
@@ -19,8 +19,17 @@ class CocoaDisplay(Display):
19
19
  activeDisplays = (CGDirectDisplayID * maxDisplays)()
20
20
  count = c_uint32()
21
21
  quartz.CGGetActiveDisplayList(maxDisplays, activeDisplays, byref(count))
22
- return [CocoaScreen(self, displayID) for displayID in list(activeDisplays)[:count.value]]
22
+ return [CocoaScreen(self, displayID) for displayID in list(activeDisplays)[: count.value]]
23
23
 
24
+ def get_default_screen(self) -> Screen:
25
+ main_id = quartz.CGMainDisplayID()
26
+
27
+ screens = self.get_screens()
28
+ for screen in screens:
29
+ if screen.get_display_id() == main_id:
30
+ return screen
31
+
32
+ return screens[0]
24
33
 
25
34
  class CocoaScreen(Screen):
26
35
 
@@ -36,6 +45,11 @@ class CocoaScreen(Screen):
36
45
  # Save the default mode so we can restore to it.
37
46
  self._default_mode = self.get_mode()
38
47
  self._ns_screen = self.get_nsscreen()
48
+ self._friendly_name = "Unknown"
49
+ if self._ns_screen is not None:
50
+ screen_name = self._ns_screen.localizedName()
51
+ if screen_name:
52
+ self._friendly_name = cfstring_to_string(screen_name)
39
53
 
40
54
  def get_nsscreen(self):
41
55
  """Returns the NSScreen instance that matches our CGDirectDisplayID."""
@@ -107,6 +121,16 @@ class CocoaScreen(Screen):
107
121
  def release_display(self):
108
122
  quartz.CGDisplayRelease(self._cg_display_id)
109
123
 
124
+ def get_display_id(self) -> str:
125
+ """Get a unique identifier for the screen."""
126
+ return self._cg_display_id
127
+
128
+ def get_monitor_name(self) -> str:
129
+ """Get a friendly name, if available.
130
+
131
+ For example, the make and model of the screen: Dell S2716DG
132
+ """
133
+ return self._friendly_name
110
134
 
111
135
  class CocoaScreenMode(ScreenMode):
112
136
 
@@ -21,7 +21,7 @@ class HeadlessDisplay(Display):
21
21
  if num_devices.value > 0:
22
22
  headless_device = pyglet.options['headless_device']
23
23
  if headless_device < 0 or headless_device >= num_devices.value:
24
- raise ValueError(f'Invalid EGL devide id: {headless_device}')
24
+ raise ValueError(f'Invalid EGL device id: {headless_device}')
25
25
  devices = (eglext.EGLDeviceEXT * num_devices.value)()
26
26
  eglext.eglQueryDevicesEXT(num_devices.value, devices, byref(num_devices))
27
27
  self._display_connection = eglext.eglGetPlatformDisplayEXT(
pyglet/display/win32.py CHANGED
@@ -1,12 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
1
4
  from ctypes import byref, sizeof
5
+ from typing import TYPE_CHECKING
2
6
 
3
7
  from pyglet.libs.win32 import _gdi32, _shcore, _user32
4
8
  from pyglet.libs.win32.constants import (
5
9
  CDS_FULLSCREEN,
6
10
  DISP_CHANGE_SUCCESSFUL,
11
+ DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME,
12
+ DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME,
7
13
  ENUM_CURRENT_SETTINGS,
8
14
  LOGPIXELSX,
9
15
  LOGPIXELSY,
16
+ MONITORINFOF_PRIMARY,
17
+ QDC_ONLY_ACTIVE_PATHS,
10
18
  USER_DEFAULT_SCREEN_DPI,
11
19
  WINDOWS_8_1_OR_GREATER,
12
20
  WINDOWS_10_CREATORS_UPDATE_OR_GREATER,
@@ -15,15 +23,23 @@ from pyglet.libs.win32.constants import (
15
23
  from pyglet.libs.win32.context_managers import device_context
16
24
  from pyglet.libs.win32.types import (
17
25
  DEVMODE,
26
+ DISPLAY_DEVICEW,
27
+ DISPLAYCONFIG_PATH_INFO,
28
+ DISPLAYCONFIG_SOURCE_DEVICE_NAME,
29
+ DISPLAYCONFIG_TARGET_DEVICE_NAME,
18
30
  DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,
19
31
  MONITORENUMPROC,
20
32
  MONITORINFOEX,
21
33
  PROCESS_PER_MONITOR_DPI_AWARE,
22
34
  UINT,
35
+ UINT32,
23
36
  )
24
37
 
25
38
  from .base import Canvas, Display, Screen, ScreenMode
26
39
 
40
+ if TYPE_CHECKING:
41
+ from ctypes.wintypes import HDC, HMONITOR, LPARAM, LPRECT
42
+
27
43
 
28
44
  def set_dpi_awareness() -> None:
29
45
  """Setting DPI varies per Windows version.
@@ -41,11 +57,19 @@ def set_dpi_awareness() -> None:
41
57
  set_dpi_awareness()
42
58
 
43
59
 
44
- class Win32Display(Display):
45
- def get_screens(self):
60
+ class Win32Display(Display): # noqa: D101
61
+ def get_default_screen(self) -> Screen:
62
+ screens = self.get_screens()
63
+ for screen in screens:
64
+ if screen.is_primary:
65
+ return screen
66
+
67
+ return screens[0]
68
+
69
+ def get_screens(self) -> list[Win32Screen]:
46
70
  screens = []
47
71
 
48
- def enum_proc(hMonitor, hdcMonitor, lprcMonitor, dwData):
72
+ def enum_proc(hMonitor: HMONITOR, hdcMonitor: HDC, lprcMonitor: LPRECT, dwData: LPARAM) -> bool: # noqa: N803, ARG001
49
73
  r = lprcMonitor.contents
50
74
  width = r.right - r.left
51
75
  height = r.bottom - r.top
@@ -58,12 +82,88 @@ class Win32Display(Display):
58
82
  return screens
59
83
 
60
84
 
61
- class Win32Screen(Screen):
85
+ class Win32Screen(Screen): # noqa: D101
86
+ _handle: HMONITOR
62
87
  _initial_mode = None
63
88
 
64
- def __init__(self, display, handle, x, y, width, height):
89
+ def __init__(self, display: Win32Display, handle: HMONITOR, x: int, y: int, width: int, height: int) -> None: # noqa: D107
65
90
  super().__init__(display, x, y, width, height)
66
91
  self._handle = handle
92
+ self._device_name = self.get_device_name() # \\.\DISPLAY1
93
+ self._friendly_name = self._get_friendly_name()
94
+
95
+ @property
96
+ def is_primary(self) -> bool:
97
+ """If the screen is considered the primary according to the operating system."""
98
+ info = self._get_monitor_info()
99
+ return info.dwFlags & MONITORINFOF_PRIMARY
100
+
101
+ def _get_friendly_name_display_config_api(self) -> str:
102
+ """Get the friendly name of a monitor using the newer Display Configuration API.
103
+
104
+ This API is meant to replace EnumDisplayDevicesW, and should be more accurate.
105
+
106
+ Requires Windows Vista or higher.
107
+ """
108
+ path_count = UINT32()
109
+ mode_count = UINT32()
110
+
111
+ result = _user32.GetDisplayConfigBufferSizes(
112
+ QDC_ONLY_ACTIVE_PATHS, ctypes.byref(path_count), ctypes.byref(mode_count),
113
+ )
114
+ if result != 0:
115
+ return "Unknown"
116
+
117
+ paths = (DISPLAYCONFIG_PATH_INFO * path_count.value)()
118
+ modes = ctypes.create_string_buffer(64 * mode_count.value) # dummy buffer
119
+
120
+ result = _user32.QueryDisplayConfig(
121
+ QDC_ONLY_ACTIVE_PATHS, ctypes.byref(path_count), paths, ctypes.byref(mode_count), modes, 0,
122
+ )
123
+ if result != 0:
124
+ return "Unknown"
125
+
126
+ for i in range(path_count.value):
127
+ path = paths[i]
128
+
129
+ source_name = DISPLAYCONFIG_SOURCE_DEVICE_NAME()
130
+ source_name.header.adapterId = path.sourceInfo.adapterId
131
+ source_name.header.id = path.sourceInfo.id
132
+ source_name.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME
133
+ source_name.header.size = ctypes.sizeof(source_name)
134
+
135
+ result = _user32.DisplayConfigGetDeviceInfo(ctypes.byref(source_name.header))
136
+ if result != 0:
137
+ continue
138
+
139
+ if source_name.viewGdiDeviceName != self._device_name:
140
+ continue
141
+
142
+ if not path.targetInfo.targetAvailable:
143
+ continue
144
+
145
+ target_name = DISPLAYCONFIG_TARGET_DEVICE_NAME()
146
+
147
+ target_name.header.adapterId = path.targetInfo.adapterId
148
+ target_name.header.id = path.targetInfo.id
149
+ target_name.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME
150
+ target_name.header.size = ctypes.sizeof(target_name)
151
+
152
+ if _user32.DisplayConfigGetDeviceInfo(ctypes.byref(target_name.header)) == 0:
153
+ return target_name.monitorFriendlyDeviceName
154
+
155
+ return "Unknown"
156
+
157
+ def _get_friendly_name(self) -> str:
158
+ if WINDOWS_VISTA_OR_GREATER:
159
+ return self._get_friendly_name_display_config_api()
160
+
161
+ dd = DISPLAY_DEVICEW()
162
+ dd.cb = ctypes.sizeof(dd)
163
+ if _user32.EnumDisplayDevicesW(self._device_name, 0, ctypes.byref(dd), 0):
164
+ return dd.DeviceString
165
+
166
+ return "Unknown"
67
167
 
68
168
  def get_matching_configs(self, template):
69
169
  with device_context(None) as hdc:
@@ -75,13 +175,23 @@ class Win32Screen(Screen):
75
175
 
76
176
  return configs
77
177
 
78
- def get_device_name(self):
178
+ def _get_monitor_info(self) -> MONITORINFOEX:
79
179
  info = MONITORINFOEX()
80
180
  info.cbSize = sizeof(MONITORINFOEX)
81
181
  _user32.GetMonitorInfoW(self._handle, byref(info))
182
+ return info
183
+
184
+ def get_display_id(self) -> str:
185
+ return self._device_name
186
+
187
+ def get_monitor_name(self) -> str:
188
+ return self._friendly_name
189
+
190
+ def get_device_name(self) -> str:
191
+ info = self._get_monitor_info()
82
192
  return info.szDevice
83
193
 
84
- def get_dpi(self):
194
+ def get_dpi(self) -> int:
85
195
  if WINDOWS_8_1_OR_GREATER:
86
196
  xdpi = UINT()
87
197
  ydpi = UINT()
@@ -95,11 +205,11 @@ class Win32Screen(Screen):
95
205
 
96
206
  return xdpi
97
207
 
98
- def get_scale(self):
208
+ def get_scale(self) -> float:
99
209
  xdpi = self.get_dpi()
100
210
  return xdpi / USER_DEFAULT_SCREEN_DPI
101
211
 
102
- def get_modes(self):
212
+ def get_modes(self) -> list[Win32ScreenMode]:
103
213
  device_name = self.get_device_name()
104
214
  i = 0
105
215
  modes = []
@@ -115,7 +225,7 @@ class Win32Screen(Screen):
115
225
 
116
226
  return modes
117
227
 
118
- def get_mode(self):
228
+ def get_mode(self) -> Win32ScreenMode:
119
229
  mode = DEVMODE()
120
230
  mode.dmSize = sizeof(DEVMODE)
121
231
  _user32.EnumDisplaySettingsW(self.get_device_name(),
@@ -123,7 +233,7 @@ class Win32Screen(Screen):
123
233
  byref(mode))
124
234
  return Win32ScreenMode(self, mode)
125
235
 
126
- def set_mode(self, mode):
236
+ def set_mode(self, mode: Win32ScreenMode) -> None:
127
237
  assert mode.screen is self
128
238
 
129
239
  if not self._initial_mode:
@@ -137,13 +247,18 @@ class Win32Screen(Screen):
137
247
  self.width = mode.width
138
248
  self.height = mode.height
139
249
 
140
- def restore_mode(self):
250
+ def restore_mode(self) -> None:
141
251
  if self._initial_mode:
142
252
  self.set_mode(self._initial_mode)
143
253
 
144
254
 
145
- class Win32ScreenMode(ScreenMode):
146
- def __init__(self, screen, mode):
255
+ _win32_scale_name = {
256
+ 0: "default",
257
+ 1: "center",
258
+ 2: "stretch",
259
+ }
260
+ class Win32ScreenMode(ScreenMode): # noqa: D101
261
+ def __init__(self, screen: Win32Screen, mode: DEVMODE) -> None: # noqa: D107
147
262
  super().__init__(screen)
148
263
  self._mode = mode
149
264
  self.width = mode.dmPelsWidth
@@ -152,11 +267,12 @@ class Win32ScreenMode(ScreenMode):
152
267
  self.rate = mode.dmDisplayFrequency
153
268
  self.scaling = mode.dmDisplayFixedOutput
154
269
 
155
- def __repr__(self):
156
- return f'{self.__class__.__name__}(width={self.width!r}, height={self.height!r}, depth={self.depth!r}, rate={self.rate}, scaling={self.scaling})'
270
+ def __repr__(self) -> str:
271
+ return (f'{self.__class__.__name__}(width={self.width!r}, height={self.height!r}, depth={self.depth!r},'
272
+ f'rate={self.rate}, scaling={_win32_scale_name.get(self.scaling)})')
157
273
 
158
- class Win32Canvas(Canvas):
159
- def __init__(self, display, hwnd, hdc):
274
+ class Win32Canvas(Canvas): # noqa: D101
275
+ def __init__(self, display: Win32Display, hwnd: HWND, hdc: HDC) -> None: # noqa: D107
160
276
  super().__init__(display)
161
277
  self.hwnd = hwnd
162
278
  self.hdc = hdc