pyglet 2.1.2__py3-none-any.whl → 2.1.4__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 (74) hide show
  1. pyglet/__init__.py +21 -9
  2. pyglet/__init__.pyi +3 -1
  3. pyglet/app/cocoa.py +6 -3
  4. pyglet/app/xlib.py +1 -1
  5. pyglet/display/cocoa.py +2 -2
  6. pyglet/display/win32.py +17 -18
  7. pyglet/display/xlib.py +2 -2
  8. pyglet/display/xlib_vidmoderestore.py +1 -1
  9. pyglet/extlibs/earcut.py +2 -2
  10. pyglet/font/__init__.py +3 -3
  11. pyglet/font/base.py +118 -51
  12. pyglet/font/dwrite/__init__.py +1381 -0
  13. pyglet/font/dwrite/d2d1_lib.py +637 -0
  14. pyglet/font/dwrite/d2d1_types_lib.py +60 -0
  15. pyglet/font/dwrite/dwrite_lib.py +1577 -0
  16. pyglet/font/fontconfig.py +79 -16
  17. pyglet/font/freetype.py +252 -77
  18. pyglet/font/freetype_lib.py +234 -125
  19. pyglet/font/harfbuzz/__init__.py +275 -0
  20. pyglet/font/harfbuzz/harfbuzz_lib.py +212 -0
  21. pyglet/font/quartz.py +432 -112
  22. pyglet/font/user.py +18 -11
  23. pyglet/font/win32.py +9 -1
  24. pyglet/gl/wgl.py +94 -87
  25. pyglet/gl/wglext_arb.py +472 -218
  26. pyglet/gl/wglext_nv.py +410 -188
  27. pyglet/gui/frame.py +4 -4
  28. pyglet/gui/widgets.py +6 -1
  29. pyglet/image/__init__.py +0 -2
  30. pyglet/image/codecs/bmp.py +3 -5
  31. pyglet/image/codecs/dds.py +1 -1
  32. pyglet/image/codecs/gdiplus.py +28 -9
  33. pyglet/image/codecs/wic.py +198 -489
  34. pyglet/image/codecs/wincodec_lib.py +413 -0
  35. pyglet/input/base.py +3 -2
  36. pyglet/input/linux/x11_xinput.py +3 -3
  37. pyglet/input/linux/x11_xinput_tablet.py +2 -2
  38. pyglet/input/macos/darwin_hid.py +28 -2
  39. pyglet/input/win32/directinput.py +3 -2
  40. pyglet/input/win32/wintab.py +1 -1
  41. pyglet/input/win32/xinput.py +10 -9
  42. pyglet/lib.py +14 -2
  43. pyglet/libs/darwin/cocoapy/cocoalibs.py +74 -3
  44. pyglet/libs/darwin/coreaudio.py +0 -2
  45. pyglet/libs/win32/__init__.py +4 -2
  46. pyglet/libs/win32/com.py +65 -12
  47. pyglet/libs/win32/constants.py +1 -0
  48. pyglet/libs/win32/dinput.py +1 -9
  49. pyglet/libs/win32/types.py +72 -8
  50. pyglet/math.py +5 -5
  51. pyglet/media/codecs/coreaudio.py +1 -0
  52. pyglet/media/codecs/wmf.py +93 -72
  53. pyglet/media/devices/win32.py +5 -4
  54. pyglet/media/drivers/directsound/lib_dsound.py +4 -4
  55. pyglet/media/drivers/xaudio2/interface.py +21 -17
  56. pyglet/media/drivers/xaudio2/lib_xaudio2.py +42 -25
  57. pyglet/model/__init__.py +78 -57
  58. pyglet/shapes.py +1 -1
  59. pyglet/text/document.py +7 -53
  60. pyglet/text/formats/attributed.py +3 -1
  61. pyglet/text/formats/plaintext.py +1 -1
  62. pyglet/text/formats/structured.py +1 -1
  63. pyglet/text/layout/base.py +76 -68
  64. pyglet/text/layout/incremental.py +38 -8
  65. pyglet/text/layout/scrolling.py +1 -1
  66. pyglet/text/runlist.py +2 -114
  67. pyglet/window/__init__.py +11 -8
  68. pyglet/window/win32/__init__.py +1 -3
  69. pyglet/window/xlib/__init__.py +2 -2
  70. {pyglet-2.1.2.dist-info → pyglet-2.1.4.dist-info}/METADATA +2 -3
  71. {pyglet-2.1.2.dist-info → pyglet-2.1.4.dist-info}/RECORD +73 -67
  72. pyglet/font/directwrite.py +0 -2798
  73. {pyglet-2.1.2.dist-info → pyglet-2.1.4.dist-info}/LICENSE +0 -0
  74. {pyglet-2.1.2.dist-info → pyglet-2.1.4.dist-info}/WHEEL +0 -0
