pyglet 2.1.3__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 (61) hide show
  1. pyglet/__init__.py +21 -9
  2. pyglet/__init__.pyi +3 -1
  3. pyglet/app/cocoa.py +6 -3
  4. pyglet/display/win32.py +14 -15
  5. pyglet/display/xlib_vidmoderestore.py +1 -1
  6. pyglet/extlibs/earcut.py +2 -2
  7. pyglet/font/__init__.py +3 -3
  8. pyglet/font/base.py +118 -51
  9. pyglet/font/dwrite/__init__.py +1381 -0
  10. pyglet/font/dwrite/d2d1_lib.py +637 -0
  11. pyglet/font/dwrite/d2d1_types_lib.py +60 -0
  12. pyglet/font/dwrite/dwrite_lib.py +1577 -0
  13. pyglet/font/fontconfig.py +79 -16
  14. pyglet/font/freetype.py +252 -77
  15. pyglet/font/freetype_lib.py +234 -125
  16. pyglet/font/harfbuzz/__init__.py +275 -0
  17. pyglet/font/harfbuzz/harfbuzz_lib.py +212 -0
  18. pyglet/font/quartz.py +432 -112
  19. pyglet/font/user.py +18 -11
  20. pyglet/font/win32.py +9 -1
  21. pyglet/gl/wgl.py +94 -87
  22. pyglet/gl/wglext_arb.py +472 -218
  23. pyglet/gl/wglext_nv.py +410 -188
  24. pyglet/gui/widgets.py +6 -1
  25. pyglet/image/codecs/bmp.py +3 -5
  26. pyglet/image/codecs/gdiplus.py +28 -9
  27. pyglet/image/codecs/wic.py +198 -489
  28. pyglet/image/codecs/wincodec_lib.py +413 -0
  29. pyglet/input/base.py +3 -2
  30. pyglet/input/macos/darwin_hid.py +28 -2
  31. pyglet/input/win32/directinput.py +2 -1
  32. pyglet/input/win32/xinput.py +10 -9
  33. pyglet/lib.py +14 -2
  34. pyglet/libs/darwin/cocoapy/cocoalibs.py +74 -3
  35. pyglet/libs/darwin/coreaudio.py +0 -2
  36. pyglet/libs/win32/__init__.py +4 -2
  37. pyglet/libs/win32/com.py +65 -12
  38. pyglet/libs/win32/constants.py +1 -0
  39. pyglet/libs/win32/dinput.py +1 -9
  40. pyglet/libs/win32/types.py +72 -8
  41. pyglet/media/codecs/coreaudio.py +1 -0
  42. pyglet/media/codecs/wmf.py +93 -72
  43. pyglet/media/devices/win32.py +5 -4
  44. pyglet/media/drivers/directsound/lib_dsound.py +4 -4
  45. pyglet/media/drivers/xaudio2/interface.py +21 -17
  46. pyglet/media/drivers/xaudio2/lib_xaudio2.py +42 -25
  47. pyglet/shapes.py +1 -1
  48. pyglet/text/document.py +7 -53
  49. pyglet/text/formats/attributed.py +3 -1
  50. pyglet/text/formats/plaintext.py +1 -1
  51. pyglet/text/formats/structured.py +1 -1
  52. pyglet/text/layout/base.py +76 -68
  53. pyglet/text/layout/incremental.py +38 -8
  54. pyglet/text/layout/scrolling.py +1 -1
  55. pyglet/text/runlist.py +2 -114
  56. pyglet/window/win32/__init__.py +1 -3
  57. {pyglet-2.1.3.dist-info → pyglet-2.1.4.dist-info}/METADATA +1 -1
  58. {pyglet-2.1.3.dist-info → pyglet-2.1.4.dist-info}/RECORD +60 -54
  59. pyglet/font/directwrite.py +0 -2798
  60. {pyglet-2.1.3.dist-info → pyglet-2.1.4.dist-info}/LICENSE +0 -0
  61. {pyglet-2.1.3.dist-info → pyglet-2.1.4.dist-info}/WHEEL +0 -0
@@ -1,30 +1,47 @@
1
- import ctypes
2
- import platform
3
1
  import os
4
- from pyglet.libs.win32.constants import *
5
- from pyglet.libs.win32.types import *
2
+ import platform
3
+ from ctypes import (
4
+ HRESULT,
5
+ POINTER,
6
+ Structure,
7
+ c_bool,
8
+ c_char,
9
+ c_float,
10
+ c_int,
11
+ c_uint32,
12
+ c_uint64,
13
+ c_void_p,
14
+ cdll,
15
+ windll,
16
+ )
17
+ from ctypes.wintypes import BOOL, BYTE, DWORD, FLOAT, LPCWSTR, UINT, WORD
18
+
6
19
  from pyglet.libs.win32 import com
