exp-runtime-player 0.5.0__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.
@@ -0,0 +1,612 @@
1
+ """Retained drawing commands for native hosts, using the shared renderer API.
2
+
3
+ Only the primitives used by our presentation code are implemented. This is not
4
+ an SDL emulator. Immutable snapshots keep cached layers and fades independent
5
+ of later writes. Native hosts rasterize commands; Pillow decodes source images
6
+ and performs the small, byte-exact asset operations used by masks and atlases.
7
+ """
8
+ import base64
9
+ from dataclasses import dataclass, field
10
+ from functools import lru_cache
11
+ from io import BytesIO
12
+ from itertools import count
13
+ import math
14
+ import struct
15
+ from types import SimpleNamespace
16
+
17
+ from PIL import Image, ImageDraw
18
+
19
+
20
+ class DrawingError(ValueError):
21
+ pass
22
+
23
+
24
+ class Rect:
25
+ cast = int
26
+
27
+ def __init__(self, *values):
28
+ if len(values) == 1:
29
+ values = tuple(values[0])
30
+ if len(values) == 2:
31
+ values = (*values[0], *values[1])
32
+ if len(values) != 4:
33
+ raise TypeError('Rectangle requires x, y, width, height')
34
+ self.x, self.y, self.width, self.height = map(self.cast, values)
35
+
36
+ def __setattr__(self, key, value):
37
+ object.__setattr__(self, key, self.cast(value) if key in ('x', 'y', 'width', 'height') else value)
38
+
39
+ def __iter__(self):
40
+ return iter((self.x, self.y, self.width, self.height))
41
+
42
+ def __getitem__(self, index):
43
+ return tuple(self)[index]
44
+
45
+ def __eq__(self, other):
46
+ return tuple(self) == tuple(other)
47
+
48
+ @property
49
+ def size(self):
50
+ return self.width, self.height
51
+
52
+ @property
53
+ def topleft(self):
54
+ return self.x, self.y
55
+
56
+ @property
57
+ def top(self):
58
+ return self.y
59
+
60
+ @property
61
+ def bottom(self):
62
+ return self.y + self.height
63
+
64
+ @property
65
+ def left(self):
66
+ return self.x
67
+
68
+ @property
69
+ def right(self):
70
+ return self.x + self.width
71
+
72
+ @property
73
+ def centerx(self):
74
+ return self.x + (self.width // 2 if self.cast is int else self.width / 2)
75
+
76
+ @centerx.setter
77
+ def centerx(self, value):
78
+ self.x = self.cast(value) - (self.width // 2 if self.cast is int else self.width / 2)
79
+
80
+ @property
81
+ def centery(self):
82
+ return self.y + (self.height // 2 if self.cast is int else self.height / 2)
83
+
84
+ @centery.setter
85
+ def centery(self, value):
86
+ self.y = self.cast(value) - (self.height // 2 if self.cast is int else self.height / 2)
87
+
88
+ @property
89
+ def center(self):
90
+ return self.centerx, self.centery
91
+
92
+ @center.setter
93
+ def center(self, point):
94
+ self.centerx, self.centery = point
95
+
96
+ def clip(self, other):
97
+ other = type(self)(other)
98
+ x, y = max(self.x, other.x), max(self.y, other.y)
99
+ w, h = min(self.right, other.right) - x, min(self.bottom, other.bottom) - y
100
+ return type(self)(x, y, w, h) if w > 0 and h > 0 else type(self)(self.x, self.y, 0, 0)
101
+
102
+ def inflate(self, x, y):
103
+ x, y = self.cast(x), self.cast(y)
104
+ return type(self)(self.x - self.cast(x / 2), self.y - self.cast(y / 2), self.width + x, self.height + y)
105
+
106
+ def inflate_ip(self, x, y):
107
+ self.x, self.y, self.width, self.height = self.inflate(x, y)
108
+
109
+ def move(self, x, y):
110
+ return type(self)(self.x + self.cast(x), self.y + self.cast(y), self.width, self.height)
111
+
112
+ def unionall(self, rectangles):
113
+ rectangles = [self, *(type(self)(rect) for rect in rectangles)]
114
+ x, y = min(rect.x for rect in rectangles), min(rect.y for rect in rectangles)
115
+ right, bottom = max(rect.right for rect in rectangles), max(rect.bottom for rect in rectangles)
116
+ return type(self)(x, y, right - x, bottom - y)
117
+
118
+ def copy(self):
119
+ return type(self)(self)
120
+
121
+ def collidepoint(self, *point):
122
+ x, y = point[0] if len(point) == 1 else point
123
+ return self.x <= x < self.right and self.y <= y < self.bottom
124
+
125
+ def colliderect(self, other):
126
+ other = type(self)(other)
127
+ return self.x < other.right and self.right > other.x and self.y < other.bottom and self.bottom > other.y
128
+
129
+
130
+ class FRect(Rect):
131
+ cast = float
132
+
133
+
134
+ _ids = count(1)
135
+ SRCALPHA, BLEND_RGBA_MULT, RESIZABLE = 1, 2, 4
136
+
137
+
138
+ def rgba(color):
139
+ return tuple(color) if len(color) == 4 else (*color, 255)
140
+
141
+
142
+ @dataclass(eq=False)
143
+ class Node:
144
+ size: tuple
145
+ ops: tuple = ()
146
+ pixels: Image.Image | None = None
147
+ identifier: int = field(default_factory=lambda: next(_ids))
148
+ encoded: str | None = None
149
+
150
+ def record(self):
151
+ record = dict(id=self.identifier, size=self.size)
152
+ if self.pixels is not None:
153
+ if self.encoded is None:
154
+ stream = BytesIO()
155
+ self.pixels.save(stream, 'PNG')
156
+ self.encoded = base64.b64encode(stream.getvalue()).decode('ascii')
157
+ record['png'] = self.encoded
158
+ else:
159
+ record['ops'] = [dict(op, source=op['source'].identifier) if 'source' in op else op for op in self.ops]
160
+ return record
161
+
162
+
163
+ class Surface:
164
+ def __init__(self, size, flags=0):
165
+ self._size = tuple(map(int, size))
166
+ if len(self._size) != 2 or min(self._size) < 0:
167
+ raise DrawingError('Invalid surface dimensions')
168
+ self._alpha = 255
169
+ self._clip = Rect(0, 0, *self._size)
170
+ self._ops = []
171
+ self._node = None
172
+ self.opaque = not bool(flags & SRCALPHA)
173
+ self.fill((0, 0, 0, 255 if self.opaque else 0))
174
+
175
+ @classmethod
176
+ def from_pixels(cls, pixels):
177
+ result = cls(pixels.size, SRCALPHA)
178
+ result._ops = []
179
+ result._node = Node(pixels.size, pixels=pixels.convert('RGBA'))
180
+ result.opaque = result._node.pixels.getextrema()[3] == (255, 255)
181
+ return result
182
+
183
+ @classmethod
184
+ def from_node(cls, node):
185
+ result = cls(node.size, SRCALPHA)
186
+ result._ops, result._node = [], node
187
+ return result
188
+
189
+ def snapshot(self):
190
+ if self._node is None:
191
+ self._node = Node(self._size, tuple(self._ops))
192
+ return self._node
193
+
194
+ def _append(self, op):
195
+ if not self._ops and self._node is not None:
196
+ self._ops = [dict(kind='blit', source=self._node, at=(0, 0), alpha=255,
197
+ clip=(0, 0, *self._size))]
198
+ self._node = None
199
+ self._ops.append(dict(op, clip=tuple(self._clip)))
200
+
201
+ def get_size(self):
202
+ return self._size
203
+
204
+ def get_width(self):
205
+ return self._size[0]
206
+
207
+ def get_height(self):
208
+ return self._size[1]
209
+
210
+ def get_rect(self, **keywords):
211
+ rect = Rect(0, 0, *self._size)
212
+ for key, value in keywords.items():
213
+ setattr(rect, key, value)
214
+ return rect
215
+
216
+ def copy(self):
217
+ result = self.from_node(self.snapshot())
218
+ result._alpha, result.opaque = self._alpha, self.opaque
219
+ result._clip = Rect(self._clip)
220
+ return result
221
+
222
+ def convert(self, depth=32):
223
+ return self
224
+
225
+ def convert_alpha(self):
226
+ return self
227
+
228
+ def set_alpha(self, alpha):
229
+ self._alpha = 255 if alpha is None else max(0, min(255, int(alpha)))
230
+
231
+ def set_clip(self, rect=None):
232
+ self._clip = self.get_rect() if rect is None else self.get_rect().clip(rect)
233
+
234
+ def get_clip(self):
235
+ return Rect(self._clip)
236
+
237
+ def fill(self, color, rect=None, special_flags=0):
238
+ region = self._clip if rect is None else self._clip.clip(rect)
239
+ if region.width <= 0 or region.height <= 0:
240
+ return region
241
+ if special_flags:
242
+ if special_flags != BLEND_RGBA_MULT:
243
+ raise DrawingError('Unsupported fill blend')
244
+ pixels = raster(self.snapshot())
245
+ box = (region.x, region.y, region.right, region.bottom)
246
+ patch = pixels.crop(box)
247
+ # SDL's multiply is (a*b + 255) >> 8, including alpha.
248
+ patch = Image.merge('RGBA', [channel.point(lambda a, b=b: (a * b + 255) >> 8)
249
+ for channel, b in zip(patch.split(), rgba(color))])
250
+ pixels.paste(patch, box)
251
+ self._ops, self._node = [], Node(self._size, pixels=pixels)
252
+ self.opaque = pixels.getextrema()[3] == (255, 255)
253
+ else:
254
+ if region == self.get_rect():
255
+ self._ops, self._node = [], None
256
+ self.opaque = rgba(color)[3] == 255
257
+ self._append(dict(kind='fill', color=rgba(color), rect=tuple(region)))
258
+ return region
259
+
260
+ def blit(self, source, destination, area=None, special_flags=0):
261
+ if area is not None:
262
+ source = source.subsurface(area)
263
+ x, y = tuple(destination)[:2]
264
+ at = int(x), int(y)
265
+ bounds = Rect(*at, *source.get_size()).clip(self._clip)
266
+ if special_flags:
267
+ if special_flags != BLEND_RGBA_MULT or at != (0, 0) or source.get_size() != self._size:
268
+ raise DrawingError('Unsupported image blend')
269
+ a, b = raster(self.snapshot()), raster(source.snapshot())
270
+ # The timer mask has binary channels. General multiply uses the
271
+ # same rounded multiplication as the atlas modulation above.
272
+ pixels = Image.frombytes('RGBA', self._size,
273
+ bytes((x * y + 255) >> 8 for x, y in zip(a.tobytes(), b.tobytes())))
274
+ self._ops, self._node = [], Node(self._size, pixels=pixels)
275
+ self.opaque = pixels.getextrema()[3] == (255, 255)
276
+ elif bounds.width > 0 and bounds.height > 0:
277
+ node = source.snapshot()
278
+ if source.opaque and source._alpha == 255 and bounds == self.get_rect():
279
+ self._ops, self._node = [], None
280
+ self.opaque = True
281
+ self._append(dict(kind='blit', source=node, at=at, alpha=source._alpha))
282
+ return bounds
283
+
284
+ def subsurface(self, rect):
285
+ rect = Rect(rect)
286
+ if rect.x < 0 or rect.y < 0 or rect.right > self.get_width() or rect.bottom > self.get_height():
287
+ raise DrawingError('Subsurface lies outside its source')
288
+ node = self.snapshot()
289
+ if node.pixels is not None:
290
+ return self.from_pixels(node.pixels.crop((rect.x, rect.y, rect.right, rect.bottom)))
291
+ result = Surface(rect.size, SRCALPHA)
292
+ result.blit(self, (-rect.x, -rect.y))
293
+ return result
294
+
295
+
296
+ class Draw:
297
+ @staticmethod
298
+ def textured_quad(surface, image, bounds, coefficients):
299
+ surface._append(dict(kind='textured_quad', source=image.snapshot(), alpha=image._alpha,
300
+ bounds=bounds, coefficients=coefficients))
301
+
302
+ @staticmethod
303
+ def textured_rows(surface, image, top, rows):
304
+ surface._append(dict(kind='textured_rows', source=image.snapshot(),
305
+ alpha=image._alpha, top=top, rows=tuple(rows)))
306
+
307
+ @staticmethod
308
+ def rect(surface, color, rect, width=0, border_radius=0):
309
+ surface._append(dict(kind='rect', color=rgba(color), rect=tuple(Rect(rect)),
310
+ width=width, radius=border_radius))
311
+
312
+ @staticmethod
313
+ def line(surface, color, start, end, width=1):
314
+ surface._append(dict(kind='line', color=rgba(color), points=(start, end), width=width))
315
+
316
+ @staticmethod
317
+ def polygon(surface, color, points, width=0):
318
+ surface._append(dict(kind='polygon', color=rgba(color), points=tuple(points), width=width))
319
+
320
+ @staticmethod
321
+ def circle(surface, color, center, radius, width=0):
322
+ surface._append(dict(kind='circle', color=rgba(color), center=center, radius=radius, width=width))
323
+
324
+
325
+ class Transform:
326
+ @staticmethod
327
+ def _scale(surface, size, smooth):
328
+ size = tuple(map(int, size))
329
+ node = surface.snapshot()
330
+ if min(surface.get_size()) == 0:
331
+ return Surface(size, SRCALPHA)
332
+ # Keep source pixels stable. Realizing resized sprites here encoded
333
+ # dozens of fresh PNGs per football frame (including four large haze
334
+ # tiles). The native canvas already implements both resampling modes.
335
+ result = Surface.from_node(Node(size, (dict(kind='scale', source=node, smooth=smooth),)))
336
+ result._alpha, result.opaque = surface._alpha, surface.opaque
337
+ return result
338
+
339
+ @classmethod
340
+ def scale(cls, surface, size):
341
+ return cls._scale(surface, size, False)
342
+
343
+ @classmethod
344
+ def smoothscale(cls, surface, size):
345
+ return cls._scale(surface, size, True)
346
+
347
+ @staticmethod
348
+ def flip(surface, horizontal, vertical):
349
+ node = surface.snapshot()
350
+ if node.pixels is not None:
351
+ pixels = node.pixels
352
+ if horizontal:
353
+ pixels = pixels.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
354
+ if vertical:
355
+ pixels = pixels.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
356
+ result = Surface.from_pixels(pixels)
357
+ else:
358
+ result = Surface.from_node(Node(surface.get_size(), (dict(kind='flip', source=node,
359
+ horizontal=horizontal, vertical=vertical),)))
360
+ result._alpha, result.opaque = surface._alpha, surface.opaque
361
+ return result
362
+
363
+ @staticmethod
364
+ def _rotate(surface, angle, scale, smooth):
365
+ w, h = surface.get_size()
366
+ if smooth:
367
+ # pygame's rotozoom binding accepts C floats before calculating
368
+ # the output bounds. Preserve that rounding at integer edges.
369
+ angle, scale = struct.unpack('ff', struct.pack('ff', angle, scale))
370
+ cosine, sine = abs(math.cos(math.radians(angle))), abs(math.sin(math.radians(angle)))
371
+ if smooth and angle % 360:
372
+ size = (max(1, math.ceil((w // 2 * cosine + h // 2 * sine) * scale)) * 2,
373
+ max(1, math.ceil((w // 2 * sine + h // 2 * cosine) * scale)) * 2)
374
+ else:
375
+ size = (max(1, int((w * cosine + h * sine) * scale + 1e-9)),
376
+ max(1, int((w * sine + h * cosine) * scale + 1e-9)))
377
+ result = Surface.from_node(Node(size, (dict(kind='rotate', source=surface.snapshot(),
378
+ angle=angle, scale=scale, smooth=smooth),)))
379
+ result._alpha = surface._alpha
380
+ return result
381
+
382
+ @classmethod
383
+ def rotate(cls, surface, angle):
384
+ return cls._rotate(surface, angle, 1, False)
385
+
386
+ @classmethod
387
+ def rotozoom(cls, surface, angle, scale):
388
+ return cls._rotate(surface, angle, scale, True)
389
+
390
+
391
+ def raster(node):
392
+ """Realize a small asset mask for the shared portrait/tint algorithms.
393
+
394
+ Whole frames are drawn by the native host, not encoded as Python bitmaps.
395
+ This also provides a reference image for protocol tests.
396
+ """
397
+ if node.pixels is not None:
398
+ return node.pixels.copy()
399
+ image = Image.new('RGBA', node.size)
400
+ for op in node.ops:
401
+ kind = op['kind']
402
+ if kind in ('scale', 'rotate', 'flip'):
403
+ source = raster(op['source'])
404
+ if kind == 'scale':
405
+ image = source.resize(node.size, Image.Resampling.BILINEAR if op['smooth'] else Image.Resampling.NEAREST)
406
+ elif kind == 'flip':
407
+ image = source
408
+ if op['horizontal']:
409
+ image = image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
410
+ if op['vertical']:
411
+ image = image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
412
+ else:
413
+ scale = op['scale']
414
+ if scale != 1:
415
+ source = source.resize((max(1, round(source.width * scale)), max(1, round(source.height * scale))))
416
+ rotated = source.rotate(op['angle'], Image.Resampling.BICUBIC if op['smooth'] else Image.Resampling.NEAREST, expand=True)
417
+ image.paste(rotated, ((image.width - rotated.width) // 2, (image.height - rotated.height) // 2))
418
+ continue
419
+ layer = image.copy()
420
+ if kind == 'blit':
421
+ source = raster(op['source'])
422
+ if op['alpha'] != 255:
423
+ source.putalpha(source.getchannel('A').point(lambda a: a * op['alpha'] // 255))
424
+ layer.alpha_composite(source, op['at'])
425
+ elif kind == 'textured_quad':
426
+ from ..projection import warp
427
+ x, y, w, h = op['bounds']
428
+ source = warp(raster(op['source']), (w, h), op['coefficients'])
429
+ if op['alpha'] != 255:
430
+ source.putalpha(source.getchannel('A').point(lambda a: a * op['alpha'] // 255))
431
+ layer.alpha_composite(source, (x, y))
432
+ elif kind == 'textured_rows':
433
+ source = raster(op['source'])
434
+ source = source.resize((source.width, len(op['rows'])), Image.Resampling.BILINEAR)
435
+ for y, (left, width) in enumerate(op['rows']):
436
+ row = source.crop((0, y, source.width, y + 1)).resize((width, 1), Image.Resampling.BILINEAR)
437
+ if op['alpha'] != 255:
438
+ row.putalpha(row.getchannel('A').point(lambda a: a * op['alpha'] // 255))
439
+ layer.alpha_composite(row, (left, op['top'] + y))
440
+ else:
441
+ draw = ImageDraw.Draw(layer)
442
+ color, width = op['color'], op.get('width', 0)
443
+ if kind in ('fill', 'rect'):
444
+ x, y, w, h = op['rect']
445
+ if w > 0 and h > 0:
446
+ draw.rounded_rectangle((x, y, x + w - 1, y + h - 1), radius=op.get('radius', 0),
447
+ fill=None if width else color, outline=color, width=width)
448
+ elif kind == 'line':
449
+ draw.line(op['points'], fill=color, width=width)
450
+ elif kind == 'polygon':
451
+ draw.polygon(op['points'], fill=None if width else color, outline=color, width=width or 1)
452
+ elif kind == 'circle':
453
+ x, y = op['center']
454
+ r = op['radius']
455
+ draw.ellipse((x - r, y - r, x + r - 1, y + r - 1), fill=None if width else color, outline=color, width=width or 1)
456
+ else:
457
+ raise DrawingError(f'Unsupported drawing operation: {kind}')
458
+ x, y, w, h = op['clip']
459
+ box = x, y, x + w, y + h
460
+ image.paste(layer.crop(box), box)
461
+ return image
462
+
463
+
464
+ class ImageAPI:
465
+ def __init__(self, icon_path):
466
+ self.icon_path = icon_path
467
+
468
+ @staticmethod
469
+ def frombytes(data, size, mode):
470
+ return Surface.from_pixels(Image.frombytes(mode, size, data))
471
+
472
+ @staticmethod
473
+ def load(source):
474
+ try:
475
+ with Image.open(source) as image:
476
+ return Surface.from_pixels(image.convert('RGBA'))
477
+ except (OSError, ValueError) as error:
478
+ raise DrawingError(str(error)) from error
479
+
480
+ @staticmethod
481
+ def tobytes(surface, mode):
482
+ return raster(surface.snapshot()).convert(mode).tobytes()
483
+
484
+ def load_sized_svg(self, source, size):
485
+ # The build renders the project's authored SVG; no game image is bundled.
486
+ return Transform.smoothscale(self.load(self.icon_path), size)
487
+
488
+
489
+ class FrameEncoder:
490
+ def __init__(self):
491
+ self.previous = set()
492
+
493
+ def encode(self, surface):
494
+ nodes, visited = [], set()
495
+ def visit(node):
496
+ if node.identifier in visited:
497
+ return
498
+ visited.add(node.identifier)
499
+ for op in node.ops:
500
+ if 'source' in op:
501
+ visit(op['source'])
502
+ if node.identifier not in self.previous:
503
+ nodes.append(node.record())
504
+ root = surface.snapshot()
505
+ visit(root)
506
+ released = sorted(self.previous - visited)
507
+ self.previous = visited
508
+ return dict(root=root.identifier, nodes=nodes, release=released)
509
+
510
+
511
+ class PlatformFont:
512
+ def __init__(self, bridge, face, size):
513
+ self.bridge, self.face, self.points = bridge, face, max(1, round(size * .75)) if face is None else size
514
+
515
+ @lru_cache(maxsize=512)
516
+ def metrics(self, text):
517
+ return self.bridge(dict(operation='text', face=self.face, size=self.points, text=text, measure=True))
518
+
519
+ def size(self, text):
520
+ result = self.metrics(text)
521
+ return result['width'], result['height']
522
+
523
+ def get_linesize(self):
524
+ return self.metrics('Mg')['height']
525
+
526
+ @lru_cache(maxsize=128)
527
+ def render(self, text, antialias, color):
528
+ result = self.bridge(dict(operation='text', face=self.face, size=self.points, text=text, color=rgba(color)))
529
+ return ImageAPI.load(BytesIO(base64.b64decode(result['png'])))
530
+
531
+
532
+ class Music:
533
+ def __init__(self, owner, channel='music'):
534
+ self.owner, self.channel = owner, channel
535
+
536
+ def command(self, action, **details):
537
+ self.owner.commands.append(dict(kind=f'{self.channel}_{action}', **details))
538
+
539
+ def load(self, stream):
540
+ self.command('load', data=base64.b64encode(stream.read()).decode('ascii'))
541
+
542
+ def play(self, loops=0, *, start=0):
543
+ self.command('play', loops=loops, start=start)
544
+
545
+ def stop(self):
546
+ self.command('stop')
547
+
548
+ def pause(self):
549
+ self.command('pause')
550
+
551
+ def unpause(self):
552
+ self.command('resume')
553
+
554
+
555
+ class Mixer:
556
+ def __init__(self):
557
+ self.commands = []
558
+ self.music = Music(self)
559
+ self.menu_music = Music(self, 'menu')
560
+
561
+ def init(self):
562
+ pass # AVAudioSession is owned by the native host.
563
+
564
+ def stop(self):
565
+ self.commands.append(dict(kind='sounds_stop'))
566
+
567
+ def Sound(self, *, file):
568
+ data = base64.b64encode(file.read()).decode('ascii')
569
+ return SimpleNamespace(play=lambda: self.commands.append(dict(kind='sound', data=data)))
570
+
571
+
572
+ class Backend:
573
+ error, Rect, FRect, Surface = DrawingError, Rect, FRect, Surface
574
+ SRCALPHA, BLEND_RGBA_MULT, RESIZABLE = SRCALPHA, BLEND_RGBA_MULT, RESIZABLE
575
+ # Event values are private to this host. Keyboard digit ordering is used by
576
+ # the shared accessibility/shortcut path, so keep ASCII digits contiguous.
577
+ QUIT, WINDOWFOCUSLOST, WINDOWFOCUSGAINED, DROPBEGIN, DROPFILE, DROPCOMPLETE = range(1, 7)
578
+ KEYDOWN, TEXTINPUT, MOUSEBUTTONDOWN, MOUSEBUTTONUP, MOUSEMOTION, MOUSEWHEEL = range(7, 13)
579
+ K_1, K_5, K_9 = 49, 53, 57
580
+ K_BACKSPACE, K_RETURN, K_ESCAPE, K_SPACE, K_TAB = 8, 13, 27, 32, 9
581
+ K_UP, K_DOWN, K_PAGEUP, K_PAGEDOWN, K_F5, K_F9 = range(256, 262)
582
+
583
+ def __init__(self, bridge, icon_path):
584
+ self.bridge = bridge
585
+ self.ticks = 0
586
+ self.keyboard = False
587
+ self.pointer = (-1, -1)
588
+ self.window = None
589
+ self.image = ImageAPI(icon_path)
590
+ self.draw, self.transform = Draw(), Transform()
591
+ self.mixer = Mixer()
592
+ self.font = SimpleNamespace(init=lambda: None, match_font=lambda *a, **k: 'ArialRoundedMTBold',
593
+ Font=lambda face, size: PlatformFont(bridge, face, size))
594
+ self.time = SimpleNamespace(get_ticks=lambda: self.ticks)
595
+ self.mouse = SimpleNamespace(get_pos=lambda: self.pointer)
596
+ self.key = SimpleNamespace(start_text_input=lambda: self._keyboard(True),
597
+ stop_text_input=lambda: self._keyboard(False), set_text_input_rect=lambda rect: None)
598
+ self.display = SimpleNamespace(init=lambda: None, set_mode=self._window, flip=lambda: None,
599
+ set_icon=lambda icon: None, set_caption=lambda caption: None)
600
+
601
+ def _keyboard(self, enabled):
602
+ self.keyboard = enabled
603
+
604
+ def _window(self, size, flags=0):
605
+ self.window = Surface(size)
606
+ return self.window
607
+
608
+ def quit(self):
609
+ self.mixer.music.stop()
610
+ self.mixer.menu_music.stop()
611
+ self.mixer.stop()
612
+ self.keyboard = False