pyglet/__init__.py CHANGED
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
15
15
  from typing import Any, Callable, ItemsView, Sized
16
16
 
17
17
  #: The release version
18
- version = '2.1.2'
18
+ version = '2.1.4'
19
19
  __version__ = version
20
20
 
21
21
  MIN_PYTHON_VERSION = 3, 8
@@ -110,6 +110,10 @@ class Options:
110
110
  debug_trace_depth: int = 1
111
111
  debug_trace_flush: bool = True
112
112
 
113
+ debug_com: bool = False
114
+ """If ``True``, prints information on COM calls. This can potentially help narrow down issues with certain libraries
115
+ that utilize COM calls. Only applies to the Windows platform."""
116
+
113
117
  debug_win32: bool = False
114
118
  """If ``True``, prints error messages related to Windows library calls. Usually get's information from
115
119
  ``Kernel32.GetLastError``. This information is output to a file called ``debug_win32.log``."""
@@ -175,6 +179,11 @@ class Options:
175
179
  .. versionadded:: 2.0
176
180
  """
177
181
 
182
+ text_antialiasing: bool = True
183
+ """If ``True``, font renderers will improve text quality by adding antialiasing to the rendered characters. If
184
+ ``False``, text quality will appear pixelated.
185
+ """
186
+
178
187
  headless: bool = False
179
188
  """If ``True``, visible Windows are not created and a running desktop environment is not required. This option
180
189
  is useful when running pyglet on a headless server, or compute cluster. OpenGL drivers with ``EGL`` support are
@@ -186,14 +195,17 @@ class Options:
186
195
  GPU to use. This is only useful on multi-GPU systems.
187
196
  """
188
197
 
189
- win32_disable_shaping: bool = False
190
- """If ``True``, will disable the shaping process for the default Windows font renderer to offer a performance
191
- speed up. If your font is simple, monospaced, or you require no advanced OpenType features, this option may be
192
- useful. You can try enabling this to see if there is any impact on clarity for your font. The advance will be
193
- determined by the glyph width.
198
+ text_shaping: Literal["platform", "harfbuzz", False] = 'platform'
199
+ """Determines how text is processed and displayed based on features of the font.
194
200
 
195
- .. note:: Shaping is the process of determining which character glyphs to use and specific placements of those
196
- glyphs when given a full string of characters.
201
+ Valid option names are:
202
+
203
+ * ``False``, Disables the shaping process for text. This may increase performance as it reduces the amount
204
+ of calls during rendering. If your font is simple, monospaced, or you require no advanced OpenType features,
205
+ this option may be useful.
206
+ * ``'platform'``, Uses platform's font system for shaping. Supported by Windows (DirectWrite) and Mac (CoreText).
207
+ * ``'harfbuzz'``, Utilize the harfbuzz library for font shaping. This requires an optional dependency, if not
208
+ found, it will fallback to platform shaping.
197
209
 
198
210
  .. versionadded:: 2.0
199
211
  """
@@ -270,7 +282,7 @@ class Options:
270
282
  rescaling and repositioning of content will be necessary, but at the cost of blurry content depending on the extent
271
283
  of the stretch. For example, 800x600 at 150% DPI will be 800x600 for `window.get_size()` and 1200x900 for
272
284
  `window.get_framebuffer_size()`.
273
-
285
+
274
286
  `'platform'`: A DPI aware window is created, however window sizing and framebuffer sizing is not interfered with