20
+ from pyglet.libs.win32.constants import WINDOWS_10_ANNIVERSARY_UPDATE_OR_GREATER
21
+ from pyglet.libs.win32.types import c_void
7
22
  from pyglet.util import debug_print
8
23
 
9
24
  _debug = debug_print('debug_media')
10
25
 
11
26
 
12
27
  def load_xaudio2(dll_name):
13
- """This will attempt to load a version of XAudio2. Versions supported: 2.9, 2.8.
14
- While Windows 8 ships with 2.8 and Windows 10 ships with version 2.9, it is possible to install 2.9 on 8/8.1.
28
+ """This will attempt to load a version of XAudio2.
29
+
30
+ Versions supported: 2.9, 2.8.
31
+
32
+ While Windows 8 ships with 2.8 and Windows 10 ships with version 2.9, it is possible to install 2.9 on 8/8.1.
15
33
  """
16
34
  xaudio2 = dll_name
17
35
  # System32 and SysWOW64 folders are opposite perception in Windows x64.
18
36
  # System32 = x64 dll's | SysWOW64 = x86 dlls
19
37
  # By default ctypes only seems to look in system32 regardless of Python architecture, which has x64 dlls.
20
- if platform.architecture()[0] == '32bit':
21
- if platform.machine().endswith('64'): # Machine is 64 bit, Python is 32 bit.
22
- xaudio2 = os.path.join(os.environ['WINDIR'], 'SysWOW64', '{}.dll'.format(xaudio2))
38
+ if platform.architecture()[0] == '32bit' and platform.machine().endswith('64'): # Machine is 64 bit, Python is 32 bit.
39
+ xaudio2 = os.path.join(os.environ['WINDIR'], 'SysWOW64', '{}.dll'.format(xaudio2))
23
40
 
24
- xaudio2_lib = ctypes.windll.LoadLibrary(xaudio2)
41
+ xaudio2_lib = windll.LoadLibrary(xaudio2)
25
42
 
26
43
  # Somehow x3d uses different calling structure than the rest of the DLL; Only affects 32 bit? Microsoft...
27
- x3d_lib = ctypes.cdll.LoadLibrary(xaudio2)
44
+ x3d_lib = cdll.LoadLibrary(xaudio2)
28
45
  return xaudio2_lib, x3d_lib
29
46
 
30
47
 
@@ -43,7 +60,7 @@ UINT32 = c_uint32
43
60
  FLOAT32 = c_float
44
61
 
45
62
 
46
- class XAUDIO2_DEBUG_CONFIGURATION(ctypes.Structure):
63
+ class XAUDIO2_DEBUG_CONFIGURATION(Structure):
47
64
  _fields_ = [
48
65
  ('TraceMask', UINT32),
49
66
  ('BreakMask', UINT32),
@@ -54,7 +71,7 @@ class XAUDIO2_DEBUG_CONFIGURATION(ctypes.Structure):
54
71
  ]
55
72
 
56
73
 
57
- class XAUDIO2_PERFORMANCE_DATA(ctypes.Structure):
74
+ class XAUDIO2_PERFORMANCE_DATA(Structure):
58
75
  _fields_ = [
59
76
  ('AudioCyclesSinceLastQuery', c_uint64),
60
77
  ('TotalCyclesSinceLastQuery', c_uint64),
@@ -76,14 +93,14 @@ class XAUDIO2_PERFORMANCE_DATA(ctypes.Structure):
76
93
  return "XAUDIO2PerformanceData(active_voices={}, total_voices={}, glitches={}, latency={} samples, memory_usage={} bytes)".format(self.ActiveSourceVoiceCount, self.TotalSourceVoiceCount, self.GlitchesSinceEngineStarted, self.CurrentLatencyInSamples, self.MemoryUsageInBytes)
77
94
 
78
95
 
79
- class XAUDIO2_VOICE_SENDS(ctypes.Structure):
96
+ class XAUDIO2_VOICE_SENDS(Structure):
80
97
  _fields_ = [
81
98
  ('SendCount', UINT32),
82
99
  ('pSends', c_void_p),
83
100
  ]
84
101
 
85
102
 
86
- class XAUDIO2_BUFFER(ctypes.Structure):
103
+ class XAUDIO2_BUFFER(Structure):
87
104
  _fields_ = [
88
105
  ('Flags', UINT32),
89
106
  ('AudioBytes', UINT32),
@@ -96,7 +113,7 @@ class XAUDIO2_BUFFER(ctypes.Structure):
96
113
  ('pContext', c_void_p),
97
114
  ]
98
115
 
99
- class XAUDIO2_VOICE_STATE(ctypes.Structure):
116
+ class XAUDIO2_VOICE_STATE(Structure):
100
117
  _fields_ = [
101
118
  ('pCurrentBufferContext', c_void_p),
102
119
  ('BuffersQueued', UINT32),
@@ -106,7 +123,7 @@ class XAUDIO2_VOICE_STATE(ctypes.Structure):
106
123
  def __repr__(self):
107
124
  return "XAUDIO2_VOICE_STATE(BuffersQueued={0}, SamplesPlayed={1})".format(self.BuffersQueued, self.SamplesPlayed)
108
125
 
109
- class WAVEFORMATEX(ctypes.Structure):
126
+ class WAVEFORMATEX(Structure):
110
127
  _fields_ = [
111
128
  ('wFormatTag', WORD),
112
129
  ('nChannels', WORD),
@@ -198,13 +215,13 @@ class IXAudio2VoiceCallback(com.Interface):
198
215
  ('OnStreamEnd',
199
216
  com.VOIDMETHOD()),
200
217
  ('OnBufferStart',
201
- com.VOIDMETHOD(ctypes.c_void_p)),
218
+ com.VOIDMETHOD(c_void_p)),
202
219
  ('OnBufferEnd',
203
- com.VOIDMETHOD(ctypes.c_void_p)),
220
+ com.VOIDMETHOD(c_void_p)),
204
221
  ('OnLoopEnd',
205
- com.VOIDMETHOD(ctypes.c_void_p)),
222
+ com.VOIDMETHOD(c_void_p)),
206
223
  ('OnVoiceError',
207
- com.VOIDMETHOD(ctypes.c_void_p, HRESULT))
224
+ com.VOIDMETHOD(c_void_p, HRESULT))
208
225
  ]
209
226
 
210
227
 
@@ -216,7 +233,7 @@ class XAUDIO2_EFFECT_DESCRIPTOR(Structure):
216
233
  ]
217
234
 
218
235
 
219
- class XAUDIO2_EFFECT_CHAIN(ctypes.Structure):
236
+ class XAUDIO2_EFFECT_CHAIN(Structure):
220
237
  _fields_ = [
221
238
  ('EffectCount', UINT32),
222
239
  ('pEffectDescriptors', POINTER(XAUDIO2_EFFECT_DESCRIPTOR)),
@@ -331,21 +348,21 @@ class IXAudio2EngineCallback(com.Interface):
331
348
 
332
349
 
333
350
  # -------------- 3D Audio Positioning----------
334
- class X3DAUDIO_DISTANCE_CURVE_POINT(ctypes.Structure):
351
+ class X3DAUDIO_DISTANCE_CURVE_POINT(Structure):
335
352
  _fields_ = [
336
353
  ('Distance', FLOAT32),
337
354
  ('DSPSetting', FLOAT32)
338
355
  ]
339
356
 
340
357
 
341
- class X3DAUDIO_DISTANCE_CURVE(ctypes.Structure):
358
+ class X3DAUDIO_DISTANCE_CURVE(Structure):
342
359
  _fields_ = [
343
360
  ('pPoints', POINTER(X3DAUDIO_DISTANCE_CURVE_POINT)),
344
361
  ('PointCount', UINT32)
345
362
  ]
346
363
 
347
364
 
348
- class X3DAUDIO_VECTOR(ctypes.Structure):
365
+ class X3DAUDIO_VECTOR(Structure):
349
366
  _fields_ = [
350
367
  ('x', c_float),
351
368
  ('y', c_float),
pyglet/shapes.py CHANGED
@@ -814,7 +814,7 @@ class Arc(ShapeBase):
814
814
  The angle of the arc, in degrees. Defaults to 360.0, which is
815
815
  a full circle.
816
816
  start_angle:
817
- The start angle of the arc, in radians. Defaults to 0.
817
+ The start angle of the arc, in degrees. Defaults to 0.
818
818
  closed:
819
819
  If ``True``, the ends of the arc will be connected with a line.
820
820
  defaults to ``False``.
pyglet/text/document.py CHANGED
@@ -247,7 +247,7 @@ class AbstractDocument(event.EventDispatcher):
247
247
  self._text = ""
248
248
  self._elements: list[InlineElement] = []
249
249
  if text:
250
- self.append_text(text)
250
+ self.insert_text(0, text)
251
251
 
252
252
  @property
253
253
  def text(self) -> str:
@@ -384,24 +384,6 @@ class AbstractDocument(event.EventDispatcher):
384
384
  if element._position >= start: # noqa: SLF001
385
385
  element._position += len_text # noqa: SLF001
386
386
 
387
- def append_text(self, text: str, attributes: dict[str, Any] | None = None) -> None:
388
- """Append text into the end of document.
389
-
390
- Dispatches an :py:meth:`~pyglet.text.document.AbstractDocument.on_insert_text` event.
391
-
392
- Args:
393
- text:
394
- Text to append.
395
- attributes:
396
- Optional dictionary giving named style attributes of the appended text.
397
- """ # noqa: D411, D405, D214, D410
398
- start = len(self._text)
399
- self._append_text(text, attributes)
400
- self.dispatch_event("on_insert_text", start, text)
401
-
402
- def _append_text(self, text: str, attributes: dict[str, Any] | None) -> None:
403
- self._text += text
404
-
405
387
  def delete_text(self, start: int, end: int) -> None:
406
388
  """Delete text from the document.
