PlayPy 0.2.1__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.
playpy/builtin.py ADDED
@@ -0,0 +1,638 @@
1
+ import pygame as pg
2
+ from pathlib import Path
3
+ from typing import Callable, Literal
4
+ import textwrap
5
+ import os
6
+
7
+ from . import state
8
+ from . import elements
9
+ from . import workspace
10
+ from . import resources
11
+
12
+ __all__ = [
13
+ "UIPadding",
14
+ "UIOutline",
15
+ "UIBorderRadius",
16
+ "UIGradient",
17
+ "UIFont",
18
+ "UIPanel",
19
+ "UIScrollablePanel",
20
+ "UIText",
21
+ "UIButton",
22
+ "UITextbox",
23
+ "Event",
24
+ "create_event",
25
+ "on_start",
26
+ "on_update",
27
+ "on_quit",
28
+ "on_scene_change",
29
+ "on_modal_change",
30
+ "on_hover",
31
+ "on_unhover",
32
+ "while_hovered",
33
+ "on_hover_inclusive",
34
+ "on_unhover_inclusive",
35
+ "while_hovered_inclusive",
36
+ ]
37
+
38
+ class UIPadding(elements.UIModifier):
39
+ def __init__(
40
+ self,
41
+ scale: float = 0,
42
+ offset: int = 10
43
+ ) -> None:
44
+ super().__init__()
45
+ self.scale = scale
46
+ self.offset = offset
47
+
48
+ class UIOutline(elements.UIModifier):
49
+ def __init__(
50
+ self,
51
+ color: state.ColorValue = (0, 0, 0),
52
+ width: int = 5,
53
+ edge_type: Literal["inset", "middle", "outset"] = "middle"
54
+ ):
55
+ super().__init__()
56
+ self.color = color
57
+ self.width = width
58
+ self.edge_type = edge_type
59
+
60
+ class UIBorderRadius(elements.UIModifier):
61
+ def __init__(
62
+ self,
63
+ radius: int = 0,
64
+ ):
65
+ super().__init__()
66
+ self.radius = radius
67
+
68
+ class UIGradient(elements.UIModifier):
69
+ def __init__(
70
+ self,
71
+ start_color: state.ColorValue = (0, 0, 0),
72
+ end_color: state.ColorValue = (255, 255, 255),
73
+ direction: Literal["vertical", "horizontal"] = "vertical",
74
+ ):
75
+ super().__init__()
76
+ self.start_color = start_color
77
+ self.end_color = end_color
78
+ self.direction = direction
79
+
80
+ class UIFont(elements.UIModifier):
81
+ def __init__(
82
+ self,
83
+ font_path: str | Path | None = None,
84
+ font_size: int | None = None,
85
+ bold: bool | None = None,
86
+ italic: bool | None = None,
87
+ antialias: bool | None = None,
88
+ ):
89
+ super().__init__()
90
+ self.font_path = font_path
91
+ self.font_size = font_size
92
+ self.bold = bold
93
+ self.italic = italic
94
+ self.antialias = antialias
95
+
96
+ class GlobalElement(elements.UIModifier):
97
+ def __init__(self):
98
+ super().__init__()
99
+
100
+ def _lerp_color(a: state.ColorValue, b: state.ColorValue, t: float) -> tuple[int, int, int, int]:
101
+ if len(a) == 3:
102
+ a = (a[0], a[1], a[2], 255)
103
+ if len(b) == 3:
104
+ b = (b[0], b[1], b[2], 255)
105
+ return (
106
+ round(a[0] + (b[0] - a[0]) * t),
107
+ round(a[1] + (b[1] - a[1]) * t),
108
+ round(a[2] + (b[2] - a[2]) * t),
109
+ round(a[3] + (b[3] - a[3]) * t),
110
+ )
111
+
112
+
113
+ def _resolve_font_path(font_path: str | Path | None) -> str | None:
114
+ if font_path is None:
115
+ return None
116
+ path = Path(font_path)
117
+ if not path.is_absolute():
118
+ candidate = os.curdir / path
119
+ if candidate.exists():
120
+ return str(candidate)
121
+ if path.exists():
122
+ return str(path)
123
+ return None
124
+
125
+ def _wrap_text_to_width(font: pg.font.Font, text: str, max_width: int) -> list[str]:
126
+ if max_width <= 0:
127
+ return text.splitlines() if text else [""]
128
+ lines: list[str] = []
129
+ for raw_line in text.splitlines() or [""]:
130
+ if raw_line == "":
131
+ lines.append("")
132
+ continue
133
+ words = raw_line.split(" ")
134
+ current = ""
135
+ for word in words:
136
+ candidate = word if current == "" else f"{current} {word}"
137
+ if font.size(candidate)[0] <= max_width:
138
+ current = candidate
139
+ else:
140
+ if current:
141
+ lines.append(current)
142
+ # If a single word is too long, hard-wrap it.
143
+ if font.size(word)[0] > max_width:
144
+ # rough wrap by characters, then refine with font size
145
+ approx = textwrap.wrap(word, width=max(1, int(len(word) * max_width / max(font.size(word)[0], 1))))
146
+ for chunk in approx[:-1]:
147
+ lines.append(chunk)
148
+ current = approx[-1] if approx else ""
149
+ else:
150
+ current = word
151
+ lines.append(current)
152
+ return lines
153
+
154
+
155
+ class UIPanel(elements.UIElement):
156
+ def __init__(
157
+ self,
158
+ scale: state.FRect,
159
+ offset: state.Rect,
160
+ color: state.ColorValue,
161
+ visible: bool = True,
162
+ enabled: bool = True,
163
+ z: int = 0,
164
+ scrollable: bool = False,
165
+ scroll_speed: int = 40,
166
+ ):
167
+ super().__init__(scale, offset, visible, enabled, z)
168
+ self.color = color
169
+ self.scrollable = scrollable
170
+ self.scroll_speed = scroll_speed
171
+ self.scroll_y = 0
172
+
173
+ def handle_input(self, workspace: workspace.Workspace):
174
+ if not self.scrollable:
175
+ return
176
+ panel_rect = self.get_rect_px(workspace)
177
+ if panel_rect.collidepoint(workspace.input.mouse_pos):
178
+ if workspace.input.mouse_wheel != 0:
179
+ self.scroll_y -= workspace.input.mouse_wheel * self.scroll_speed
180
+ self.scroll_y = max(0, self.scroll_y)
181
+
182
+ def draw(self, workspace: "workspace.Workspace"):
183
+ radius_modifier = self.get_modifier(UIBorderRadius)
184
+ border_radius = -1
185
+
186
+ if radius_modifier is not None:
187
+ border_radius = radius_modifier.radius
188
+
189
+ rect = self.get_rect_px(workspace)
190
+
191
+ gradient_modifier = self.get_modifier(UIGradient)
192
+ if gradient_modifier is not None and rect.w > 0 and rect.h > 0:
193
+ surf = pg.Surface((rect.w, rect.h), pg.SRCALPHA)
194
+ if gradient_modifier.direction == "horizontal":
195
+ denom = max(1, rect.w - 1)
196
+ for x in range(rect.w):
197
+ t = x / denom
198
+ color = _lerp_color(gradient_modifier.start_color, gradient_modifier.end_color, t)
199
+ pg.draw.line(surf, color, (x, 0), (x, rect.h - 1))
200
+ else:
201
+ denom = max(1, rect.h - 1)
202
+ for y in range(rect.h):
203
+ t = y / denom
204
+ color = _lerp_color(gradient_modifier.start_color, gradient_modifier.end_color, t)
205
+ pg.draw.line(surf, color, (0, y), (rect.w - 1, y))
206
+ if border_radius >= 0:
207
+ mask = pg.Surface((rect.w, rect.h), pg.SRCALPHA)
208
+ pg.draw.rect(mask, (255, 255, 255, 255), mask.get_rect(), border_radius=border_radius)
209
+ surf.blit(mask, (0, 0), special_flags=pg.BLEND_RGBA_MULT)
210
+ workspace.screen.blit(surf, rect.topleft)
211
+ else:
212
+ pg.draw.rect(workspace.screen, self.color, rect, border_radius=border_radius)
213
+
214
+ outline_modifier = self.get_modifier(UIOutline)
215
+ if outline_modifier is not None:
216
+ xy_offset = (-2 if outline_modifier.edge_type == "outset" else -1 if outline_modifier.edge_type == "middle" else 0) * outline_modifier.width // 2
217
+
218
+ out_rect = rect.copy()
219
+
220
+ out_rect.x += xy_offset
221
+ out_rect.y += xy_offset
222
+ out_rect.w -= xy_offset * 2
223
+ out_rect.h -= xy_offset * 2
224
+
225
+ pg.draw.rect(workspace.screen, outline_modifier.color, out_rect, outline_modifier.width, border_radius=border_radius)
226
+
227
+
228
+ class UIScrollablePanel(UIPanel):
229
+ def __init__(
230
+ self,
231
+ scale: state.FRect,
232
+ offset: state.Rect,
233
+ color: state.ColorValue,
234
+ visible: bool = True,
235
+ enabled: bool = True,
236
+ z: int = 0,
237
+ scroll_speed: int = 40,
238
+ ):
239
+ super().__init__(scale, offset, color, visible, enabled, z, scrollable=True, scroll_speed=scroll_speed)
240
+
241
+
242
+ class UIText(elements.UIElement):
243
+ def __init__(
244
+ self,
245
+ scale: state.FRect,
246
+ offset: state.Rect,
247
+ text: str,
248
+ color: state.ColorValue = (0, 0, 0),
249
+ align: Literal[
250
+ "topleft", "topright", "midtop",
251
+ "midleft", "center", "midright",
252
+ "bottomleft", "bottomright", "midbottom",
253
+ ] = "topleft",
254
+ visible: bool = True,
255
+ enabled: bool = True,
256
+ z: int = 0,
257
+ ):
258
+ super().__init__(scale, offset, visible, enabled, z)
259
+ self.text = text
260
+ self.color = color
261
+ self.align = align
262
+
263
+ def draw(self, workspace: "workspace.Workspace"):
264
+ font_mod = self.get_modifier(UIFont)
265
+ font_path = None
266
+ font_size = 24
267
+ bold = False
268
+ italic = False
269
+ antialias = True
270
+
271
+ if font_mod is not None:
272
+ if font_mod.font_path is not None:
273
+ font_path = _resolve_font_path(font_mod.font_path)
274
+ if font_mod.font_size is not None:
275
+ font_size = font_mod.font_size
276
+ if font_mod.bold is not None:
277
+ bold = font_mod.bold
278
+ if font_mod.italic is not None:
279
+ italic = font_mod.italic
280
+ if font_mod.antialias is not None:
281
+ antialias = font_mod.antialias
282
+
283
+ font = pg.font.Font(font_path, font_size)
284
+ font.set_bold(bold)
285
+ font.set_italic(italic)
286
+ rect = self.get_rect_px(workspace)
287
+ lines = _wrap_text_to_width(font, self.text, rect.w)
288
+ line_h = font.get_linesize()
289
+ block_h = line_h * len(lines)
290
+
291
+ # Determine starting position based on alignment
292
+ if "top" in self.align:
293
+ start_y = rect.y
294
+ elif "bottom" in self.align:
295
+ start_y = rect.bottom - block_h
296
+ else:
297
+ start_y = rect.centery - block_h // 2
298
+
299
+ if "left" in self.align:
300
+ start_x = rect.x
301
+ elif "right" in self.align:
302
+ start_x = rect.right
303
+ else:
304
+ start_x = rect.centerx
305
+
306
+ for i, line in enumerate(lines):
307
+ surface = font.render(line, antialias, self.color)
308
+ text_rect = surface.get_rect()
309
+ if "left" in self.align:
310
+ text_rect.x = start_x
311
+ elif "right" in self.align:
312
+ text_rect.right = start_x
313
+ else:
314
+ text_rect.centerx = start_x
315
+ text_rect.y = start_y + i * line_h
316
+ workspace.screen.blit(surface, text_rect)
317
+
318
+ def handle_input(self, workspace: workspace.Workspace):
319
+ pass
320
+
321
+ class UIButton(UIPanel):
322
+ def __init__(
323
+ self,
324
+ scale: state.FRect,
325
+ offset: state.Rect,
326
+ text: str,
327
+ on_click: Callable[[workspace.Workspace], None] | None = None,
328
+ color: state.ColorValue = (200, 200, 200),
329
+ hover_color: state.ColorValue | None = None,
330
+ pressed_color: state.ColorValue | None = None,
331
+ text_color: state.ColorValue = (0, 0, 0),
332
+ font_path: str | None = None,
333
+ font_size: int = 24,
334
+ visible: bool = True,
335
+ enabled: bool = True,
336
+ z: int = 0,
337
+ ):
338
+ super().__init__(scale, offset, color, visible, enabled, z)
339
+ self.on_click = on_click
340
+ self.idle_color = color
341
+ self.hover_color = hover_color if hover_color is not None else color
342
+ self.pressed_color = pressed_color if pressed_color is not None else color
343
+ self._pressed = False
344
+ self._label = UIText(
345
+ state.FRect(0, 0, 1, 1),
346
+ state.Rect(0, 0, 0, 0),
347
+ text=text,
348
+ color=text_color,
349
+ align="center",
350
+ enabled=False,
351
+ z=1,
352
+ )
353
+ self._label.parent = self
354
+ self._font_mod = UIFont(font_path=font_path, font_size=font_size)
355
+ self._label.set_modifier(self._font_mod)
356
+
357
+ @property
358
+ def label(self):
359
+ return self._label
360
+
361
+ @property
362
+ def text(self):
363
+ return self._label.text
364
+
365
+ @text.setter
366
+ def text(self, value: str):
367
+ self._label.text = value
368
+
369
+ @property
370
+ def text_color(self):
371
+ return self._label.color
372
+
373
+ @text_color.setter
374
+ def text_color(self, value: state.ColorValue):
375
+ self._label.color = value
376
+
377
+ @property
378
+ def font_path(self):
379
+ return self._font_mod.font_path
380
+
381
+ @font_path.setter
382
+ def font_path(self, value: str | None):
383
+ self._font_mod.font_path = value
384
+
385
+ @property
386
+ def font_size(self):
387
+ return self._font_mod.font_size
388
+
389
+ @font_size.setter
390
+ def font_size(self, value: int):
391
+ self._font_mod.font_size = value
392
+
393
+ def draw(self, workspace: "workspace.Workspace"):
394
+ self.color = self.idle_color
395
+ if self.is_mouse_over(workspace):
396
+ self.color = self.hover_color
397
+ if self._pressed:
398
+ self.color = self.pressed_color
399
+ super().draw(workspace)
400
+
401
+ def handle_input(self, workspace: workspace.Workspace):
402
+ if self.is_mouse_top(workspace) and self.is_mouse_over(workspace):
403
+ if workspace.input.mousebutton_down(state.MouseButton.LEFT):
404
+ self._pressed = True
405
+ if self._pressed and workspace.input.mousebutton_up(state.MouseButton.LEFT):
406
+ if self.is_mouse_top(workspace):
407
+ if self.on_click is not None:
408
+ self.on_click(workspace)
409
+ self._pressed = False
410
+
411
+
412
+ class UITextbox(UIPanel):
413
+ def __init__(
414
+ self,
415
+ scale: state.FRect,
416
+ offset: state.Rect,
417
+ text: str = "",
418
+ placeholder: str = "",
419
+ color: state.ColorValue = (255, 255, 255),
420
+ font_path: str | None = None,
421
+ font_size: int = 24,
422
+ text_color: state.ColorValue = (0, 0, 0),
423
+ placeholder_color: state.ColorValue = (120, 120, 120),
424
+ align: Literal['topleft', 'topright', 'midtop', 'midleft', 'center', 'midright', 'bottomleft', 'bottomright', 'midbottom'] = "topleft",
425
+ max_length: int | None = None,
426
+ visible: bool = True,
427
+ enabled: bool = True,
428
+ z: int = 0,
429
+ ):
430
+ super().__init__(scale, offset, color, visible, enabled, z)
431
+ self.text = text
432
+ self.placeholder = placeholder
433
+ self.text_color = text_color
434
+ self.placeholder_color = placeholder_color
435
+ self._value_label = UIText(state.FRect(0, 0, 1, 1), state.Rect(0, 0, 0, 0), "", self.placeholder_color, align, enabled=False)
436
+ self._value_label.parent = self
437
+ self._font_mod = UIFont(font_path=font_path, font_size=font_size)
438
+ self._value_label.set_modifier(self._font_mod)
439
+ self.max_length = max_length
440
+ self.focused = False
441
+ self._caret_visible = True
442
+ self._caret_timer = 0.0
443
+
444
+ @property
445
+ def value_label(self):
446
+ return self._value_label
447
+
448
+ @property
449
+ def font_path(self):
450
+ return self._font_mod.font_path
451
+
452
+ @font_path.setter
453
+ def font_path(self, value: str | None):
454
+ self._font_mod.font_path = value
455
+
456
+ @property
457
+ def font_size(self):
458
+ return self._font_mod.font_size
459
+
460
+ @font_size.setter
461
+ def font_size(self, value: int):
462
+ self._font_mod.font_size = value
463
+
464
+ def draw(self, workspace: "workspace.Workspace"):
465
+ super().draw(workspace)
466
+ display_text = self.text if self.text else self.placeholder
467
+ color = self.text_color if self.text else self.placeholder_color
468
+ self._value_label.text = display_text
469
+ self._value_label.color = color
470
+
471
+ rect = self.get_rect_px(workspace)
472
+
473
+ if self.focused:
474
+ self._caret_timer += workspace.input.dt
475
+ if self._caret_timer > 0.5:
476
+ self._caret_timer = 0.0
477
+ self._caret_visible = not self._caret_visible
478
+ if self._caret_visible:
479
+ caret_x = self._value_label.get_rect_px(workspace).right + 2
480
+ caret_y1 = rect.y + 6
481
+ caret_y2 = rect.y + rect.h - 6
482
+ pg.draw.line(workspace.screen, self.text_color, (caret_x, caret_y1), (caret_x, caret_y2), 2)
483
+
484
+ def handle_input(self, workspace: workspace.Workspace):
485
+ if workspace.input.mousebutton_down(state.MouseButton.LEFT):
486
+ self.focused = self.is_mouse_top(workspace) and self.is_mouse_over(workspace)
487
+
488
+ if not self.focused:
489
+ return
490
+
491
+ for key in workspace.input.key_downs:
492
+ if key == state.Key.BACKSPACE:
493
+ self.text = self.text[:-1]
494
+ elif key == state.Key.RETURN:
495
+ pass
496
+
497
+ for chunk in workspace.input.text_input:
498
+ if not chunk:
499
+ continue
500
+ if any(ord(ch) < 32 for ch in chunk):
501
+ continue
502
+ if self.max_length is not None and len(self.text) >= self.max_length:
503
+ break
504
+ self.text += chunk
505
+
506
+ class Event(elements.UIElement):
507
+ def __init__(
508
+ self,
509
+ condition_function: Callable[[workspace.Workspace], bool],
510
+ event_function: Callable[[workspace.Workspace], None],
511
+ negative_function: Callable[[workspace.Workspace], None] | None = None,
512
+ enabled: bool = True,
513
+ once: bool = False,
514
+ global_event: bool = True,
515
+ ):
516
+ super().__init__(state.empty_frect(), state.empty_rect(), False, enabled, 9999)
517
+ self.event_func = event_function
518
+ self.cond_func = condition_function
519
+ self.neg_func = negative_function
520
+ self.once = once
521
+
522
+ if global_event:
523
+ GlobalElement().parent = self
524
+
525
+ def draw(self, workspace: workspace.Workspace):
526
+ pass
527
+
528
+ def handle_input(self, workspace: workspace.Workspace):
529
+ if self.cond_func(workspace):
530
+ self.event_func(workspace)
531
+ if self.once:
532
+ self.enabled = False
533
+ elif self.neg_func:
534
+ self.neg_func(workspace)
535
+ if self.once:
536
+ self.enabled = False
537
+
538
+ def create_event(target: workspace.Workspace | elements.UIElement, cond_func: Callable[[workspace.Workspace], bool]):
539
+ def wrapper(event_func: Callable[[workspace.Workspace], None]):
540
+ new_event = Event(cond_func, event_func)
541
+ new_event.parent = target
542
+ return wrapper
543
+
544
+
545
+ def on_start(target: workspace.Workspace | elements.UIElement):
546
+ def wrapper(event_func: Callable[[workspace.Workspace], None]):
547
+ if isinstance(target, elements.Scene):
548
+ new_event = Event(lambda w, scene=target: w.current_scene is scene, event_func)
549
+ new_event.parent = target
550
+ return
551
+ elif isinstance(target, elements.UIElement):
552
+ new_event = Event(lambda w, modal=target: w.current_modal is modal, event_func)
553
+ new_event.parent = target
554
+ return
555
+ new_event = Event(lambda w: w.input.runtime == 0, event_func, once=True)
556
+ new_event.parent = target
557
+ return wrapper
558
+
559
+
560
+ def on_update(target: workspace.Workspace | elements.UIElement):
561
+ def wrapper(event_func: Callable[[workspace.Workspace], None]):
562
+ if isinstance(target, elements.Scene):
563
+ new_event = Event(lambda w, scene=target: w.current_scene is scene, event_func)
564
+ new_event.parent = target
565
+ return
566
+ elif isinstance(target, elements.UIElement):
567
+ new_event = Event(lambda w, modal=target: w.current_modal is modal, event_func)
568
+ new_event.parent = target
569
+ return
570
+ new_event = Event(lambda w: True, event_func)
571
+ new_event.parent = target
572
+ return wrapper
573
+
574
+
575
+ def on_quit(target: workspace.Workspace | elements.UIElement):
576
+ def wrapper(event_func: Callable[[workspace.Workspace], None]):
577
+ new_event = Event(lambda w: w.input.quit, event_func)
578
+ new_event.parent = target
579
+ return wrapper
580
+
581
+
582
+ def on_scene_change(target: workspace.Workspace | elements.UIElement):
583
+ def wrapper(event_func: Callable[[workspace.Workspace], None]):
584
+ if isinstance(target, elements.Scene):
585
+ new_event = Event(
586
+ lambda w, scene=target: w.scene_changed and w.current_scene is scene,
587
+ event_func,
588
+ )
589
+ new_event.parent = target
590
+ return
591
+ new_event = Event(lambda w: w.scene_changed, event_func)
592
+ new_event.parent = target
593
+ return wrapper
594
+
595
+
596
+ def on_modal_change(target: workspace.Workspace | elements.UIElement):
597
+ def wrapper(event_func: Callable[[workspace.Workspace], None]):
598
+ if isinstance(target, elements.UIElement) and not isinstance(target, elements.Scene):
599
+ new_event = Event(
600
+ lambda w, modal=target: w.modal_changed and w.active_modal is modal,
601
+ event_func,
602
+ )
603
+ new_event.parent = target
604
+ return
605
+ new_event = Event(lambda w: w.modal_changed, event_func)
606
+ new_event.parent = target
607
+ return wrapper
608
+
609
+ def on_hover(target: workspace.Workspace | elements.UIElement, hovered: elements.UIElement):
610
+ def wrapper(event_func: Callable[[workspace.Workspace], None]):
611
+ Event(lambda w: w.just_hovered(hovered), event_func).parent = target
612
+ return wrapper
613
+
614
+ def on_unhover(target: workspace.Workspace | elements.UIElement, hovered: elements.UIElement):
615
+ def wrapper(event_func: Callable[[workspace.Workspace], None]):
616
+ Event(lambda w: w.just_unhovered(hovered), event_func).parent = target
617
+ return wrapper
618
+
619
+ def while_hovered(target: workspace.Workspace | elements.UIElement, hovered: elements.UIElement):
620
+ def wrapper(event_func: Callable[[workspace.Workspace], None]):
621
+ Event(lambda w: w.is_mouse_top(hovered), event_func).parent = target
622
+ return wrapper
623
+
624
+ def on_hover_inclusive(target: workspace.Workspace | elements.UIElement, hovered: elements.UIElement):
625
+ def wrapper(event_func: Callable[[workspace.Workspace], None]):
626
+ Event(lambda w: w.just_hovered_inclusive(hovered), event_func).parent = target
627
+ return wrapper
628
+
629
+ def on_unhover_inclusive(target: workspace.Workspace | elements.UIElement, hovered: elements.UIElement):
630
+ def wrapper(event_func: Callable[[workspace.Workspace], None]):
631
+ Event(lambda w: w.just_unhovered_inclusive(hovered), event_func).parent = target
632
+ return wrapper
633
+
634
+ def while_hovered_inclusive(target: workspace.Workspace | elements.UIElement, hovered: elements.UIElement):
635
+ def wrapper(event_func: Callable[[workspace.Workspace], None]):
636
+ Event(lambda w: w.is_mouse_over(hovered), event_func).parent = target
637
+ return wrapper
638
+
@@ -0,0 +1,5 @@
1
+ P3
2
+ 2 2
3
+ 255
4
+ 20 20 20 50 50 50
5
+ 50 50 50 20 20 20