275
287
  by Pyglet. Final sizes are dictated by the platform the window was created on. It is up to the user to make any
276
288
  platform adjustments themselves such as sizing on a platform, mouse coordinate adjustments, or framebuffer size
pyglet/__init__.pyi CHANGED
@@ -49,15 +49,17 @@ class Options:
49
49
  debug_win32: bool
50
50
  debug_input: bool
51
51
  debug_x11: bool
52
+ debug_com: bool
52
53
  shadow_window: bool
53
54
  vsync: bool | None
54
55
  xsync: bool
55
56
  xlib_fullscreen_override_redirect: bool
56
57
  search_local_libs: bool
57
58
  win32_gdi_font: bool
59
+ text_antialiasing: bool
58
60
  headless: bool
59
61
  headless_device: int
60
- win32_disable_shaping: bool
62
+ text_shaping: Literal["platform", "harfbuzz", False]
61
63
  dw_legacy_naming: bool
62
64
  win32_disable_xinput: bool
63
65
  com_mta: bool
pyglet/app/cocoa.py CHANGED
@@ -113,8 +113,10 @@ class CocoaAlternateEventLoop(EventLoop):
113
113
  super().__init__()
114
114
  self.platform_event_loop = None
115
115
 
116
- def run(self, interval=1/60):
117
- if not interval:
116
+ def run(self, interval: float | None = 1/60):
117
+ if interval is None:
118
+ pass # do not schedule redraws
119
+ elif not interval:
118
120
  self.clock.schedule(self._redraw_windows)
119
121
  else:
120
122
  self.clock.schedule_interval(self._redraw_windows, interval)
@@ -133,7 +135,8 @@ class CocoaAlternateEventLoop(EventLoop):
133
135
 
134
136
  self.dispatch_event('on_enter')
135
137
  self.is_running = True
136
- self.platform_event_loop.nsapp_start(interval)
138
+
139
+ self.platform_event_loop.nsapp_start(interval or 0)
137
140
 
138
141
  def exit(self):
139
142
  """Safely exit the event loop at the end of the current iteration.
pyglet/app/xlib.py CHANGED
@@ -54,7 +54,7 @@ class NotificationDevice(XlibSelectDevice):
54
54
 
55
55
  class XlibEventLoop(PlatformEventLoop):
56
56
  def __init__(self):
57
- super(XlibEventLoop, self).__init__()
57
+ super().__init__()
58
58
  self._notification_device = NotificationDevice()
59
59
  self.select_devices = set()
60
60
  self.select_devices.add(self._notification_device)
pyglet/display/cocoa.py CHANGED
@@ -31,7 +31,7 @@ class CocoaScreen(Screen):
31
31
  # http://www.cocoabuilder.com/archive/cocoa/233492-ns-cg-rect-conversion-and-screen-coordinates.html
32
32
  x, y = bounds.origin.x, bounds.origin.y
33
33
  width, height = bounds.size.width, bounds.size.height
34
- super(CocoaScreen, self).__init__(display, int(x), int(y), int(width), int(height))
34
+ super().__init__(display, int(x), int(y), int(width), int(height))
35
35
  self._cg_display_id = displayID
36
36
  # Save the default mode so we can restore to it.
37
37
  self._default_mode = self.get_mode()
@@ -111,7 +111,7 @@ class CocoaScreen(Screen):
111
111
  class CocoaScreenMode(ScreenMode):
112
112
 
113
113
  def __init__(self, screen, cgmode):
114
- super(CocoaScreenMode, self).__init__(screen)
114
+ super().__init__(screen)
115
115
  quartz.CGDisplayModeRetain(cgmode)
116
116
  self.cgmode = cgmode
117
117
  self.width = int(quartz.CGDisplayModeGetWidth(cgmode))
pyglet/display/win32.py CHANGED
@@ -1,35 +1,34 @@
1
- from .base import Display, Screen, ScreenMode, Canvas
1
+ from ctypes import byref, sizeof
2
2
 
3
- from pyglet.libs.win32 import _user32, _shcore, _gdi32
3
+ from pyglet.libs.win32 import _gdi32, _shcore, _user32
4
4
  from pyglet.libs.win32.constants import (
5
5
  CDS_FULLSCREEN,
6
6
  DISP_CHANGE_SUCCESSFUL,
7
7
  ENUM_CURRENT_SETTINGS,
8
- WINDOWS_8_1_OR_GREATER,
9
- WINDOWS_VISTA_OR_GREATER,
10
- WINDOWS_10_CREATORS_UPDATE_OR_GREATER,
11
- USER_DEFAULT_SCREEN_DPI,
12
8
  LOGPIXELSX,
13
9
  LOGPIXELSY,
14
-
10
+ USER_DEFAULT_SCREEN_DPI,
11
+ WINDOWS_8_1_OR_GREATER,
12
+ WINDOWS_10_CREATORS_UPDATE_OR_GREATER,
13
+ WINDOWS_VISTA_OR_GREATER,
15
14
  )
15
+ from pyglet.libs.win32.context_managers import device_context
16
16
  from pyglet.libs.win32.types import (
17
17
  DEVMODE,
18
18
  DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,
19
- MONITORINFOEX,
20
19
  MONITORENUMPROC,
20
+ MONITORINFOEX,
21
21
  PROCESS_PER_MONITOR_DPI_AWARE,
22
22
  UINT,
23
- sizeof,
24
- byref
25
23
  )
26
- from pyglet.libs.win32.context_managers import device_context
27
24
 
25
+ from .base import Canvas, Display, Screen, ScreenMode
28
26
 
29
- def set_dpi_awareness():
30
- """
31
- Setting DPI varies per Windows version.
32
- Note: DPI awareness needs to be set before Window, Display, or Screens are initialized.
27
+
28
+ def set_dpi_awareness() -> None:
29
+ """Setting DPI varies per Windows version.
30
+
31
+ .. note:: DPI awareness needs to be set before Window, Display, or Screens are initialized.
33
32
  """