407
389
 
@@ -645,49 +627,21 @@ class FormattedDocument(AbstractDocument):
645
627
  def get_element_runs(self) -> _ElementIterator:
646
628
  return _ElementIterator(self._elements, len(self._text))
647
629
 
648
- def _insert_text(self, start: int, text: str, attributes: dict[str, Any] | None) -> None:
630
+ def _insert_text(self, start: int, text: str, attributes: dict[str, Any]) -> None:
649
631
  super()._insert_text(start, text, attributes)
650
632
 
651
633
  len_text = len(text)
652
- if attributes is None:
653
- for runs in self._style_runs.values():
654
- runs.insert(start, len_text)
655
-
656
- else:
657
- for name, runs in self._style_runs.items():
658
- if name not in attributes:
659
- runs.insert(start, len_text)
660
-
661
- for attribute, value in attributes.items():
662
- try:
663
- runs = self._style_runs[attribute]
664
- except KeyError:
665
- runs = self._style_runs[attribute] = runlist.RunList(0, None)
666
- runs.append(len(self.text))
667
- runs.set_run(start, start+len_text, value)
668
- else:
669
- runs.insert_run(start, len_text, value)
670
-
671
- def _append_text(self, text: str, attributes: dict[str, Any] | None) -> None:
672
- super()._append_text(text, attributes)
673
-
674
- len_text = len(text)
675
- if attributes is None:
676
- for runs in self._style_runs.values():
677
- runs.append(len_text)
678
-
679
- else:
680
- for name, runs in self._style_runs.items():
681
- if name not in attributes:
682
- runs.append(len_text)
634
+ for runs in self._style_runs.values():
635
+ runs.insert(start, len_text)
683
636
 
