WinColl 0.9.6__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.
- WinColl-0.9.6.dist-info/METADATA +138 -0
- WinColl-0.9.6.dist-info/RECORD +37 -0
- WinColl-0.9.6.dist-info/WHEEL +5 -0
- WinColl-0.9.6.dist-info/entry_points.txt +2 -0
- WinColl-0.9.6.dist-info/top_level.txt +1 -0
- wincoll/Collect.wav +0 -0
- wincoll/Slide.wav +0 -0
- wincoll/Splat.wav +0 -0
- wincoll/Unlock.wav +0 -0
- wincoll/__init__.py +643 -0
- wincoll/__main__.py +7 -0
- wincoll/acorn-mode-1.ttf +0 -0
- wincoll/diamond.png +0 -0
- wincoll/langdetect.py +60 -0
- wincoll/levels/01.tmx +61 -0
- wincoll/levels/02.tmx +61 -0
- wincoll/levels/03.tmx +61 -0
- wincoll/levels/04.tmx +61 -0
- wincoll/levels/05.tmx +61 -0
- wincoll/levels/06.tmx +61 -0
- wincoll/levels/Blob.png +0 -0
- wincoll/levels/Brick.png +0 -0
- wincoll/levels/Diamond.png +0 -0
- wincoll/levels/Earth.png +0 -0
- wincoll/levels/Gap.png +0 -0
- wincoll/levels/Key.png +0 -0
- wincoll/levels/Rock.png +0 -0
- wincoll/levels/Safe.png +0 -0
- wincoll/levels/Win.png +0 -0
- wincoll/levels/WinColl.tsx +34 -0
- wincoll/locale/el/LC_MESSAGES/wincoll.mo +0 -0
- wincoll/locale/fr/LC_MESSAGES/argparse.mo +0 -0
- wincoll/locale/fr/LC_MESSAGES/wincoll.mo +0 -0
- wincoll/ptext.py +1196 -0
- wincoll/splat.png +0 -0
- wincoll/title.png +0 -0
- wincoll/warnings_util.py +29 -0
wincoll/ptext.py
ADDED
|
@@ -0,0 +1,1196 @@
|
|
|
1
|
+
# ptext module: place this in your import directory.
|
|
2
|
+
|
|
3
|
+
# ptext.draw(text, pos=None, **options)
|
|
4
|
+
|
|
5
|
+
# Please see README.md for explanation of options.
|
|
6
|
+
# https://github.com/cosmologicon/pygame-text
|
|
7
|
+
|
|
8
|
+
from __future__ import division, print_function
|
|
9
|
+
|
|
10
|
+
from math import ceil, sin, cos, radians, exp
|
|
11
|
+
from collections import namedtuple
|
|
12
|
+
import pygame
|
|
13
|
+
|
|
14
|
+
# Global default values
|
|
15
|
+
DEFAULT_FONT_SIZE = 24
|
|
16
|
+
REFERENCE_FONT_SIZE = 100
|
|
17
|
+
DEFAULT_LINE_HEIGHT = 1.0
|
|
18
|
+
DEFAULT_PARAGRAPH_SPACE = 0.0
|
|
19
|
+
DEFAULT_FONT_NAME = None
|
|
20
|
+
DEFAULT_SYSFONT_NAME = None
|
|
21
|
+
FONT_NAME_TEMPLATE = "%s"
|
|
22
|
+
DEFAULT_COLOR = "white"
|
|
23
|
+
DEFAULT_BACKGROUND = None
|
|
24
|
+
DEFAULT_SHADE = 0
|
|
25
|
+
DEFAULT_OUTLINE_WIDTH = None
|
|
26
|
+
DEFAULT_OUTLINE_COLOR = "black"
|
|
27
|
+
OUTLINE_UNIT = 1 / 24
|
|
28
|
+
DEFAULT_SHADOW_OFFSET = None
|
|
29
|
+
DEFAULT_SHADOW_COLOR = "black"
|
|
30
|
+
SHADOW_UNIT = 1 / 18
|
|
31
|
+
DEFAULT_ALIGN = "left" # left, center, or right
|
|
32
|
+
DEFAULT_ANCHOR = 0, 0 # 0, 0 = top left ; 1, 1 = bottom right
|
|
33
|
+
DEFAULT_STRIP = True
|
|
34
|
+
ALPHA_RESOLUTION = 16
|
|
35
|
+
ANGLE_RESOLUTION_DEGREES = 3
|
|
36
|
+
DEFAULT_UNDERLINE_TAG = None
|
|
37
|
+
DEFAULT_BOLD_TAG = None
|
|
38
|
+
DEFAULT_ITALIC_TAG = None
|
|
39
|
+
DEFAULT_COLOR_TAG = {}
|
|
40
|
+
|
|
41
|
+
AUTO_CLEAN = True
|
|
42
|
+
MEMORY_LIMIT_MB = 64
|
|
43
|
+
MEMORY_REDUCTION_FACTOR = 0.5
|
|
44
|
+
|
|
45
|
+
pygame.font.init()
|
|
46
|
+
|
|
47
|
+
# Options objects encapsulate the keyword arguments to functions that take a lot of optional keyword
|
|
48
|
+
# arguments.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Options object base class. Subclass for Options objects specific to different functions.
|
|
52
|
+
# Specify valid fields in the _fields list. All keyword fields are optional. Unspecified fields
|
|
53
|
+
# default to None, unless otherwise specified in the _defaults list.
|
|
54
|
+
class _Options(object):
|
|
55
|
+
_fields = ()
|
|
56
|
+
_defaults = {}
|
|
57
|
+
|
|
58
|
+
def __init__(self, **kwargs):
|
|
59
|
+
fields = self._allfields()
|
|
60
|
+
badfields = set(kwargs) - fields
|
|
61
|
+
if badfields:
|
|
62
|
+
raise ValueError("Unrecognized args: " + ", ".join(badfields))
|
|
63
|
+
for field in fields:
|
|
64
|
+
value = kwargs[field] if field in kwargs else self._defaults.get(field)
|
|
65
|
+
setattr(self, field, value)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def _allfields(cls):
|
|
69
|
+
return set(cls._fields) | set(cls._defaults)
|
|
70
|
+
|
|
71
|
+
def asdict(self):
|
|
72
|
+
return {field: getattr(self, field) for field in self._allfields()}
|
|
73
|
+
|
|
74
|
+
def copy(self):
|
|
75
|
+
return self.__class__(**self.asdict())
|
|
76
|
+
|
|
77
|
+
def keys(self):
|
|
78
|
+
return self._allfields()
|
|
79
|
+
|
|
80
|
+
def __getitem__(self, field):
|
|
81
|
+
return getattr(self, field)
|
|
82
|
+
|
|
83
|
+
def update(self, **newkwargs):
|
|
84
|
+
kwargs = self.asdict()
|
|
85
|
+
kwargs.update(**newkwargs)
|
|
86
|
+
return self.__class__(**kwargs)
|
|
87
|
+
|
|
88
|
+
# For cached function calls, this is a hashable representation of the options object. Assumes
|
|
89
|
+
# that all field values are either hashable, or dicts whose keys are comparable and values are
|
|
90
|
+
# hashable.
|
|
91
|
+
def key(self):
|
|
92
|
+
values = []
|
|
93
|
+
for field in sorted(self._allfields()):
|
|
94
|
+
value = getattr(self, field)
|
|
95
|
+
if isinstance(value, dict):
|
|
96
|
+
value = tuple(sorted(value.items()))
|
|
97
|
+
values.append(value)
|
|
98
|
+
return tuple(values)
|
|
99
|
+
|
|
100
|
+
def getsuboptions(self, optclass):
|
|
101
|
+
return {
|
|
102
|
+
field: getattr(self, field)
|
|
103
|
+
for field in optclass._allfields()
|
|
104
|
+
if hasattr(self, field)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# The following methods are just put here for code deduplication. A couple different functions
|
|
108
|
+
# use a lot of the same code.
|
|
109
|
+
def resolvetags(self):
|
|
110
|
+
if self.underlinetag is _default_sentinel:
|
|
111
|
+
self.underlinetag = DEFAULT_UNDERLINE_TAG
|
|
112
|
+
if self.boldtag is _default_sentinel:
|
|
113
|
+
self.boldtag = DEFAULT_BOLD_TAG
|
|
114
|
+
if self.italictag is _default_sentinel:
|
|
115
|
+
self.italictag = DEFAULT_ITALIC_TAG
|
|
116
|
+
if self.colortag is _default_sentinel:
|
|
117
|
+
self.colortag = DEFAULT_COLOR_TAG
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# Used as the default value for any argument for which (1) None is a valid value, and (2) there's a
|
|
121
|
+
# global default value.
|
|
122
|
+
_default_sentinel = ()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# Options argument for the draw function. Specifies both text styling and positioning.
|
|
126
|
+
class _DrawOptions(_Options):
|
|
127
|
+
_fields = (
|
|
128
|
+
"pos",
|
|
129
|
+
"fontname",
|
|
130
|
+
"fontsize",
|
|
131
|
+
"sysfontname",
|
|
132
|
+
"antialias",
|
|
133
|
+
"bold",
|
|
134
|
+
"italic",
|
|
135
|
+
"underline",
|
|
136
|
+
"color",
|
|
137
|
+
"background",
|
|
138
|
+
"top",
|
|
139
|
+
"left",
|
|
140
|
+
"bottom",
|
|
141
|
+
"right",
|
|
142
|
+
"topleft",
|
|
143
|
+
"bottomleft",
|
|
144
|
+
"topright",
|
|
145
|
+
"bottomright",
|
|
146
|
+
"midtop",
|
|
147
|
+
"midleft",
|
|
148
|
+
"midbottom",
|
|
149
|
+
"midright",
|
|
150
|
+
"center",
|
|
151
|
+
"centerx",
|
|
152
|
+
"centery",
|
|
153
|
+
"width",
|
|
154
|
+
"widthem",
|
|
155
|
+
"lineheight",
|
|
156
|
+
"pspace",
|
|
157
|
+
"strip",
|
|
158
|
+
"align",
|
|
159
|
+
"owidth",
|
|
160
|
+
"ocolor",
|
|
161
|
+
"shadow",
|
|
162
|
+
"scolor",
|
|
163
|
+
"gcolor",
|
|
164
|
+
"shade",
|
|
165
|
+
"alpha",
|
|
166
|
+
"anchor",
|
|
167
|
+
"angle",
|
|
168
|
+
"underlinetag",
|
|
169
|
+
"boldtag",
|
|
170
|
+
"italictag",
|
|
171
|
+
"colortag",
|
|
172
|
+
"surf",
|
|
173
|
+
"cache",
|
|
174
|
+
)
|
|
175
|
+
_defaults = {
|
|
176
|
+
"fontname": _default_sentinel,
|
|
177
|
+
"sysfontname": _default_sentinel,
|
|
178
|
+
"antialias": True,
|
|
179
|
+
"alpha": 1.0,
|
|
180
|
+
"angle": 0,
|
|
181
|
+
"owidth": _default_sentinel,
|
|
182
|
+
"shadow": _default_sentinel,
|
|
183
|
+
"underlinetag": _default_sentinel,
|
|
184
|
+
"boldtag": _default_sentinel,
|
|
185
|
+
"italictag": _default_sentinel,
|
|
186
|
+
"colortag": _default_sentinel,
|
|
187
|
+
"surf": _default_sentinel,
|
|
188
|
+
"cache": True,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
def __init__(self, **kwargs):
|
|
192
|
+
_Options.__init__(self, **kwargs)
|
|
193
|
+
self.expandposition()
|
|
194
|
+
self.expandanchor()
|
|
195
|
+
self.resolvesurf()
|
|
196
|
+
|
|
197
|
+
# Expand each 2-element position specifier and overwrite the corresponding 1-element
|
|
198
|
+
# position specifiers.
|
|
199
|
+
def expandposition(self):
|
|
200
|
+
if self.topleft:
|
|
201
|
+
self.left, self.top = self.topleft
|
|
202
|
+
if self.bottomleft:
|
|
203
|
+
self.left, self.bottom = self.bottomleft
|
|
204
|
+
if self.topright:
|
|
205
|
+
self.right, self.top = self.topright
|
|
206
|
+
if self.bottomright:
|
|
207
|
+
self.right, self.bottom = self.bottomright
|
|
208
|
+
if self.midtop:
|
|
209
|
+
self.centerx, self.top = self.midtop
|
|
210
|
+
if self.midleft:
|
|
211
|
+
self.left, self.centery = self.midleft
|
|
212
|
+
if self.midbottom:
|
|
213
|
+
self.centerx, self.bottom = self.midbottom
|
|
214
|
+
if self.midright:
|
|
215
|
+
self.right, self.centery = self.midright
|
|
216
|
+
if self.center:
|
|
217
|
+
self.centerx, self.centery = self.center
|
|
218
|
+
|
|
219
|
+
# Update the pos and anchor fields, if unspecified, to be specified by the positional
|
|
220
|
+
# keyword arguments.
|
|
221
|
+
def expandanchor(self):
|
|
222
|
+
x, y = self.pos or (None, None)
|
|
223
|
+
hanchor, vanchor = self.anchor or (None, None)
|
|
224
|
+
if self.left is not None:
|
|
225
|
+
x, hanchor = self.left, 0
|
|
226
|
+
if self.centerx is not None:
|
|
227
|
+
x, hanchor = self.centerx, 0.5
|
|
228
|
+
if self.right is not None:
|
|
229
|
+
x, hanchor = self.right, 1
|
|
230
|
+
if self.top is not None:
|
|
231
|
+
y, vanchor = self.top, 0
|
|
232
|
+
if self.centery is not None:
|
|
233
|
+
y, vanchor = self.centery, 0.5
|
|
234
|
+
if self.bottom is not None:
|
|
235
|
+
y, vanchor = self.bottom, 1
|
|
236
|
+
if x is None:
|
|
237
|
+
raise ValueError("Unable to determine horizontal position")
|
|
238
|
+
if y is None:
|
|
239
|
+
raise ValueError("Unable to determine vertical position")
|
|
240
|
+
self.pos = x, y
|
|
241
|
+
|
|
242
|
+
if self.align is None:
|
|
243
|
+
self.align = hanchor
|
|
244
|
+
if hanchor is None:
|
|
245
|
+
hanchor = DEFAULT_ANCHOR[0]
|
|
246
|
+
if vanchor is None:
|
|
247
|
+
vanchor = DEFAULT_ANCHOR[1]
|
|
248
|
+
self.anchor = hanchor, vanchor
|
|
249
|
+
|
|
250
|
+
# Unspecified surf values default to the display surface.
|
|
251
|
+
def resolvesurf(self):
|
|
252
|
+
if self.surf is _default_sentinel:
|
|
253
|
+
self.surf = pygame.display.get_surface()
|
|
254
|
+
|
|
255
|
+
def togetsurfoptions(self):
|
|
256
|
+
return self.getsuboptions(_GetsurfOptions)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# Options for the layout function. By design, this has the same options as draw, although some of
|
|
260
|
+
# them are silently ignored.
|
|
261
|
+
class _LayoutOptions(_DrawOptions):
|
|
262
|
+
def __init__(self, **kwargs):
|
|
263
|
+
_Options.__init__(self, **kwargs)
|
|
264
|
+
self.expandposition()
|
|
265
|
+
self.expandanchor()
|
|
266
|
+
if self.lineheight is None:
|
|
267
|
+
self.lineheight = DEFAULT_LINE_HEIGHT
|
|
268
|
+
if self.pspace is None:
|
|
269
|
+
self.pspace = DEFAULT_PARAGRAPH_SPACE
|
|
270
|
+
self.resolvetags()
|
|
271
|
+
|
|
272
|
+
def towrapoptions(self):
|
|
273
|
+
return self.getsuboptions(_WrapOptions)
|
|
274
|
+
|
|
275
|
+
def togetfontoptions(self):
|
|
276
|
+
return self.getsuboptions(_GetfontOptions)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class _DrawboxOptions(_Options):
|
|
280
|
+
_fields = (
|
|
281
|
+
"fontname",
|
|
282
|
+
"sysfontname",
|
|
283
|
+
"antialias",
|
|
284
|
+
"bold",
|
|
285
|
+
"italic",
|
|
286
|
+
"underline",
|
|
287
|
+
"color",
|
|
288
|
+
"background",
|
|
289
|
+
"lineheight",
|
|
290
|
+
"pspace",
|
|
291
|
+
"strip",
|
|
292
|
+
"align",
|
|
293
|
+
"owidth",
|
|
294
|
+
"ocolor",
|
|
295
|
+
"shadow",
|
|
296
|
+
"scolor",
|
|
297
|
+
"gcolor",
|
|
298
|
+
"shade",
|
|
299
|
+
"underlinetag",
|
|
300
|
+
"boldtag",
|
|
301
|
+
"italictag",
|
|
302
|
+
"colortag",
|
|
303
|
+
"alpha",
|
|
304
|
+
"anchor",
|
|
305
|
+
"angle",
|
|
306
|
+
"surf",
|
|
307
|
+
"cache",
|
|
308
|
+
)
|
|
309
|
+
_defaults = {
|
|
310
|
+
"fontname": _default_sentinel,
|
|
311
|
+
"sysfontname": _default_sentinel,
|
|
312
|
+
"antialias": True,
|
|
313
|
+
"alpha": 1.0,
|
|
314
|
+
"angle": 0,
|
|
315
|
+
"anchor": (0.5, 0.5),
|
|
316
|
+
"owidth": _default_sentinel,
|
|
317
|
+
"shadow": _default_sentinel,
|
|
318
|
+
"underlinetag": _default_sentinel,
|
|
319
|
+
"boldtag": _default_sentinel,
|
|
320
|
+
"italictag": _default_sentinel,
|
|
321
|
+
"colortag": _default_sentinel,
|
|
322
|
+
"surf": _default_sentinel,
|
|
323
|
+
"cache": True,
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
def __init__(self, **kwargs):
|
|
327
|
+
_Options.__init__(self, **kwargs)
|
|
328
|
+
if self.fontname is _default_sentinel:
|
|
329
|
+
self.fontname = DEFAULT_FONT_NAME
|
|
330
|
+
if self.sysfontname is _default_sentinel:
|
|
331
|
+
self.sysfontname = DEFAULT_SYSFONT_NAME
|
|
332
|
+
if self.lineheight is None:
|
|
333
|
+
self.lineheight = DEFAULT_LINE_HEIGHT
|
|
334
|
+
if self.pspace is None:
|
|
335
|
+
self.pspace = DEFAULT_PARAGRAPH_SPACE
|
|
336
|
+
|
|
337
|
+
def todrawoptions(self):
|
|
338
|
+
return self.getsuboptions(_DrawOptions)
|
|
339
|
+
|
|
340
|
+
def tofitsizeoptions(self):
|
|
341
|
+
return self.getsuboptions(_FitsizeOptions)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class _GetsurfOptions(_Options):
|
|
345
|
+
_fields = (
|
|
346
|
+
"fontname",
|
|
347
|
+
"fontsize",
|
|
348
|
+
"sysfontname",
|
|
349
|
+
"bold",
|
|
350
|
+
"italic",
|
|
351
|
+
"underline",
|
|
352
|
+
"width",
|
|
353
|
+
"widthem",
|
|
354
|
+
"strip",
|
|
355
|
+
"color",
|
|
356
|
+
"background",
|
|
357
|
+
"antialias",
|
|
358
|
+
"ocolor",
|
|
359
|
+
"owidth",
|
|
360
|
+
"scolor",
|
|
361
|
+
"shadow",
|
|
362
|
+
"gcolor",
|
|
363
|
+
"shade",
|
|
364
|
+
"alpha",
|
|
365
|
+
"align",
|
|
366
|
+
"lineheight",
|
|
367
|
+
"pspace",
|
|
368
|
+
"angle",
|
|
369
|
+
"underlinetag",
|
|
370
|
+
"boldtag",
|
|
371
|
+
"italictag",
|
|
372
|
+
"colortag",
|
|
373
|
+
"cache",
|
|
374
|
+
)
|
|
375
|
+
_defaults = {
|
|
376
|
+
"fontname": _default_sentinel,
|
|
377
|
+
"sysfontname": _default_sentinel,
|
|
378
|
+
"antialias": True,
|
|
379
|
+
"alpha": 1.0,
|
|
380
|
+
"angle": 0,
|
|
381
|
+
"owidth": _default_sentinel,
|
|
382
|
+
"shadow": _default_sentinel,
|
|
383
|
+
"underlinetag": _default_sentinel,
|
|
384
|
+
"boldtag": _default_sentinel,
|
|
385
|
+
"italictag": _default_sentinel,
|
|
386
|
+
"colortag": _default_sentinel,
|
|
387
|
+
"cache": True,
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
def __init__(self, **kwargs):
|
|
391
|
+
_Options.__init__(self, **kwargs)
|
|
392
|
+
if self.fontname is _default_sentinel:
|
|
393
|
+
self.fontname = DEFAULT_FONT_NAME
|
|
394
|
+
if self.sysfontname is _default_sentinel:
|
|
395
|
+
self.sysfontname = DEFAULT_SYSFONT_NAME
|
|
396
|
+
if self.fontsize is None:
|
|
397
|
+
self.fontsize = DEFAULT_FONT_SIZE
|
|
398
|
+
self.fontsize = int(round(self.fontsize))
|
|
399
|
+
if self.align is None:
|
|
400
|
+
self.align = DEFAULT_ALIGN
|
|
401
|
+
if self.align in ["left", "center", "right"]:
|
|
402
|
+
self.align = [0, 0.5, 1][["left", "center", "right"].index(self.align)]
|
|
403
|
+
if self.lineheight is None:
|
|
404
|
+
self.lineheight = DEFAULT_LINE_HEIGHT
|
|
405
|
+
if self.pspace is None:
|
|
406
|
+
self.pspace = DEFAULT_PARAGRAPH_SPACE
|
|
407
|
+
self.color = _resolvecolor(self.color, DEFAULT_COLOR)
|
|
408
|
+
self.background = _resolvecolor(self.background, DEFAULT_BACKGROUND)
|
|
409
|
+
self.gcolor = _resolvecolor(self.gcolor, None)
|
|
410
|
+
if self.shade is None:
|
|
411
|
+
self.shade = DEFAULT_SHADE
|
|
412
|
+
if self.shade:
|
|
413
|
+
self.gcolor = _applyshade(self.gcolor or self.color, self.shade)
|
|
414
|
+
self.shade = 0
|
|
415
|
+
self.resolveoutlineshadow()
|
|
416
|
+
self.alpha = _resolvealpha(self.alpha)
|
|
417
|
+
self.angle = _resolveangle(self.angle)
|
|
418
|
+
self.strip = DEFAULT_STRIP if self.strip is None else self.strip
|
|
419
|
+
self.resolvetags()
|
|
420
|
+
|
|
421
|
+
def resolveoutlineshadow(self):
|
|
422
|
+
if self.owidth is _default_sentinel:
|
|
423
|
+
self.owidth = DEFAULT_OUTLINE_WIDTH
|
|
424
|
+
if self.shadow is _default_sentinel:
|
|
425
|
+
self.shadow = DEFAULT_SHADOW_OFFSET
|
|
426
|
+
self.ocolor = (
|
|
427
|
+
None
|
|
428
|
+
if self.owidth is None
|
|
429
|
+
else _resolvecolor(self.ocolor, DEFAULT_OUTLINE_COLOR)
|
|
430
|
+
)
|
|
431
|
+
self.scolor = (
|
|
432
|
+
None
|
|
433
|
+
if self.shadow is None
|
|
434
|
+
else _resolvecolor(self.scolor, DEFAULT_SHADOW_COLOR)
|
|
435
|
+
)
|
|
436
|
+
self._opx = (
|
|
437
|
+
None
|
|
438
|
+
if self.owidth is None
|
|
439
|
+
else ceil(self.owidth * self.fontsize * OUTLINE_UNIT)
|
|
440
|
+
)
|
|
441
|
+
self._spx = (
|
|
442
|
+
None
|
|
443
|
+
if self.shadow is None
|
|
444
|
+
else tuple(ceil(s * self.fontsize * SHADOW_UNIT) for s in self.shadow)
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
def checkinline(self):
|
|
448
|
+
if (
|
|
449
|
+
self.angle is None
|
|
450
|
+
or self._opx is not None
|
|
451
|
+
or self._spx is not None
|
|
452
|
+
or self.align != 0
|
|
453
|
+
or self.gcolor
|
|
454
|
+
or self.shade
|
|
455
|
+
):
|
|
456
|
+
raise ValueError(
|
|
457
|
+
"Inline style not compatible with rotation, outline, drop shadow, gradient, or non-left-aligned text."
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
def towrapoptions(self):
|
|
461
|
+
return self.getsuboptions(_WrapOptions)
|
|
462
|
+
|
|
463
|
+
def togetfontoptions(self):
|
|
464
|
+
return self.getsuboptions(_GetfontOptions)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
class _WrapOptions(_Options):
|
|
468
|
+
_fields = (
|
|
469
|
+
"fontname",
|
|
470
|
+
"fontsize",
|
|
471
|
+
"sysfontname",
|
|
472
|
+
"bold",
|
|
473
|
+
"italic",
|
|
474
|
+
"underline",
|
|
475
|
+
"width",
|
|
476
|
+
"widthem",
|
|
477
|
+
"strip",
|
|
478
|
+
"color",
|
|
479
|
+
"underlinetag",
|
|
480
|
+
"boldtag",
|
|
481
|
+
"italictag",
|
|
482
|
+
"colortag",
|
|
483
|
+
)
|
|
484
|
+
_defaults = {
|
|
485
|
+
"underlinetag": _default_sentinel,
|
|
486
|
+
"boldtag": _default_sentinel,
|
|
487
|
+
"italictag": _default_sentinel,
|
|
488
|
+
"colortag": _default_sentinel,
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
def __init__(self, **kwargs):
|
|
492
|
+
_Options.__init__(self, **kwargs)
|
|
493
|
+
self.resolvetags()
|
|
494
|
+
if self.widthem is not None and self.width is not None:
|
|
495
|
+
raise ValueError("Can't set both width and widthem")
|
|
496
|
+
|
|
497
|
+
if self.widthem is not None:
|
|
498
|
+
self.fontsize = REFERENCE_FONT_SIZE
|
|
499
|
+
self.width = self.widthem * self.fontsize
|
|
500
|
+
|
|
501
|
+
if self.strip is None:
|
|
502
|
+
self.strip = DEFAULT_STRIP
|
|
503
|
+
|
|
504
|
+
def togetfontoptions(self):
|
|
505
|
+
return self.getsuboptions(_GetfontOptions)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
class _GetfontOptions(_Options):
|
|
509
|
+
_fields = ("fontname", "fontsize", "sysfontname", "bold", "italic", "underline")
|
|
510
|
+
_defaults = {
|
|
511
|
+
"fontname": _default_sentinel,
|
|
512
|
+
"sysfontname": _default_sentinel,
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
def __init__(self, **kwargs):
|
|
516
|
+
_Options.__init__(self, **kwargs)
|
|
517
|
+
if self.fontname is _default_sentinel:
|
|
518
|
+
self.fontname = DEFAULT_FONT_NAME
|
|
519
|
+
if self.sysfontname is _default_sentinel:
|
|
520
|
+
self.sysfontname = DEFAULT_SYSFONT_NAME
|
|
521
|
+
if self.fontname is not None and self.sysfontname is not None:
|
|
522
|
+
raise ValueError("Can't set both fontname and sysfontname")
|
|
523
|
+
if self.fontsize is None:
|
|
524
|
+
self.fontsize = DEFAULT_FONT_SIZE
|
|
525
|
+
|
|
526
|
+
def getfontpath(self):
|
|
527
|
+
return (
|
|
528
|
+
self.fontname
|
|
529
|
+
if self.fontname is None
|
|
530
|
+
else FONT_NAME_TEMPLATE % self.fontname
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
class _FitsizeOptions(_Options):
|
|
535
|
+
_fields = (
|
|
536
|
+
"fontname",
|
|
537
|
+
"sysfontname",
|
|
538
|
+
"bold",
|
|
539
|
+
"italic",
|
|
540
|
+
"underline",
|
|
541
|
+
"lineheight",
|
|
542
|
+
"pspace",
|
|
543
|
+
"strip",
|
|
544
|
+
"underlinetag",
|
|
545
|
+
"boldtag",
|
|
546
|
+
"italictag",
|
|
547
|
+
"colortag",
|
|
548
|
+
)
|
|
549
|
+
_defaults = {
|
|
550
|
+
"underlinetag": _default_sentinel,
|
|
551
|
+
"boldtag": _default_sentinel,
|
|
552
|
+
"italictag": _default_sentinel,
|
|
553
|
+
"colortag": _default_sentinel,
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
def togetfontoptions(self):
|
|
557
|
+
return self.getsuboptions(_GetfontOptions)
|
|
558
|
+
|
|
559
|
+
def towrapoptions(self):
|
|
560
|
+
return self.getsuboptions(_WrapOptions)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
_font_cache = {}
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def getfont(**kwargs):
|
|
567
|
+
options = _GetfontOptions(**kwargs)
|
|
568
|
+
key = options.key()
|
|
569
|
+
if key in _font_cache:
|
|
570
|
+
return _font_cache[key]
|
|
571
|
+
if options.sysfontname is not None:
|
|
572
|
+
font = pygame.font.SysFont(
|
|
573
|
+
options.sysfontname,
|
|
574
|
+
options.fontsize,
|
|
575
|
+
options.bold or False,
|
|
576
|
+
options.italic or False,
|
|
577
|
+
)
|
|
578
|
+
else:
|
|
579
|
+
try:
|
|
580
|
+
font = pygame.font.Font(options.getfontpath(), options.fontsize)
|
|
581
|
+
except IOError:
|
|
582
|
+
raise IOError("unable to read font filename: %s" % options.getfontpath())
|
|
583
|
+
if options.bold is not None:
|
|
584
|
+
font.set_bold(options.bold)
|
|
585
|
+
if options.italic is not None:
|
|
586
|
+
font.set_italic(options.italic)
|
|
587
|
+
if options.underline is not None:
|
|
588
|
+
font.set_underline(options.underline)
|
|
589
|
+
_font_cache[key] = font
|
|
590
|
+
return font
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
# Return the largest integer in the range [xmin, xmax] such that f(x) is True.
|
|
594
|
+
def _binarysearch(f, xmin=1, xmax=256):
|
|
595
|
+
if not f(xmin):
|
|
596
|
+
return xmin
|
|
597
|
+
if f(xmax):
|
|
598
|
+
return xmax
|
|
599
|
+
# xmin is the largest known value for which f(x) is True
|
|
600
|
+
# xmax is the smallest known value for which f(x) is False
|
|
601
|
+
while xmax - xmin > 1:
|
|
602
|
+
x = (xmax + xmin) // 2
|
|
603
|
+
if f(x):
|
|
604
|
+
xmin = x
|
|
605
|
+
else:
|
|
606
|
+
xmax = x
|
|
607
|
+
return xmin
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
_fit_cache = {}
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _fitsize(text, size, **kwargs):
|
|
614
|
+
options = _FitsizeOptions(**kwargs)
|
|
615
|
+
key = text, size, options.key()
|
|
616
|
+
if key in _fit_cache:
|
|
617
|
+
return _fit_cache[key]
|
|
618
|
+
width, height = size
|
|
619
|
+
|
|
620
|
+
def fits(fontsize):
|
|
621
|
+
opts = options.copy()
|
|
622
|
+
wmax, hmax = 0, 0
|
|
623
|
+
for span in _wrap(text, fontsize=fontsize, width=width, **opts.towrapoptions()):
|
|
624
|
+
y = span.font.get_linesize() * (
|
|
625
|
+
opts.pspace * span.jpara + opts.lineheight * span.jline
|
|
626
|
+
)
|
|
627
|
+
w, h = span.font.size(span.text)
|
|
628
|
+
wmax = max(wmax, span.right)
|
|
629
|
+
hmax = max(hmax, y + h)
|
|
630
|
+
return wmax <= width and hmax <= height
|
|
631
|
+
|
|
632
|
+
fontsize = _binarysearch(fits)
|
|
633
|
+
_fit_cache[key] = fontsize
|
|
634
|
+
return fontsize
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
# Returns the color as a color RGB or RGBA tuple (i.e. 3 or 4 integers in the range 0-255)
|
|
638
|
+
# If color is None, fall back to the default. If default is also None, return None.
|
|
639
|
+
# Both color and default can be a list, tuple, a color name, an HTML color format string, a hex
|
|
640
|
+
# number string, or an integer pixel value. See pygame.Color constructor for specification.
|
|
641
|
+
def _resolvecolor(color, default):
|
|
642
|
+
if color is None:
|
|
643
|
+
color = default
|
|
644
|
+
if color is None:
|
|
645
|
+
return None
|
|
646
|
+
try:
|
|
647
|
+
return tuple(pygame.Color(color))
|
|
648
|
+
except ValueError:
|
|
649
|
+
return tuple(color)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _applyshade(color, shade):
|
|
653
|
+
f = exp(-0.4 * shade)
|
|
654
|
+
r, g, b = [min(max(int(round((c + 50) * f - 50)), 0), 255) for c in color[:3]]
|
|
655
|
+
return (r, g, b) + tuple(color[3:])
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def _resolvealpha(alpha):
|
|
659
|
+
if alpha >= 1:
|
|
660
|
+
return 1
|
|
661
|
+
return max(int(round(alpha * ALPHA_RESOLUTION)) / ALPHA_RESOLUTION, 0)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _resolveangle(angle):
|
|
665
|
+
if not angle:
|
|
666
|
+
return 0
|
|
667
|
+
angle %= 360
|
|
668
|
+
return int(round(angle / ANGLE_RESOLUTION_DEGREES)) * ANGLE_RESOLUTION_DEGREES
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
# Return the set of points in the circle radius r, using Bresenham's circle algorithm
|
|
672
|
+
_circle_cache = {}
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _circlepoints(r):
|
|
676
|
+
r = int(round(r))
|
|
677
|
+
if r in _circle_cache:
|
|
678
|
+
return _circle_cache[r]
|
|
679
|
+
x, y, e = r, 0, 1 - r
|
|
680
|
+
_circle_cache[r] = points = []
|
|
681
|
+
while x >= y:
|
|
682
|
+
points.append((x, y))
|
|
683
|
+
y += 1
|
|
684
|
+
if e < 0:
|
|
685
|
+
e += 2 * y - 1
|
|
686
|
+
else:
|
|
687
|
+
x -= 1
|
|
688
|
+
e += 2 * (y - x) - 1
|
|
689
|
+
points += [(y, x) for x, y in points if x > y]
|
|
690
|
+
points += [(-x, y) for x, y in points if x]
|
|
691
|
+
points += [(x, -y) for x, y in points if y]
|
|
692
|
+
points.sort()
|
|
693
|
+
return points
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
# Rotate the given surface by the given angle, in degrees.
|
|
697
|
+
# If angle is an exact multiple of 90, use pygame.transform.rotate, otherwise fall back to
|
|
698
|
+
# pygame.transform.rotozoom.
|
|
699
|
+
def _rotatesurf(surf, angle):
|
|
700
|
+
if angle in (90, 180, 270):
|
|
701
|
+
return pygame.transform.rotate(surf, angle)
|
|
702
|
+
else:
|
|
703
|
+
return pygame.transform.rotozoom(surf, angle, 1.0)
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
# Apply the given alpha value to a copy of the Surface.
|
|
707
|
+
def _fadesurf(surf, alpha):
|
|
708
|
+
surf = surf.copy()
|
|
709
|
+
asurf = surf.copy()
|
|
710
|
+
asurf.fill((255, 255, 255, int(round(255 * alpha))))
|
|
711
|
+
surf.blit(asurf, (0, 0), None, pygame.BLEND_RGBA_MULT)
|
|
712
|
+
return surf
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _istransparent(color):
|
|
716
|
+
return len(color) > 3 and color[3] == 0
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
# Produce a 1xh Surface with the given color gradient.
|
|
720
|
+
_grad_cache = {}
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _gradsurf(h, y0, y1, color0, color1):
|
|
724
|
+
key = h, y0, y1, color0, color1
|
|
725
|
+
if key in _grad_cache:
|
|
726
|
+
return _grad_cache[key]
|
|
727
|
+
surf = pygame.Surface((1, h)).convert_alpha()
|
|
728
|
+
r0, g0, b0 = color0[:3]
|
|
729
|
+
r1, g1, b1 = color1[:3]
|
|
730
|
+
for y in range(h):
|
|
731
|
+
f = min(max((y - y0) / (y1 - y0), 0), 1)
|
|
732
|
+
g = 1 - f
|
|
733
|
+
surf.set_at(
|
|
734
|
+
(0, y),
|
|
735
|
+
(
|
|
736
|
+
int(round(g * r0 + f * r1)),
|
|
737
|
+
int(round(g * g0 + f * g1)),
|
|
738
|
+
int(round(g * b0 + f * b1)),
|
|
739
|
+
0,
|
|
740
|
+
),
|
|
741
|
+
)
|
|
742
|
+
_grad_cache[key] = surf
|
|
743
|
+
return surf
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
# Tracks everything that can be updated by tags.
|
|
747
|
+
class TagSpec(namedtuple("TagSpec", ["underline", "bold", "italic", "color"])):
|
|
748
|
+
@staticmethod
|
|
749
|
+
def fromoptions(options):
|
|
750
|
+
return TagSpec(
|
|
751
|
+
underline=options.underline,
|
|
752
|
+
bold=options.bold,
|
|
753
|
+
italic=options.italic,
|
|
754
|
+
color=options.color,
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
def updateoptions(self, options):
|
|
758
|
+
options.underline = self.underline
|
|
759
|
+
options.bold = self.bold
|
|
760
|
+
options.italic = self.italic
|
|
761
|
+
options.color = self.color
|
|
762
|
+
|
|
763
|
+
def toggleunderline(self):
|
|
764
|
+
return self._replace(underline=not self.underline)
|
|
765
|
+
|
|
766
|
+
def togglebold(self):
|
|
767
|
+
return self._replace(bold=not self.bold)
|
|
768
|
+
|
|
769
|
+
def toggleitalic(self):
|
|
770
|
+
return self._replace(italic=not self.italic)
|
|
771
|
+
|
|
772
|
+
def setcolor(self, color):
|
|
773
|
+
return self._replace(color=color)
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
# Splits a string into substrings with corresponding tag specs.
|
|
777
|
+
# Empty strings are skipped. Consecutive identical tag specs are not merged.
|
|
778
|
+
# e.g. if tagspec0.underline = False and underlinetag = "_" then:
|
|
779
|
+
# _splitbytags("_abc__def_ ghi_") yields three items:
|
|
780
|
+
# ("abc", TagSpec(underline=True))
|
|
781
|
+
# ("def", TagSpec(underline=True))
|
|
782
|
+
# (" ghi", TagSpec(underline=False))
|
|
783
|
+
def _splitbytags(text, tagspec0, color0, underlinetag, boldtag, italictag, colortag):
|
|
784
|
+
colortag = {k: _resolvecolor(v, color0) for k, v in colortag.items()}
|
|
785
|
+
tags = sorted(
|
|
786
|
+
(set([underlinetag, boldtag, italictag]) | set(colortag.keys())) - set([None])
|
|
787
|
+
)
|
|
788
|
+
if not tags:
|
|
789
|
+
yield text, tagspec0
|
|
790
|
+
return
|
|
791
|
+
tagspec = tagspec0
|
|
792
|
+
while text:
|
|
793
|
+
tagsin = [tag for tag in tags if tag in text]
|
|
794
|
+
if not tagsin:
|
|
795
|
+
break
|
|
796
|
+
a, tag = min((text.index(tag), tag) for tag in tagsin)
|
|
797
|
+
if a > 0:
|
|
798
|
+
yield text[:a], tagspec
|
|
799
|
+
text = text[a + len(tag) :]
|
|
800
|
+
if tag == underlinetag:
|
|
801
|
+
tagspec = tagspec.toggleunderline()
|
|
802
|
+
if tag == boldtag:
|
|
803
|
+
tagspec = tagspec.togglebold()
|
|
804
|
+
if tag == italictag:
|
|
805
|
+
tagspec = tagspec.toggleitalic()
|
|
806
|
+
if tag in colortag:
|
|
807
|
+
tagspec = tagspec.setcolor(colortag[tag])
|
|
808
|
+
if text:
|
|
809
|
+
yield text, tagspec
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
# The _Span class tracks many attributes of a single span of text, i.e. a string of text within a
|
|
813
|
+
# single line that has a single font and TagSpec. That is, a single span corresponds to a single
|
|
814
|
+
# call to font.render.
|
|
815
|
+
# This is not a clean abstraction, and some of the state of this object only makes sense in the
|
|
816
|
+
# context of the overall draw call. At various stages of the call, some of the fields will not yet
|
|
817
|
+
# be populated.
|
|
818
|
+
class _Span:
|
|
819
|
+
# Phase 1: set by _wrapline
|
|
820
|
+
def __init__(self, text, tagspec, x, font):
|
|
821
|
+
self.tagspec = tagspec
|
|
822
|
+
self.x = x # Offset from the beginning of the line
|
|
823
|
+
self.font = font
|
|
824
|
+
self.settext(text)
|
|
825
|
+
|
|
826
|
+
# Phase 2: set by _wrap
|
|
827
|
+
def setlayout(self, jpara, jline, linewidth):
|
|
828
|
+
self.jpara = jpara
|
|
829
|
+
self.jline = jline
|
|
830
|
+
self.linewidth = linewidth
|
|
831
|
+
|
|
832
|
+
# Phase 3: set by getsurf
|
|
833
|
+
# These are not required to determine layout or position, only for rendering.
|
|
834
|
+
def setdetails(self, antialias, gcolor, background):
|
|
835
|
+
self.antialias = antialias
|
|
836
|
+
self.gcolor = gcolor
|
|
837
|
+
self.background = background
|
|
838
|
+
|
|
839
|
+
def settext(self, text):
|
|
840
|
+
self.text = text
|
|
841
|
+
self.width = self.getwidth(self.text)
|
|
842
|
+
self.right = self.x + self.width
|
|
843
|
+
|
|
844
|
+
def getwidth(self, text):
|
|
845
|
+
return self.font.size(text)[0]
|
|
846
|
+
|
|
847
|
+
def render(self):
|
|
848
|
+
if self.gcolor is None:
|
|
849
|
+
# Workaround: pygame.Font.render does not allow passing None as an argument value for
|
|
850
|
+
# background. We have to call the 3-argument form to specify no background.
|
|
851
|
+
args = self.text, self.antialias, self.tagspec.color
|
|
852
|
+
if self.background is not None and not _istransparent(self.background):
|
|
853
|
+
args += (self.background,)
|
|
854
|
+
self.surf = self.font.render(*args).convert_alpha()
|
|
855
|
+
else:
|
|
856
|
+
self.surf = self.font.render(
|
|
857
|
+
self.text, self.antialias, (0, 0, 0)
|
|
858
|
+
).convert_alpha()
|
|
859
|
+
w, h = self.surf.get_size()
|
|
860
|
+
asc = self.font.get_ascent()
|
|
861
|
+
gsurf0 = _gradsurf(h, 0.5 * asc, asc, self.tagspec.color, self.gcolor)
|
|
862
|
+
gsurf = pygame.transform.scale(gsurf0, (w, h))
|
|
863
|
+
self.surf.blit(gsurf, (0, 0), None, pygame.BLEND_RGBA_ADD)
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
# Finds the last valid breakpoint in the line of text. A breakpoint is a position at which the line
|
|
867
|
+
# can be split without improperly breaking words.
|
|
868
|
+
# Returns (breaktext, breakpoint)
|
|
869
|
+
def _breaktext(text, width, font, canbreakatstart=False):
|
|
870
|
+
# TODO: binary search
|
|
871
|
+
# The text to be printed that actually comes from text. Does not include stripped characters,
|
|
872
|
+
# e.g. soft hyphens, trailing or otherwise. Does include trailing spaces.
|
|
873
|
+
btext = ""
|
|
874
|
+
# Index of the first character in text that does not appear in btext.
|
|
875
|
+
b = 0 if canbreakatstart else None
|
|
876
|
+
# Any additional characters to be appended on return, i.e. hyphen generated by soft hyphens.
|
|
877
|
+
bapp = ""
|
|
878
|
+
# Partial buildup of btext.
|
|
879
|
+
ptext = ""
|
|
880
|
+
|
|
881
|
+
def isvalid(t):
|
|
882
|
+
return width is None or font.size(t)[0] <= width
|
|
883
|
+
|
|
884
|
+
for j, c in enumerate(text):
|
|
885
|
+
atbreak, napp = False, ""
|
|
886
|
+
# Space and hyphen character allow for a breakpoint.
|
|
887
|
+
if c in [" ", "-"]:
|
|
888
|
+
atbreak = True
|
|
889
|
+
# Non-breaking space. No breakpoint here. Instead just add a space.
|
|
890
|
+
elif c == "\u00A0":
|
|
891
|
+
c = " "
|
|
892
|
+
# Non-breaking hyphen. No breakpoint here. Instead just add a hyphen.
|
|
893
|
+
elif c == "\u2011":
|
|
894
|
+
c = "-"
|
|
895
|
+
# Zero-width space. Allow a breakpoint but don't add anything (i.e. remove this character)
|
|
896
|
+
elif c == "\u200B":
|
|
897
|
+
atbreak = True
|
|
898
|
+
c = ""
|
|
899
|
+
# Soft hyphen. Allow a breakpoint with an appending string of hyphen ("-").
|
|
900
|
+
elif c == "\u00AD":
|
|
901
|
+
atbreak = True
|
|
902
|
+
c = ""
|
|
903
|
+
napp = "-"
|
|
904
|
+
ptext += c
|
|
905
|
+
if atbreak:
|
|
906
|
+
if b is None or isvalid((ptext + napp).rstrip(" ")):
|
|
907
|
+
btext = ptext
|
|
908
|
+
b = j + 1
|
|
909
|
+
bapp = napp
|
|
910
|
+
else:
|
|
911
|
+
break
|
|
912
|
+
else:
|
|
913
|
+
# One past the end of the line is always considered a breakpoint.
|
|
914
|
+
if b is None or isvalid(ptext):
|
|
915
|
+
return ptext, len(text)
|
|
916
|
+
# Invalid breakpoint found. Take trailing spaces starting from the last valid breakpoint.
|
|
917
|
+
while b < len(text) and text[b] == " ":
|
|
918
|
+
b += 1
|
|
919
|
+
bapp += " "
|
|
920
|
+
return btext + bapp, b
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
# Split a single line of text.
|
|
924
|
+
# textandtags is the output of _splitbytags, i.e. a sequence of (string, tag spec) tuples.
|
|
925
|
+
def _wrapline(textandtags, width, getfontbytagspec):
|
|
926
|
+
x = 0
|
|
927
|
+
canbreakatstart = False
|
|
928
|
+
lines = []
|
|
929
|
+
line = []
|
|
930
|
+
for text, tagspec in textandtags:
|
|
931
|
+
font = getfontbytagspec(tagspec)
|
|
932
|
+
while text:
|
|
933
|
+
rwidth = None if width is None else width - x
|
|
934
|
+
btext, b = _breaktext(text, rwidth, font, canbreakatstart)
|
|
935
|
+
if b == 0:
|
|
936
|
+
lines.append((line, x))
|
|
937
|
+
line = []
|
|
938
|
+
x = 0
|
|
939
|
+
canbreakatstart = False
|
|
940
|
+
else:
|
|
941
|
+
span = _Span(btext, tagspec, x, font)
|
|
942
|
+
line.append(span)
|
|
943
|
+
x += span.width
|
|
944
|
+
text = text[b:]
|
|
945
|
+
canbreakatstart = True
|
|
946
|
+
lines.append((line, x))
|
|
947
|
+
return lines
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def _wrap(text, **kwargs):
|
|
951
|
+
options = _WrapOptions(**kwargs)
|
|
952
|
+
# Returns a function mapping strings to int widths in the specified font
|
|
953
|
+
opts = options.copy()
|
|
954
|
+
|
|
955
|
+
def getfontbytagspec(tagspec):
|
|
956
|
+
tagspec.updateoptions(opts)
|
|
957
|
+
return getfont(**opts.togetfontoptions())
|
|
958
|
+
|
|
959
|
+
# Apparently Font.render accepts None for the text argument, in which case it's treated as the
|
|
960
|
+
# empty string. We match that behavior here.
|
|
961
|
+
if text is None:
|
|
962
|
+
text = ""
|
|
963
|
+
spans = []
|
|
964
|
+
tagspec0 = TagSpec.fromoptions(options)
|
|
965
|
+
jline = 0
|
|
966
|
+
for jpara, para in enumerate(text.replace("\t", " ").split("\n")):
|
|
967
|
+
if options.strip:
|
|
968
|
+
para = para.rstrip(" ")
|
|
969
|
+
tagargs = (
|
|
970
|
+
options.underlinetag,
|
|
971
|
+
options.boldtag,
|
|
972
|
+
options.italictag,
|
|
973
|
+
options.colortag,
|
|
974
|
+
)
|
|
975
|
+
textandtags = list(_splitbytags(para, tagspec0, options.color, *tagargs))
|
|
976
|
+
_, tagspec0 = textandtags[-1]
|
|
977
|
+
for line, linewidth in _wrapline(textandtags, options.width, getfontbytagspec):
|
|
978
|
+
if not line:
|
|
979
|
+
jline += 1
|
|
980
|
+
continue
|
|
981
|
+
# Strip trailing spaces from the end of each line.
|
|
982
|
+
span = line[-1]
|
|
983
|
+
if options.strip:
|
|
984
|
+
span.settext(span.text.rstrip(" "))
|
|
985
|
+
elif options.width is not None:
|
|
986
|
+
while span.text[-1] == " " and span.right > options.width:
|
|
987
|
+
span.settext(span.text[:-1])
|
|
988
|
+
linewidth = span.right
|
|
989
|
+
for span in line:
|
|
990
|
+
span.setlayout(jpara, jline, linewidth)
|
|
991
|
+
spans.append(span)
|
|
992
|
+
jline += 1
|
|
993
|
+
return spans
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
_surf_cache = {}
|
|
997
|
+
_surf_tick_usage = {}
|
|
998
|
+
_surf_size_total = 0
|
|
999
|
+
_unrotated_size = {}
|
|
1000
|
+
_tick = 0
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def getsurf(text, **kwargs):
|
|
1004
|
+
global _tick, _surf_size_total
|
|
1005
|
+
options = _GetsurfOptions(**kwargs)
|
|
1006
|
+
key = text, options.key()
|
|
1007
|
+
if key in _surf_cache:
|
|
1008
|
+
_surf_tick_usage[key] = _tick
|
|
1009
|
+
_tick += 1
|
|
1010
|
+
return _surf_cache[key]
|
|
1011
|
+
|
|
1012
|
+
if options.angle:
|
|
1013
|
+
surf0 = getsurf(text, **options.update(angle=0))
|
|
1014
|
+
surf = _rotatesurf(surf0, options.angle)
|
|
1015
|
+
# draw() requires the unrotated size for proper positioning, but the unrotated surface will
|
|
1016
|
+
# not necessarily be cached, so we add it to a global store here. In principle you could
|
|
1017
|
+
# compute it from surf.get_size() and options.angle, were it not for rounding issues.
|
|
1018
|
+
_unrotated_size[(surf.get_size(), options.angle, text)] = surf0.get_size()
|
|
1019
|
+
elif options.alpha < 1.0:
|
|
1020
|
+
surf = _fadesurf(getsurf(text, **options.update(alpha=1.0)), options.alpha)
|
|
1021
|
+
elif options._spx is not None:
|
|
1022
|
+
color = (0, 0, 0) if _istransparent(options.color) else options.color
|
|
1023
|
+
surf0 = getsurf(
|
|
1024
|
+
text,
|
|
1025
|
+
**options.update(
|
|
1026
|
+
background=(0, 0, 0, 0), color=color, shadow=None, scolor=None
|
|
1027
|
+
),
|
|
1028
|
+
)
|
|
1029
|
+
sopts = {
|
|
1030
|
+
"color": options.scolor,
|
|
1031
|
+
"shadow": None,
|
|
1032
|
+
"scolor": None,
|
|
1033
|
+
"background": (0, 0, 0, 0),
|
|
1034
|
+
"gcolor": None,
|
|
1035
|
+
"colortag": {k: None for k in options.colortag},
|
|
1036
|
+
}
|
|
1037
|
+
ssurf = getsurf(text, **options.update(**sopts))
|
|
1038
|
+
w0, h0 = surf0.get_size()
|
|
1039
|
+
sx, sy = options._spx
|
|
1040
|
+
surf = pygame.Surface((w0 + abs(sx), h0 + abs(sy))).convert_alpha()
|
|
1041
|
+
surf.fill(options.background or (0, 0, 0, 0))
|
|
1042
|
+
dx, dy = max(sx, 0), max(sy, 0)
|
|
1043
|
+
surf.blit(ssurf, (dx, dy))
|
|
1044
|
+
x0, y0 = abs(sx) - dx, abs(sy) - dy
|
|
1045
|
+
if _istransparent(options.color):
|
|
1046
|
+
surf.blit(surf0, (x0, y0), None, pygame.BLEND_RGBA_SUB)
|
|
1047
|
+
else:
|
|
1048
|
+
surf.blit(surf0, (x0, y0))
|
|
1049
|
+
elif options._opx is not None:
|
|
1050
|
+
color = (0, 0, 0) if _istransparent(options.color) else options.color
|
|
1051
|
+
surf0 = getsurf(text, **options.update(color=color, ocolor=None, owidth=None))
|
|
1052
|
+
oopts = {
|
|
1053
|
+
"color": options.ocolor,
|
|
1054
|
+
"ocolor": None,
|
|
1055
|
+
"owidth": None,
|
|
1056
|
+
"background": (0, 0, 0, 0),
|
|
1057
|
+
"gcolor": None,
|
|
1058
|
+
"colortag": {k: None for k in options.colortag},
|
|
1059
|
+
}
|
|
1060
|
+
osurf = getsurf(text, **options.update(**oopts))
|
|
1061
|
+
w0, h0 = surf0.get_size()
|
|
1062
|
+
opx = options._opx
|
|
1063
|
+
surf = pygame.Surface((w0 + 2 * opx, h0 + 2 * opx)).convert_alpha()
|
|
1064
|
+
surf.fill(options.background or (0, 0, 0, 0))
|
|
1065
|
+
for dx, dy in _circlepoints(opx):
|
|
1066
|
+
surf.blit(osurf, (dx + opx, dy + opx))
|
|
1067
|
+
if _istransparent(options.color):
|
|
1068
|
+
surf.blit(surf0, (opx, opx), None, pygame.BLEND_RGBA_SUB)
|
|
1069
|
+
else:
|
|
1070
|
+
surf.blit(surf0, (opx, opx))
|
|
1071
|
+
else:
|
|
1072
|
+
# Each span is rendered separately into a Surface, and then the different spans' Surfaces
|
|
1073
|
+
# are blitted onto the final Surface.
|
|
1074
|
+
spans = _wrap(text, **options.towrapoptions())
|
|
1075
|
+
for span in spans:
|
|
1076
|
+
span.setdetails(options.antialias, options.gcolor, options.background)
|
|
1077
|
+
span.render()
|
|
1078
|
+
# Now to blit the span Surfaces together onto a single Surface.
|
|
1079
|
+
if not spans:
|
|
1080
|
+
surf = pygame.Surface((0, 0)).convert_alpha()
|
|
1081
|
+
else:
|
|
1082
|
+
font = spans[0].font
|
|
1083
|
+
w = max(options.width or 0, max(span.linewidth for span in spans))
|
|
1084
|
+
linesize = font.get_linesize() * options.lineheight
|
|
1085
|
+
parasize = font.get_linesize() * options.pspace
|
|
1086
|
+
for span in spans:
|
|
1087
|
+
span.y = int(round(span.jline * linesize + span.jpara * parasize))
|
|
1088
|
+
h = max(span.y for span in spans) + font.get_height()
|
|
1089
|
+
surf = pygame.Surface((w, h)).convert_alpha()
|
|
1090
|
+
surf.fill(options.background or (0, 0, 0, 0))
|
|
1091
|
+
for span in spans:
|
|
1092
|
+
x = int(round(span.x + options.align * (w - span.linewidth)))
|
|
1093
|
+
surf.blit(span.surf, (x, span.y))
|
|
1094
|
+
if options.cache:
|
|
1095
|
+
w, h = surf.get_size()
|
|
1096
|
+
_surf_size_total += 4 * w * h
|
|
1097
|
+
_surf_cache[key] = surf
|
|
1098
|
+
_surf_tick_usage[key] = _tick
|
|
1099
|
+
_tick += 1
|
|
1100
|
+
return surf
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
# The actual position on the screen where the surf is to be blitted, rather than the specified
|
|
1104
|
+
# anchor position.
|
|
1105
|
+
def _blitpos(angle, pos, anchor, size, text):
|
|
1106
|
+
angle = _resolveangle(angle)
|
|
1107
|
+
x, y = pos
|
|
1108
|
+
sw, sh = size
|
|
1109
|
+
hanchor, vanchor = anchor
|
|
1110
|
+
if angle:
|
|
1111
|
+
w0, h0 = _unrotated_size[(size, angle, text)]
|
|
1112
|
+
S, C = sin(radians(angle)), cos(radians(angle))
|
|
1113
|
+
dx, dy = (0.5 - hanchor) * w0, (0.5 - vanchor) * h0
|
|
1114
|
+
x += dx * C + dy * S - 0.5 * sw
|
|
1115
|
+
y += -dx * S + dy * C - 0.5 * sh
|
|
1116
|
+
else:
|
|
1117
|
+
x -= hanchor * sw
|
|
1118
|
+
y -= vanchor * sh
|
|
1119
|
+
x = int(round(x))
|
|
1120
|
+
y = int(round(y))
|
|
1121
|
+
return x, y
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
def layout(text, **kwargs):
|
|
1125
|
+
options = _LayoutOptions(**kwargs)
|
|
1126
|
+
if options.angle != 0:
|
|
1127
|
+
raise ValueError("Nonzero angle not yet supported for ptext.layout")
|
|
1128
|
+
font = getfont(**options.togetfontoptions())
|
|
1129
|
+
fl = font.get_linesize()
|
|
1130
|
+
linesize = fl * options.lineheight
|
|
1131
|
+
parasize = fl * options.pspace
|
|
1132
|
+
|
|
1133
|
+
spans = _wrap(text, **options.towrapoptions())
|
|
1134
|
+
|
|
1135
|
+
rects = []
|
|
1136
|
+
sw = max(span.linewidth for span in spans)
|
|
1137
|
+
for span in spans:
|
|
1138
|
+
y = int(round(span.jpara * parasize + span.jline * linesize))
|
|
1139
|
+
rect = pygame.Rect(span.x, y, *font.size(span.text))
|
|
1140
|
+
rect.x += int(round(options.align * (sw - span.linewidth)))
|
|
1141
|
+
rects.append(rect)
|
|
1142
|
+
sh = max(rect.bottom for rect in rects)
|
|
1143
|
+
|
|
1144
|
+
x0, y0 = _blitpos(options.angle, options.pos, options.anchor, (sw, sh), None)
|
|
1145
|
+
|
|
1146
|
+
# Adjust the rects as necessary to account for outline and shadow.
|
|
1147
|
+
# TODO: the following is duplicated from _GetsurfOptions.__init__
|
|
1148
|
+
dx, dy = 0, 0
|
|
1149
|
+
if options.owidth is not None:
|
|
1150
|
+
opx = ceil(options.owidth * options.fontsize * OUTLINE_UNIT)
|
|
1151
|
+
dx, dy = max(dx, abs(opx)), max(dy, abs(opx))
|
|
1152
|
+
if options.shadow is not None:
|
|
1153
|
+
spx, spy = (ceil(s * options.fontsize * SHADOW_UNIT) for s in options.shadow)
|
|
1154
|
+
dx, dy = max(dx, -spx), max(dy, -spy)
|
|
1155
|
+
rects = [rect.move(x0 + dx, y0 + dy) for rect in rects]
|
|
1156
|
+
|
|
1157
|
+
return [(span.text, rect, span.font) for span, rect in zip(spans, rects)]
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
def draw(text, pos=None, **kwargs):
|
|
1161
|
+
options = _DrawOptions(pos=pos, **kwargs)
|
|
1162
|
+
tsurf = getsurf(text, **options.togetsurfoptions())
|
|
1163
|
+
pos = _blitpos(options.angle, options.pos, options.anchor, tsurf.get_size(), text)
|
|
1164
|
+
if options.surf is not None:
|
|
1165
|
+
options.surf.blit(tsurf, pos)
|
|
1166
|
+
if AUTO_CLEAN:
|
|
1167
|
+
clean()
|
|
1168
|
+
return tsurf, pos
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
def drawbox(text, rect, **kwargs):
|
|
1172
|
+
options = _DrawboxOptions(**kwargs)
|
|
1173
|
+
rect = pygame.Rect(rect)
|
|
1174
|
+
hanchor, vanchor = options.anchor
|
|
1175
|
+
x = rect.x + hanchor * rect.width
|
|
1176
|
+
y = rect.y + vanchor * rect.height
|
|
1177
|
+
fontsize = _fitsize(text, rect.size, **options.tofitsizeoptions())
|
|
1178
|
+
return draw(
|
|
1179
|
+
text, pos=(x, y), width=rect.width, fontsize=fontsize, **options.todrawoptions()
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def clean():
|
|
1184
|
+
global _surf_size_total
|
|
1185
|
+
memory_limit = MEMORY_LIMIT_MB * (1 << 20)
|
|
1186
|
+
if _surf_size_total < memory_limit:
|
|
1187
|
+
return
|
|
1188
|
+
memory_limit *= MEMORY_REDUCTION_FACTOR
|
|
1189
|
+
keys = sorted(_surf_cache, key=_surf_tick_usage.get)
|
|
1190
|
+
for key in keys:
|
|
1191
|
+
w, h = _surf_cache[key].get_size()
|
|
1192
|
+
del _surf_cache[key]
|
|
1193
|
+
del _surf_tick_usage[key]
|
|
1194
|
+
_surf_size_total -= 4 * w * h
|
|
1195
|
+
if _surf_size_total < memory_limit:
|
|
1196
|
+
break
|