34
33
  if WINDOWS_10_CREATORS_UPDATE_OR_GREATER:
35
34
  _user32.SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)
@@ -63,7 +62,7 @@ class Win32Screen(Screen):
63
62
  _initial_mode = None
64
63
 
65
64
  def __init__(self, display, handle, x, y, width, height):
66
- super(Win32Screen, self).__init__(display, x, y, width, height)
65
+ super().__init__(display, x, y, width, height)
67
66
  self._handle = handle
68
67
 
69
68
  def get_matching_configs(self, template):
@@ -145,7 +144,7 @@ class Win32Screen(Screen):
145
144
 
146
145
  class Win32ScreenMode(ScreenMode):
147
146
  def __init__(self, screen, mode):
148
- super(Win32ScreenMode, self).__init__(screen)
147
+ super().__init__(screen)
149
148
  self._mode = mode
150
149
  self.width = mode.dmPelsWidth
151
150
  self.height = mode.dmPelsHeight
@@ -158,6 +157,6 @@ class Win32ScreenMode(ScreenMode):
158
157
 
159
158
  class Win32Canvas(Canvas):
160
159
  def __init__(self, display, hwnd, hdc):
161
- super(Win32Canvas, self).__init__(display)
160
+ super().__init__(display)
162
161
  self.hwnd = hwnd
163
162
  self.hdc = hdc
pyglet/display/xlib.py CHANGED
@@ -96,7 +96,7 @@ class XlibDisplay(XlibSelectDevice, Display):
96
96
  if x_screen >= screen_count:
97
97
  raise NoSuchDisplayException(f'Display "{name}" has no screen {x_screen:d}')
98
98
 
99
- super(XlibDisplay, self).__init__()
99
+ super().__init__()
100
100
  self.name = name
101
101
  self.x_screen = x_screen
102
102
 
@@ -276,7 +276,7 @@ class XlibScreenMode(ScreenMode):
276
276
  self.info = info
277
277
  self.width = info.hdisplay
278
278
  self.height = info.vdisplay
279
- self.rate = info.dotclock
279
+ self.rate = (info.dotclock * 1000) / (info.htotal * info.vtotal)
280
280
  self.depth = None
281
281
 
282
282
 