637
+ if attributes is not None:
684
638
  for attribute, value in attributes.items():
685
639
  try:
686
640
  runs = self._style_runs[attribute]
687
641
  except KeyError:
688
642
  runs = self._style_runs[attribute] = runlist.RunList(0, None)
689
- runs.append(len(self.text) - len_text)
690
- runs.append_run(len_text, value)
643
+ runs.insert(0, len(self.text))
644
+ runs.set_run(start, start + len_text, value)
691
645
 
692
646
  def _delete_text(self, start: int, end: int) -> None:
693
647
  super()._delete_text(start, end)
@@ -30,6 +30,7 @@ class AttributedTextDecoder(pyglet.text.DocumentDecoder): # noqa: D101
30
30
 
31
31
  def __init__(self) -> None: # noqa: D107
32
32
  self.doc = pyglet.text.document.FormattedDocument()
33
+ self.length = 0
33
34
  self.attributes = {}
34
35
 
35
36
  def decode(self, text: str, location: Location | None = None) -> pyglet.text.document.FormattedDocument: # noqa: ARG002
@@ -77,5 +78,6 @@ class AttributedTextDecoder(pyglet.text.DocumentDecoder): # noqa: D101
77
78
  return self.doc
78
79
 
79
80
  def append(self, text: str) -> None:
80
- self.doc.append_text(text, self.attributes)
81
+ self.doc.insert_text(self.length, text, self.attributes)
82
+ self.length += len(text)
81
83
  self.attributes.clear()
@@ -13,5 +13,5 @@ if TYPE_CHECKING:
13
13
  class PlainTextDecoder(pyglet.text.DocumentDecoder): # noqa: D101
14
14
  def decode(self, text: str, location: Location | None=None) -> UnformattedDocument: # noqa: ARG002
15
15
  document = pyglet.text.document.UnformattedDocument()