@@ -165,7 +165,7 @@ def set_initial_mode(mode):
165
165
  if (display, screen) in _restorable_screens:
166
166
  return
167
167
 
168
- packet = ModePacket(display, screen, mode.width, mode.height, mode.rate)
168
+ packet = ModePacket(display, screen, mode.width, mode.height, mode.info.dotclock)
169
169
 
170
170
  os.write(_mode_write_pipe, packet.encode())
171
171
  _restorable_screens.add((display, screen))
pyglet/extlibs/earcut.py CHANGED
@@ -473,8 +473,8 @@ def sortLinked(_list):
473
473
  # z-order of a point given coords and size of the data bounding box
474
474
  def zOrder(x, y, minX, minY, size):
475
475
  # coords are transformed into non-negative 15-bit integer range
476
- x = 32767 * (x - minX) // size
477
- y = 32767 * (y - minY) // size
476
+ x = int(32767 * (x - minX) / size)
477
+ y = int(32767 * (y - minY) / size)
478
478
 
479
479
  x = (x | (x << 8)) & 0x00FF00FF
480
480
  x = (x | (x << 4)) & 0x0F0F0F0F
pyglet/font/__init__.py CHANGED
@@ -42,7 +42,7 @@ def _get_system_font_class() -> type[Font]:
42
42
  elif pyglet.compat_platform in ("win32", "cygwin"):
43
43
  from pyglet.libs.win32.constants import WINDOWS_7_OR_GREATER
44
44
  if WINDOWS_7_OR_GREATER and not pyglet.options["win32_gdi_font"]:
45
- from pyglet.font.directwrite import Win32DirectWriteFont
45
+ from pyglet.font.dwrite import Win32DirectWriteFont
46
46
  _font_class = Win32DirectWriteFont
47
47
  else:
48
48
  from pyglet.font.win32 import GDIPlusFont
@@ -194,7 +194,7 @@ if not getattr(sys, "is_pyglet_doc_run", False):
194
194
  _user_fonts = []
195
195
 
196
196
 
197
- def add_file(font: str | BinaryIO) -> None:
197
+ def add_file(font: str | BinaryIO | bytes) -> None:
198
198
  """Add a font to pyglet's search path.
199
199
 
200
200
  In order to load a font that is not installed on the system, you must
@@ -208,7 +208,7 @@ def add_file(font: str | BinaryIO) -> None:
208
208
 
209
209
  Args:
210
210
  font:
211
- Filename or file-like object to load fonts from.
211
+ Filename, file-like object, or bytes to load fonts from.
212
212
 
213
213
  """
214
214
  if isinstance(font, str):
pyglet/font/base.py CHANGED
@@ -7,66 +7,66 @@ classes as a documented interface to the concrete classes.
7
7
  from __future__ import annotations
8
8
 
9
9
  import abc
10
+ from dataclasses import dataclass
11
+
10
12
  import unicodedata
11
13
  from typing import BinaryIO, ClassVar
12
14
 
13
15
  from pyglet import image
14
16
  from pyglet.gl import GL_LINEAR, GL_RGBA, GL_TEXTURE_2D
15
17
 
16
- _other_grapheme_extend = list(map(chr, [0x09be, 0x09d7, 0x0be3, 0x0b57, 0x0bbe, 0x0bd7, 0x0cc2,
17
- 0x0cd5, 0x0cd6, 0x0d3e, 0x0d57, 0x0dcf, 0x0ddf, 0x200c,
18
- 0x200d, 0xff9e, 0xff9f])) # skip codepoints above U+10000
19
- _logical_order_exception = list(map(chr, list(range(0xe40, 0xe45)) + list(range(0xec0, 0xec4))))
18
+ _OTHER_GRAPHEME_EXTEND = {
19
+ chr(x) for x in [0x09be, 0x09d7, 0x0be3, 0x0b57, 0x0bbe, 0x0bd7, 0x0cc2,
20
+ 0x0cd5, 0x0cd6, 0x0d3e, 0x0d57, 0x0dcf, 0x0ddf, 0x200c,
21
+ 0x200d, 0xff9e, 0xff9f]
22
+ } # skip codepoints above U+10000
23
+ _LOGICAL_ORDER_EXCEPTION = {chr(x) for x in range(0xe40, 0xe45)} | {chr(x) for x in range(0xec0, 0xec4)}
20
24
 
21
- _grapheme_extend = lambda c, cc: cc in ("Me", "Mn") or c in _other_grapheme_extend
25
+ _EXTEND_CHARS = {chr(x) for x in [0xe30, 0xe32, 0xe33, 0xe45, 0xeb0, 0xeb2, 0xeb3]}
22
26
 
23
27
  _CR = "\u000d"
24
28
  _LF = "\u000a"
25
- _control = lambda c, cc: cc in ("ZI", "Zp", "Cc", "Cf") and c not in list(map(chr, [0x000d, 0x000a, 0x200c, 0x200d]))
26
- _extend = lambda c, cc: _grapheme_extend(c, cc) or \
27
- c in list(map(chr, [0xe30, 0xe32, 0xe33, 0xe45, 0xeb0, 0xeb2, 0xeb3]))
28
- _prepend = lambda c, cc: c in _logical_order_exception # noqa: ARG005
29
- _spacing_mark = lambda c, cc: cc == "Mc" and c not in _other_grapheme_extend
29
+
30
+ _CATEGORY_EXTEND = {"Me", "Mn"}
31
+ _CATEGORY_CONTROL = {"ZI", "Zp", "Cc", "Cf"}
32
+ _CATEGORY_SPACING_MARK = {"Mc"}
30
33
 
31
34
 
32
- def grapheme_break(left: str, right: str) -> bool: # noqa: D103
35
+ def grapheme_break(left: str, left_cc: str, right: str, right_cc: str) -> bool:
36
+ """Determines if there should be a break between characters."""
33
37
  # GB1
34
38
  if left is None:
35
39
  return True
36
40
 
37
41
  # GB2 not required, see end of get_grapheme_clusters
38
42
 
39
- # GB3
43
+ # GB3: CR + LF do not break
40
44
  if left == _CR and right == _LF:
41
45
  return False
42
46
 
43
- left_cc = unicodedata.category(left)
44
-
45
- # GB4
46
- if _control(left, left_cc):
47
+ # GB4: Break before Control characters
48
+ if left_cc in _CATEGORY_CONTROL and left not in _OTHER_GRAPHEME_EXTEND:
47
49
  return True
48
50
 
49
- right_cc = unicodedata.category(right)
50
-
51
- # GB5
52
- if _control(right, right_cc):
51
+ # GB5: Break after Control characters
52
+ if right_cc in _CATEGORY_CONTROL and right not in _OTHER_GRAPHEME_EXTEND:
53
53
  return True
54
54
 
55
55
  # GB6, GB7, GB8 not implemented
56
56
 
57
- # GB9
58
- if _extend(right, right_cc):
57
+ # GB9: Do not break before Extend characters
58
+ if right_cc in _CATEGORY_EXTEND or right in _EXTEND_CHARS:
59
59
  return False
60
60
 
61
- # GB9a
62
- if _spacing_mark(right, right_cc):
61
+ # GB9a: Do not break before SpacingMark characters
62
+ if right_cc == "Mc" and right not in _OTHER_GRAPHEME_EXTEND:
63
63
  return False
64
64
 
65
- # GB9b
66
- if _prepend(left, left_cc):
65
+ # GB9b: Do not break after Prepend characters
66
+ if left in _LOGICAL_ORDER_EXCEPTION: # noqa: SIM103
67
67
  return False
68
68
 
69
- # GB10
69
+ # GB999: Default to break
70
70
  return True
71
71
 
72
72
 
@@ -83,24 +83,38 @@ def get_grapheme_clusters(text: str) -> list[str]:
83
83
  List of Unicode grapheme clusters.