16
- document.append_text(text)
16
+ document.insert_text(0, text)
17
17
  return document
@@ -315,7 +315,7 @@ class StructuredTextDecoder(pyglet.text.DocumentDecoder): # noqa: D101
315
315
  break
316
316
 
317
317
  def add_text(self, text: str) -> None:
318
- self.document.append_text(text, self.next_style)
318
+ self.document.insert_text(self.len_text, text, self.next_style)
319
319
  self.next_style.clear()
320
320
  self.len_text += len(text)
321
321
 
@@ -33,6 +33,7 @@ from pyglet.gl import (
33
33
  )
34
34
  from pyglet.graphics import Group
35
35
  from pyglet.text import runlist
36
+ from pyglet.font.base import GlyphPosition
36
37
 
37
38
  if TYPE_CHECKING:
38
39
  from pyglet.customtypes import AnchorX, AnchorY, ContentVAlign, HorizontalAlign
@@ -102,7 +103,7 @@ layout_fragment_source = """#version 330 core
102
103
 
103
104
  void main()
104
105
  {
105
- final_colors = vec4(text_colors.rgb, texture(text, texture_coords).a * text_colors.a);
106
+ final_colors = texture(text, texture_coords) * text_colors;
106
107
  if (scissor == true) {
107
108
  if (vert_position.x < scissor_area[0]) discard; // left
108
109
  if (vert_position.y < scissor_area[1]) discard; // bottom
@@ -410,25 +411,26 @@ class _AbstractBox(ABC):
410
411
  class _GlyphBox(_AbstractBox):
411
412
  owner: Texture
412
413
  font: Font
413
- glyphs: list[tuple[int, Glyph]]
414
+ glyphs: list[tuple[int, Glyph, GlyphPosition]]
414
415
  advance: int
415
416
  vertex_lists: list[_LayoutVertexList]
416
417
 
417
- def __init__(self, owner: Texture, font: Font, glyphs: list[tuple[int, Glyph]], advance: int) -> None:
418
+ def __init__(self, owner: Texture, font: Font, glyphs: list[tuple[int, Glyph, GlyphPosition]], advance: int) -> None:
418
419
  """Create a run of glyphs sharing the same texture.
419
420
 
420
- :Parameters:
421
- `owner` : `pyglet.image.Texture`
421
+ Args:
422
+ owner:
422
423
  Texture of all glyphs in this run.
423
- `font` : `pyglet.font.base.Font`
424
+ font:
424
425
  Font of all glyphs in this run.
425
- `glyphs` : list of (int, `pyglet.font.base.Glyph`)
426
+ glyphs:
426
427
  Pairs of ``(kern, glyph)``, where ``kern`` gives horizontal
427
428
  displacement of the glyph in pixels (typically 0).
428
- `advance` : int
429
+ advance:
429
430
  Width of glyph run; must correspond to the sum of advances
430
431
  and kerns in the glyph list.