84
84
  """
85
85
  clusters = []
86
- cluster = ""
86
+ cluster_chars = []
87
87
  left = None
88
+ left_cc = None
89
+
88
90
  for right in text:
89
- if cluster and grapheme_break(left, right):
90
- clusters.append(cluster)
91
- cluster = ""
92
- elif cluster:
93
- # Add a zero-width space to keep len(clusters) == len(text)
94
- clusters.append("\u200b")
95
- cluster += right
91
+ right_cc = unicodedata.category(right)
92
+
93
+ if cluster_chars and grapheme_break(left, left_cc, right, right_cc):
94
+ clusters.append("".join(cluster_chars))
95
+ cluster_chars.clear()
96
+
97
+ cluster_chars.append(right)
96
98
  left = right
99
+ left_cc = right_cc
100
+
101
+ if cluster_chars:
102
+ clusters.append("".join(cluster_chars))
97
103
 
98
- # GB2
99
- if cluster:
100
- clusters.append(cluster)
101
104
  return clusters
102
105
 
103
106
 
107
+ #: :meta private:
108
+ @dataclass
109
+ class GlyphPosition:
110
+ """Positioning offsets for a glyph."""
111
+ __slots__ = ('x_advance', 'x_offset', 'y_advance', 'y_offset')
112
+ x_advance: int # How far the line advances AFTER drawing horizontal.
113
+ y_advance: int # How far the line advances AFTER drawing vertical.
114
+ x_offset: int # How much the current glyph moves on the X-axis when drawn. Does not advance.
115
+ y_offset: int # How much the current glyph moves on the Y-axis when drawn. Does not advance.
116
+
117
+
104
118
  class Glyph(image.TextureRegion):
105
119
  """A single glyph located within a larger texture.
106
120
 
@@ -116,8 +130,7 @@ class Glyph(image.TextureRegion):
116
130
  #: :If a glyph is colored by the font renderer, such as an emoji, it may be treated differently by pyglet.
117
131
  colored = False
118
132
 
119
- def set_bearings(self, baseline: int, left_side_bearing: int, advance: int, x_offset: int = 0,
120
- y_offset: int = 0) -> None:
133
+ def set_bearings(self, baseline: int, left_side_bearing: int, advance: int) -> None:
121
134
  """Set metrics for this glyph.
122
135
 
123
136
  Args:
@@ -127,20 +140,16 @@ class Glyph(image.TextureRegion):
127
140
  Distance to add to the left edge of the glyph.
128
141
  advance:
129
142
  Distance to move the horizontal advance to the next glyph, in pixels.
130
- x_offset:
131
- Distance to move the glyph horizontally from its default position.
132
- y_offset:
133
- Distance to move the glyph vertically from its default position.
134
143
  """
135
144
  self.baseline = baseline
136
145
  self.lsb = left_side_bearing
137
146
  self.advance = advance
138
147
 
139
148
  self.vertices = (
140
- left_side_bearing + x_offset,
141
- -baseline + y_offset,
142
- left_side_bearing + self.width + x_offset,
143
- -baseline + self.height + y_offset)
149
+ left_side_bearing,
150
+ -baseline,
151
+ left_side_bearing + self.width,
152
+ -baseline + self.height)
144
153
 
145
154
 
146
155
  class GlyphTexture(image.Texture):
@@ -191,6 +200,7 @@ class GlyphRenderer(abc.ABC):
191
200
  Args:
192
201
  font: The :py:class:`~pyglet.font.base.Font` object to be rendered.
193
202
  """
203
+ self.font = font
194
204
 
195
205
  @abc.abstractmethod
196
206
  def render(self, text: str) -> Glyph:
@@ -203,6 +213,15 @@ class GlyphRenderer(abc.ABC):
203
213
  A Glyph with the proper metrics for that specific character.
204
214
  """
205
215
 
216
+ def create_zero_glyph(self) -> Glyph:
217
+ """Zero glyph is a 1x1 image that has a -1 advance.
218
+
219
+ This is to fill in for potential substitutions since font system requires 1 glyph per character in a string.
220
+ """
221
+ image_data = image.ImageData(1, 1, 'RGBA', bytes([0, 0, 0, 0]))
222
+ glyph = self.font.create_glyph(image_data)
223
+ glyph.set_bearings(-self.font.descent, 0, -1)
224
+ return glyph
206
225
 
207
226
  class FontException(Exception): # noqa: N818
208
227
  """Generic exception related to errors from the font module. Typically, from invalid font data."""
@@ -240,7 +259,7 @@ class Font:
240
259
  ``GL_NEAREST`` to prevent aliasing with pixelated fonts.
241
260
  """
242
261
  #: :meta private:
243
- glyphs: dict[str, Glyph]
262
+ glyphs: dict[str | int, Glyph]
244
263
 
245
264
  texture_width: int = 512
246
265
  texture_height: int = 512
@@ -264,10 +283,47 @@ class Font:
264
283
  # The default type of texture bins. Should not be overridden by users.
265
284
  texture_class: ClassVar[type[GlyphTextureBin]] = GlyphTextureBin
266
285
 
286
+ # A list of fallback fonts to use when an existing glyph is not found.
287
+ fallbacks: list[Font]
288
+
289
+ _glyph_renderer: GlyphRenderer | None
290
+ _missing_glyph: Glyph | None
291
+ _zero_glyph: Glyph | None
292
+
293
+ # The size of the font in pixels.
294
+ pixel_size: float
295
+
267
296
  def __init__(self) -> None:
268
297
  """Initialize a font that can be used with Pyglet."""
269
298
  self.texture_bin = None
299
+ self.hb_resource = None
300
+ self._glyph_renderer = None
301
+
302
+ # Represents a missing glyph.
303
+ self._missing_glyph = None
304
+
305
+ # Represents a zero width glyph.
306
+ self._zero_glyph = None
270
307
  self.glyphs = {}
308
+ self.fallbacks = []
309
+
310
+ def _initialize_renderer(self) -> None:
311
+ """Initialize the glyph renderer and cache it on the Font.
312
+
313
+ This way renderers for fonts that have been loaded but not used will not have unnecessary loaders.
314
+ """
315
+ if not self._glyph_renderer:
316
+ self._glyph_renderer = self.glyph_renderer_class(self)
317
+ self._missing_glyph = self._glyph_renderer.render(" ")
318
+ self._zero_glyph = self._glyph_renderer.create_zero_glyph()
319
+
320
+ def add_fallback(self, font: Font) -> None:
321
+ assert font not in self.fallbacks, "Font is already added."
322
+ self.fallbacks.append(font)
323
+
324
+ def remove_fallback(self, font: Font) -> None:
325
+ assert font not in self.fallbacks, "Font has not been added."
326
+ self.fallbacks.remove(font)
271
327
 
272
328
  @property
273
329
  @abc.abstractmethod
@@ -346,7 +402,7 @@ class Font:
346
402
 
347
403
  return atlas_size
348
404
 
349
- def get_glyphs(self, text: str) -> list[Glyph]:
405
+ def get_glyphs(self, text: str) -> tuple[list[Glyph], list[GlyphPosition]]:
350
406
  """Create and return a list of Glyphs for `text`.
351
407
 
352
408
  If any characters do not have a known glyph representation in this
@@ -357,7 +413,9 @@ class Font:
357
413
  Text to render.
358
414
  """
359
415
  glyph_renderer = None
416
+
360
417
  glyphs = [] # glyphs that are committed.
418
+ offsets = []
361
419
  for c in get_grapheme_clusters(str(text)):
362
420
  # Get the glyph for 'c'. Hide tabs (Windows and Linux render
363
421
  # boxes)
@@ -368,7 +426,16 @@ class Font:
368
426
  glyph_renderer = self.glyph_renderer_class(self)
369
427
  self.glyphs[c] = glyph_renderer.render(c)
370
428
  glyphs.append(self.glyphs[c])
371
- return glyphs
429
+ offsets.append(GlyphPosition(0, 0, 0, 0))
430
+
431
+ return glyphs, offsets
432
+
433
+ @abc.abstractmethod
434
+ def get_text_size(self, text: str) -> tuple[int, int]:
435
+ """Return's an estimated width and height of text using glyph metrics without rendering..
436
+
437
+ This does not take into account any shaping.
438
+ """
372
439
 
373
440
  def get_glyphs_for_width(self, text: str, width: int) -> list[Glyph]:
374
441
  """Return a list of glyphs for ``text`` that fit within the given width.