431
-
432
+ offsets:
433
+ A list of all position transformations done to each glyph.
432
434
  """
433
435
  super().__init__(font.ascent, font.descent, advance, len(glyphs))
434
436
  assert owner
@@ -446,7 +448,7 @@ class _GlyphBox(_AbstractBox):
446
448
  rotation: float, visible: bool, anchor_x: float, anchor_y: float, context: _LayoutContext) -> None:
447
449
  # Creates the initial attributes and vertex lists of the glyphs.
448
450
  # line_x/line_y are calculated when lines shift. To prevent having to destroy and recalculate the layout
449
- # everytime we move this layout, we bake those into the vertices. This way the translate can be moved directly.
451
+ # everytime it moves, they are merged into the vertices. This way the translation can be moved directly.
450
452
  assert self.glyphs
451
453
  assert not self.vertex_lists
452
454
  try:
@@ -463,17 +465,19 @@ class _GlyphBox(_AbstractBox):
463
465
  for start, end, baseline_ in context.baseline_iter.ranges(i, i + n_glyphs):
464
466
  baseline = layout._parse_distance(baseline_) # noqa: SLF001
465
467
  assert len(self.glyphs[start - i:end - i]) == end - start
466
- for kern, glyph in self.glyphs[start - i:end - i]:
468
+ for (kern, glyph, glyph_pos) in self.glyphs[start - i:end - i]:
467
469
  x1 += kern
468
470
  v0, v1, v2, v3 = glyph.vertices
469
- v0 += x1
470
- v2 += x1
471
- v1 += line_y + baseline
472
- v3 += line_y + baseline
471
+ v0 += x1 + glyph_pos.x_offset
472
+ v2 += x1 + glyph_pos.x_offset
473
+ v1 += line_y + baseline + glyph_pos.y_offset
474
+ v3 += line_y + baseline + glyph_pos.y_offset
473
475
  vertices.extend(map(round, [v0, v1, 0, v2, v1, 0, v2, v3, 0, v0, v3, 0]))
474
476
  t = glyph.tex_coords
475
477
  tex_coords.extend(t)
476
- x1 += glyph.advance
478
+ x1 += glyph.advance + glyph_pos.x_advance
479
+ v1 += glyph_pos.y_advance
480
+ v3 += glyph_pos.y_advance
477
481
 
478
482
  # Text color
479
483
  colors = []
@@ -519,8 +523,8 @@ class _GlyphBox(_AbstractBox):
519
523
  for start, end, decoration in context.decoration_iter.ranges(i, i + n_glyphs):
520
524
  bg, underline = decoration
521
525
  x2 = x1
522
- for kern, glyph in self.glyphs[start - i:end - i]:
523
- x2 += glyph.advance + kern
526
+ for (kern, glyph, glyph_pos) in self.glyphs[start - i:end - i]:
527
+ x2 += glyph.advance + kern + glyph_pos.x_advance
524
528
 
525
529
  if bg is not None:
526
530
  if len(bg) != 4:
@@ -619,19 +623,19 @@ class _GlyphBox(_AbstractBox):
619
623
 
620
624
  def get_point_in_box(self, position: int) -> int:
621
625
  x = 0
622
- for (kern, glyph) in self.glyphs:
626
+ for (kern, glyph, offset) in self.glyphs:
623
627
  if position == 0:
624
628
  break
625
629
  position -= 1
626
- x += glyph.advance + kern
630
+ x += glyph.advance + kern + offset.x_advance
627
631
  return x
628
632
 
629
633
  def get_position_in_box(self, x: float) -> int:
630
634
  position = 0
631
635
  last_glyph_x = 0
632
- for kern, glyph in self.glyphs:
636
+ for (kern, glyph, offset) in self.glyphs:
633
637
  last_glyph_x += kern
634
- if last_glyph_x + glyph.advance // 2 > x:
638
+ if last_glyph_x + glyph.advance + offset.x_advance // 2 > x:
635
639
  return position
636
640
  position += 1
637
641
  last_glyph_x += glyph.advance
@@ -813,6 +817,9 @@ class TextDecorationGroup(Group):
813
817
  self.program.stop()
814
818
 
815
819
 
820
+ # Just have one object for empty positions in layout. It won't be modified.
821
+ _empty_pos = GlyphPosition(0, 0, 0, 0)
822
+
816
823
  class TextLayout:
817
824
  """Lay out and display documents.
818
825
 
@@ -978,11 +985,6 @@ class TextLayout:
978
985
  self.group_cache.clear()
979
986
  self._update()
980
987
 
981
- @property
982
- def dpi(self) -> float:
983
- """Get DPI used by this layout."""
984
- return self._dpi
985
-
986
988
  @property
987
989
  def document(self) -> AbstractDocument:
988
990
  """Document to display.
@@ -1370,17 +1372,12 @@ class TextLayout:
1370
1372
  self._update()
1371
1373
 
1372
1374
  @property
1373
- def dpi(self):
1374
- """Get DPI used by this layout.
1375
-
1376
- Read-only.
1377
-
1378
- :type: float
1379
- """
1375
+ def dpi(self) -> float:
1376
+ """Get DPI used by this layout."""
1380
1377
  return self._dpi
1381
1378
 
1382
1379
  @dpi.setter
1383
- def dpi(self, value):
1380
+ def dpi(self, value: float) -> None:
1384
1381
  self._dpi = value
1385
1382
  self._update()
1386
1383
 
@@ -1438,9 +1435,10 @@ class TextLayout:
1438
1435
 
1439
1436
  def _get_lines(self) -> list[_Line]:
1440
1437
  len_text = len(self._document.text)
1441
- glyphs = self._get_glyphs()
1442
- owner_runs = self._get_owner_runs(glyphs)
1443
- lines = [line for line in self._flow_glyphs(glyphs, owner_runs, 0, len_text)]
1438
+ glyphs, offsets = self._get_glyphs()
1439
+ owner_runs = runlist.RunList(len_text, None)
1440
+ self._get_owner_runs(owner_runs, glyphs, 0, len_text)
1441
+ lines = list(self._flow_glyphs(glyphs, offsets, owner_runs, 0, len_text))
1444
1442
  self._content_width = 0
1445
1443
  self._line_count = len(lines)
1446
1444
  self._flow_lines(lines, 0, self._line_count)
@@ -1620,8 +1618,9 @@ class TextLayout:
1620
1618
  else:
1621
1619
  self._init_document()
1622
1620
 
1623
- def _get_glyphs(self) -> list[_InlineElementBox | Glyph]:
1621
+ def _get_glyphs(self) -> tuple[list[_InlineElementBox | Glyph], list[tuple[int, int]]]:
1624
1622
  glyphs = []
1623
+ offsets = []
1625
1624
  runs = runlist.ZipRunIterator((
1626
1625
  self._document.get_font_runs(dpi=self._dpi),
1627
1626
  self._document.get_element_runs()))
@@ -1629,28 +1628,33 @@ class TextLayout:
1629
1628
  for start, end, (font, element) in runs.ranges(0, len(text)):
1630
1629
  if element:
1631
1630
  glyphs.append(_InlineElementBox(element))
1631
+ offsets.append(_empty_pos)
1632
1632
  else:
1633
- glyphs.extend(font.get_glyphs(text[start:end]))
1634
- return glyphs
1633
+ char_glyphs, char_offsets = font.get_glyphs(text[start:end])
1634
+ glyphs.extend(char_glyphs)
1635
+ offsets.extend(char_offsets)
1636
+
1637
+ return glyphs, offsets
1635
1638
 
1636
- def _get_owner_runs(self, glyphs: list[_InlineElementBox | Glyph]) -> runlist.RunList:
1637
- owner = glyphs[0].owner
1638
- run_start = 0
1639
- owner_runs = runlist.RunList(0, owner)
1639
+ def _get_owner_runs(self, owner_runs: runlist.RunList, glyphs: list[_InlineElementBox | Glyph], start: int,
1640
+ end: int) -> None:
1641
+ owner = glyphs[start].owner
1642
+ run_start = start
1640
1643
 
1641
- for i, glyph in enumerate(glyphs):
1644
+ # TODO avoid glyph slice on non-incremental
1645
+ for i, glyph in enumerate(glyphs[start:end]):
1642
1646
  if owner != glyph.owner:
1643
- owner_runs.append_run(i-run_start, owner)
1647
+ owner_runs.set_run(run_start, i + start, owner)
1644
1648
  owner = glyph.owner
1645
- run_start = i
1646
- owner_runs.append_run(len(glyphs)-run_start, owner)
1647
- return owner_runs
1649
+ run_start = i + start
1650
+ owner_runs.set_run(run_start, end, owner)
1648
1651
 
1649
- def _flow_glyphs_wrap(self, glyphs: list[_InlineElementBox | Glyph], owner_runs: runlist.RunList, start: int,
1652
+ def _flow_glyphs_wrap(self, glyphs: list[_InlineElementBox | Glyph],
1653
+ offsets: list[GlyphPosition],
1654
+ owner_runs: runlist.RunList, start: int,
1650
1655
  end: int) -> Iterator[_Line]:
1651
1656
  # Word-wrap styled text into lines of fixed width.
1652
1657
  # Fits glyphs in range start to end into Lines which are then yielded.
1653
-
1654
1658
  owner_iterator = owner_runs.get_run_iterator().ranges(start, end)
1655
1659
 
1656
1660
  font_iterator = self._document.get_font_runs(dpi=self._dpi)
@@ -1721,7 +1725,7 @@ class TextLayout:
1721
1725
  # Iterate over glyphs in this owner run. `text` is the
1722
1726
  # corresponding character data for the glyph, and is used to find
1723
1727
  # whitespace and newlines.
1724
- for (text, glyph) in zip(self.document.text[start:end], glyphs[start:end]):
1728
+ for (text, glyph, offset) in zip(self.document.text[start:end], glyphs[start:end], offsets[start:end]):
1725
1729
  if nokern:
1726
1730
  kern = 0
1727
1731
  nokern = False
@@ -1747,15 +1751,15 @@ class TextLayout:
1747
1751
  tab_stop = (((x + line.margin_left) // tab) + 1) * tab
1748
1752
  kern = int(tab_stop - x - line.margin_left - glyph.advance)
1749
1753
 
1750
- owner_accum.append((kern, glyph))
1754
+ owner_accum.append((kern, glyph, offset))
1751
1755
  owner_accum_commit.extend(owner_accum)
1752
- owner_accum_commit_width += owner_accum_width + glyph.advance + kern
1753
- eol_ws += glyph.advance + kern
1756
+ owner_accum_commit_width += owner_accum_width + glyph.advance + kern + offset.x_advance
1757
+ eol_ws += glyph.advance + kern + offset.x_advance
1754
1758
 
1755
1759
  owner_accum = []
1756
1760
  owner_accum_width = 0
1757
1761
 
1758
- x += glyph.advance + kern
1762
+ x += glyph.advance + kern + offset.x_advance
1759
1763
  index += 1
1760
1764
 
1761
1765
  # The index at which the next line will begin (the
@@ -1765,7 +1769,7 @@ class TextLayout:
1765
1769
  else:
1766
1770
  new_paragraph = text in "\n\u2029"
1767
1771
  new_line = (text == "\u2028") or new_paragraph
1768
- if (wrap and self._wrap_lines and x + kern + glyph.advance >= width) or new_line:
1772
+ if (wrap and self._wrap_lines and x + kern + glyph.advance + offset.x_advance >= width) or new_line:
1769
1773
  # Either the pending runs have overflowed the allowed
1770
1774
  # line width or a newline was encountered. Either
1771
1775
  # way, the current line must be flushed.
@@ -1827,11 +1831,11 @@ class TextLayout:
1827
1831
  # Remove kern from first glyph of line
1828
1832
  if run_accum and hasattr(run_accum, "glyphs") and run_accum.glyphs:
1829
1833
  k, g = run_accum[0].glyphs[0]
1830
- run_accum[0].glyphs[0] = (0, g)
1834
+ run_accum[0].glyphs[0] = (0, g, _empty_pos)
1831
1835
  run_accum_width -= k
1832
1836
  elif owner_accum:
1833
- k, g = owner_accum[0]
1834
- owner_accum[0] = (0, g)
1837
+ k, g, _ = owner_accum[0]
1838
+ owner_accum[0] = (0, g, _empty_pos)
1835
1839
  owner_accum_width -= k
1836
1840
  else:
1837
1841
  nokern = True
@@ -1843,8 +1847,8 @@ class TextLayout:
1843
1847
  if isinstance(glyph, _AbstractBox):
1844
1848
  # Glyph is already in a box. XXX Ignore kern?
1845
1849
  run_accum.append(glyph)
1846
- run_accum_width += glyph.advance
1847
- x += glyph.advance
1850
+ run_accum_width += glyph.advance + offset.x_advance
1851
+ x += glyph.advance + offset.x_advance
1848
1852
  elif new_paragraph:
1849
1853
  # New paragraph started, update wrap style
1850
1854
  wrap = wrap_iterator[next_start]
@@ -1854,9 +1858,9 @@ class TextLayout:
1854
1858
  elif not new_line:
1855
1859
  # If the glyph was any non-whitespace, non-newline
1856
1860
  # character, add it to the pending run.
1857
- owner_accum.append((kern, glyph))
1858
- owner_accum_width += glyph.advance + kern
1859
- x += glyph.advance + kern
1861
+ owner_accum.append((kern, glyph, offset))
1862
+ owner_accum_width += glyph.advance + kern + offset.x_advance
1863
+ x += glyph.advance + kern + offset.x_advance
1860
1864
  index += 1
1861
1865
  eol_ws = 0
1862
1866
 
@@ -1882,7 +1886,9 @@ class TextLayout:
1882
1886
 
1883
1887
  yield line
1884
1888
 
1885
- def _flow_glyphs_single_line(self, glyphs: list[_InlineElementBox | Glyph], owner_runs: runlist.RunList,
1889
+ def _flow_glyphs_single_line(self, glyphs: list[_InlineElementBox | Glyph],
1890
+ offsets: list[GlyphPosition],
1891
+ owner_runs: runlist.RunList,
1886
1892
  start: int, end: int) -> Iterator[_Line]:
1887
1893
  owner_iterator = owner_runs.get_run_iterator().ranges(start, end)
1888
1894
  font_iterator = self.document.get_font_runs(dpi=self._dpi)
@@ -1905,9 +1911,11 @@ class TextLayout:
1905
1911
  owner_glyphs = []
1906
1912
  for kern_start, kern_end, kern in kern_iterator.ranges(start, end):
1907
1913
  gs = glyphs[kern_start:kern_end]
1914
+ os = offsets[kern_start:kern_end]
1908
1915
  width += sum([g.advance for g in gs])
1909
1916
  width += kern * (kern_end - kern_start)
1910
- owner_glyphs.extend(zip([kern] * (kern_end - kern_start), gs))
1917
+ width += sum([o.x_advance for o in os])
1918
+ owner_glyphs.extend(zip([kern] * (kern_end - kern_start), gs, os))
1911
1919
  if owner is None:
1912
1920
  # Assume glyphs are already boxes.
1913
1921
  for kern, glyph in owner_glyphs: