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/workspace.py ADDED
@@ -0,0 +1,432 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import pygame as pg
6
+
7
+ from .elements import Scene, UIElement
8
+ from . import resources
9
+ from .resources import DEFAULT_ICON_PATH
10
+ from .state import ColorValue, FRect, InputState, Key, MouseButton, Rect
11
+ from .builtin import UIPadding, GlobalElement
12
+
13
+ def init():
14
+ resources.init()
15
+ pg.init()
16
+
17
+ class Workspace:
18
+ def __init__(
19
+ self,
20
+ windowed_width: int = 800,
21
+ windowed_height: int = 600,
22
+ color: ColorValue = (255, 255, 255),
23
+ fps: int = 60,
24
+ name: str | None = None,
25
+ icon: str | Path | None = None,
26
+ ):
27
+ resources.require_init()
28
+
29
+ self.windowed_size = (windowed_width, windowed_height)
30
+
31
+ info = pg.display.Info()
32
+ self.fullscreen_size = (info.current_w, info.current_h)
33
+
34
+ self.size = self.windowed_size
35
+
36
+ self.color = color
37
+ self.fps = fps
38
+ self.running = False
39
+ self.name = name
40
+ self.icon = icon
41
+
42
+ self._children: list[UIElement] = []
43
+ self._scene_stack: list[Scene] = []
44
+ self._modal_stack: list[UIElement] = []
45
+ self._hovered_set: set[UIElement] = set()
46
+ self._hovered_top: UIElement | None = None
47
+ self._last_hovered_set: set[UIElement] = set()
48
+ self._last_hovered_top: UIElement | None = None
49
+ self._scene_changed = False
50
+ self._modal_changed = False
51
+ self.last_scene_change_time = 0.0
52
+ self.last_modal_change_time = 0.0
53
+ self.scene_change_queue: list[Scene | None] = []
54
+ self.scene_push_queue: list[Scene | None] = []
55
+ self.modal_push_queue: list[tuple[UIElement, bool]] = [] # (modal, is_push)
56
+
57
+ self.screen = pg.display.set_mode(self.size)
58
+ self.clock = pg.time.Clock()
59
+
60
+ if self.name:
61
+ pg.display.set_caption(self.name)
62
+ else:
63
+ pg.display.set_caption("PlayPy Window")
64
+
65
+ icon_path = Path(self.icon) if self.icon is not None else DEFAULT_ICON_PATH
66
+ if icon_path and icon_path.exists():
67
+ try:
68
+ pg.display.set_icon(pg.image.load(str(icon_path)))
69
+ except Exception:
70
+ pass
71
+
72
+ # Input tracking
73
+ self.input = InputState()
74
+
75
+ @property
76
+ def children(self) -> list[UIElement]:
77
+ return self._children
78
+
79
+ def add_child(self, child: UIElement, z: int | None = None) -> None:
80
+ if z is not None:
81
+ child.z = z
82
+ child.parent = self
83
+
84
+ def remove_child(self, child: UIElement) -> None:
85
+ child.parent = None
86
+
87
+ @property
88
+ def current_scene(self) -> Scene | None:
89
+ if not self._scene_stack:
90
+ return None
91
+ return self._scene_stack[-1]
92
+
93
+ @property
94
+ def scene_changed(self) -> bool:
95
+ return self._scene_changed
96
+
97
+ @property
98
+ def current_modal(self) -> UIElement | None:
99
+ if not self._modal_stack:
100
+ return None
101
+ return self._modal_stack[-1]
102
+
103
+ @property
104
+ def modal_changed(self) -> bool:
105
+ return self._modal_changed
106
+
107
+ def is_ancestor_of(self, descendant: "UIElement") -> bool:
108
+ return descendant.is_descendant_of(self)
109
+
110
+ def is_parent_of(self, child: "UIElement") -> bool:
111
+ return child._parent is self
112
+
113
+ def _record_scene_change(self, old_scene: Scene | None, new_scene: Scene | None) -> None:
114
+ if old_scene is not new_scene:
115
+ self.previous_scene = old_scene
116
+ self._scene_changed = True
117
+ self.last_scene_change_time = self.input.runtime
118
+
119
+ def _record_modal_change(self, old_modal: UIElement | None, new_modal: UIElement | None) -> None:
120
+ if old_modal is not new_modal:
121
+ self.previous_modal = old_modal
122
+ self._modal_changed = True
123
+ self.last_modal_change_time = self.input.runtime
124
+
125
+ def _resolve_queued_scene_change(self):
126
+ if not self._scene_changed:
127
+ return
128
+ old_scene = self.current_scene
129
+ while self._scene_stack:
130
+ old = self._scene_stack.pop()
131
+ try:
132
+ old.on_exit(self)
133
+ finally:
134
+ self.remove_child(old)
135
+ if self.scene_change_queue:
136
+ scene = self.scene_change_queue.pop(0)
137
+ else:
138
+ scene = None
139
+ if scene is None:
140
+ self._record_scene_change(old_scene, None)
141
+ return
142
+ self._scene_stack.append(scene)
143
+ scene.parent = self
144
+ scene.on_enter(self)
145
+ self._record_scene_change(old_scene, scene)
146
+
147
+ def queue_scene_change(self, scene: Scene | None) -> None:
148
+ self.scene_change_queue.append(scene)
149
+ self._scene_changed = True
150
+
151
+ def _resolve_queued_scene_push(self):
152
+ while self.scene_push_queue:
153
+ scene = self.scene_push_queue.pop(0)
154
+ if scene is None:
155
+ if not self._scene_stack:
156
+ continue
157
+ popped = self._scene_stack.pop()
158
+ try:
159
+ popped.on_exit(self)
160
+ finally:
161
+ self.remove_child(popped)
162
+ current = self.current_scene
163
+ if current is not None:
164
+ current.on_resume(self)
165
+ self._record_scene_change(popped, current)
166
+ else:
167
+ current = self.current_scene
168
+ if current is not None:
169
+ current.on_pause(self)
170
+ self._scene_stack.append(scene)
171
+ scene.parent = self
172
+ scene.on_enter(self)
173
+ self._record_scene_change(current, scene)
174
+
175
+ def queue_scene_push(self, scene: Scene) -> None:
176
+ self.scene_push_queue.append(scene)
177
+
178
+ def queue_scene_pop(self) -> None:
179
+ self.scene_push_queue.append(None)
180
+
181
+ @property
182
+ def has_modal(self) -> bool:
183
+ return bool(self._modal_stack)
184
+
185
+ @property
186
+ def active_modal(self) -> UIElement | None:
187
+ if not self._modal_stack:
188
+ return None
189
+ return self._modal_stack[-1]
190
+
191
+ # pushing a modal should add it to the stack, while popping it should remove it. When a modal is pushed, the old modal (if any) should have its on_pause called, and the new modal should have its on_enter called. When a modal is popped, the old modal should have its on_exit called, and the new top modal (if any) should have its on_resume called.
192
+ def _resolve_queued_modal_push(self):
193
+ while self.modal_push_queue:
194
+ modal, is_push = self.modal_push_queue.pop(0)
195
+ if is_push:
196
+ current = self.active_modal
197
+ if current is not None:
198
+ pause_func = getattr(current, "on_pause", None)
199
+ if callable(pause_func):
200
+ pause_func(modal, self)
201
+ self._modal_stack.append(modal)
202
+ modal.parent = self
203
+ enter_func = getattr(modal, "on_enter", None)
204
+ if callable(enter_func):
205
+ enter_func(modal, self)
206
+ self._record_modal_change(current, modal)
207
+ else:
208
+ if not self._modal_stack:
209
+ continue
210
+ if modal in self._modal_stack:
211
+ was_active = self.active_modal is modal
212
+ self._modal_stack.remove(modal)
213
+ try:
214
+ exit_func = getattr(modal, "on_exit", None)
215
+ if callable(exit_func):
216
+ exit_func(modal, self)
217
+ finally:
218
+ self.remove_child(modal)
219
+ if was_active:
220
+ current = self.active_modal
221
+ if current is not None:
222
+ resume_func = getattr(current, "on_resume", None)
223
+ if callable(resume_func):
224
+ resume_func(modal, self)
225
+ self._record_modal_change(modal, current)
226
+ else:
227
+ continue
228
+
229
+ def queue_modal_push(self, modal: UIElement) -> None:
230
+ self.modal_push_queue.append((modal, True))
231
+
232
+ def queue_modal_pop(self, modal: UIElement) -> None:
233
+ self.modal_push_queue.append((modal, False))
234
+
235
+ def queue_modal_clear(self) -> None:
236
+ for modal in reversed(self._modal_stack):
237
+ self.modal_push_queue.append((modal, False))
238
+
239
+ def get_element_rect(self, element: UIElement) -> pg.Rect:
240
+ parent = element.parent
241
+ if isinstance(parent, UIElement):
242
+ base_rect = self.get_element_rect(parent)
243
+ base_x, base_y = base_rect.x, base_rect.y
244
+ base_w, base_h = base_rect.w, base_rect.h
245
+ if getattr(parent, "scrollable", False):
246
+ base_y -= int(getattr(parent, "scroll_y", 0))
247
+ else:
248
+ base_x, base_y = 0, 0
249
+ base_w, base_h = self.size
250
+
251
+ padding = element.get_modifier(UIPadding)
252
+
253
+ if padding:
254
+ pad_scale = padding.scale
255
+ pad_offset = padding.offset
256
+ else:
257
+ pad_scale = 0.0
258
+ pad_offset = 0
259
+
260
+ rect_x = max(round((element.scale.x + pad_scale) * base_w) + element.offset.x + pad_offset + base_x, 0)
261
+ rect_y = max(round((element.scale.y + pad_scale) * base_h) + element.offset.y + pad_offset + base_y, 0)
262
+ rect_w = max(round((element.scale.w - 2 * pad_scale) * base_w) + element.offset.w - 2 * pad_offset, 0)
263
+ rect_h = max(round((element.scale.h - 2 * pad_scale) * base_h) + element.offset.h - 2 * pad_offset, 0)
264
+ return pg.Rect(rect_x, rect_y, rect_w, rect_h)
265
+
266
+ def _clips_descendants(self, element: UIElement) -> bool:
267
+ return isinstance(element, Scene) or getattr(element, "scrollable", False)
268
+
269
+ def _clip_for_element(self, element: UIElement) -> pg.Rect | None:
270
+ clip: pg.Rect | None = None
271
+ parent = element.parent
272
+ while isinstance(parent, UIElement):
273
+ if self._clips_descendants(parent):
274
+ rect = self.get_element_rect(parent)
275
+ clip = rect if clip is None else clip.clip(rect)
276
+ parent = parent.parent
277
+ return clip
278
+
279
+ def _point_within_clip_ancestors(self, element: UIElement, point: tuple[int, int]) -> bool:
280
+ parent = element.parent
281
+ while isinstance(parent, UIElement):
282
+ if self._clips_descendants(parent):
283
+ if not self.get_element_rect(parent).collidepoint(point):
284
+ return False
285
+ parent = parent.parent
286
+ return True
287
+
288
+ def update_input(self):
289
+ self.input.text_input = []
290
+ self.input.mouse_wheel = 0
291
+ for event in pg.event.get():
292
+ if event.type == pg.QUIT:
293
+ self.input.quit = True
294
+ else:
295
+ self.input.quit = False
296
+
297
+ if event.type == pg.KEYDOWN:
298
+ self.input.keys_pressed.add(Key.from_pygame(event.key))
299
+ elif event.type == pg.KEYUP:
300
+ self.input.keys_pressed.discard(Key.from_pygame(event.key))
301
+
302
+ elif event.type == pg.MOUSEBUTTONDOWN:
303
+ self.input.mouse_buttons_pressed.add(MouseButton.from_pygame(event.button))
304
+ elif event.type == pg.MOUSEBUTTONUP:
305
+ self.input.mouse_buttons_pressed.discard(MouseButton.from_pygame(event.button))
306
+
307
+ elif event.type == pg.MOUSEMOTION:
308
+ self.input.mouse_pos = event.pos
309
+ elif event.type == pg.TEXTINPUT:
310
+ if event.text:
311
+ self.input.text_input.append(event.text)
312
+ elif event.type == pg.MOUSEWHEEL:
313
+ self.input.mouse_wheel += event.y
314
+
315
+ def _is_element_hittable(self, element: UIElement) -> bool:
316
+ if not element.visible or not element.enabled:
317
+ return False
318
+ rect = element.get_rect_px(self)
319
+ if rect.w <= 0 or rect.h <= 0:
320
+ return False
321
+ if not rect.collidepoint(self.input.mouse_pos):
322
+ return False
323
+ return self._point_within_clip_ancestors(element, self.input.mouse_pos)
324
+
325
+ def _update_hover_state(self, draw_order: list[UIElement]) -> None:
326
+ self._last_hovered_set = self._hovered_set
327
+ self._last_hovered_top = self._hovered_top
328
+
329
+ hovered: list[UIElement] = []
330
+ for element in draw_order:
331
+ if self._is_element_hittable(element):
332
+ hovered.append(element)
333
+ self._hovered_set = set(hovered)
334
+ self._hovered_top = hovered[-1] if hovered else None
335
+
336
+ def generate_processing_order(self, target: UIElement | None = None):
337
+ order: list[UIElement] = []
338
+ if target is None:
339
+ targ = self
340
+ else:
341
+ targ = target
342
+ order.append(targ)
343
+
344
+ for child in sorted(targ._children, key=lambda x: x.z):
345
+ order.extend(self.generate_processing_order(child))
346
+ return order
347
+
348
+ def generate_global_processing_order(self) -> list[UIElement]:
349
+ # Generate processing order for elements that are in the workspace but not in the current scene, AND have the global modifier
350
+ order: list[UIElement] = []
351
+ for child in sorted(self._children, key=lambda x: x.z):
352
+ if child not in self._scene_stack and child.get_modifier(GlobalElement):
353
+ order.append(child)
354
+ order.extend(self.generate_processing_order(child))
355
+ return order
356
+
357
+ def _base_processing_order(self) -> list[UIElement]:
358
+ scene = self.current_scene
359
+ if scene is not None:
360
+ return self.generate_processing_order(scene) + self.generate_global_processing_order()
361
+ return self.generate_processing_order()
362
+
363
+ def is_mouse_over(self, element: UIElement) -> bool:
364
+ return element in self._hovered_set
365
+
366
+ def is_mouse_top(self, element: UIElement) -> bool:
367
+ return self._hovered_top is element
368
+
369
+ def just_hovered_inclusive(self, element: UIElement) -> bool:
370
+ return element in self._hovered_set and element not in self._last_hovered_set
371
+
372
+ def just_hovered(self, element: UIElement) -> bool:
373
+ return self._hovered_top is element and self._last_hovered_top is not element
374
+
375
+ def just_unhovered_inclusive(self, element: UIElement) -> bool:
376
+ return element not in self._hovered_set and element in self._last_hovered_set
377
+
378
+ def just_unhovered(self, element: UIElement) -> bool:
379
+ return self._hovered_top is not element and self._last_hovered_top is element
380
+
381
+ def run(self):
382
+ self.running = True
383
+ while self.running:
384
+ self.screen.fill(self.color)
385
+
386
+ base_order = self._base_processing_order()
387
+ modal = self.active_modal
388
+ if modal is not None:
389
+ modal_order = self.generate_processing_order(modal)
390
+ else:
391
+ modal_order = []
392
+ self.update_input()
393
+
394
+ self._resolve_queued_scene_change()
395
+ self._resolve_queued_scene_push()
396
+ self._resolve_queued_modal_push()
397
+
398
+ input_order = modal_order if modal_order else base_order
399
+ self._update_hover_state(base_order + modal_order)
400
+ for inp_target in reversed(input_order):
401
+ if (
402
+ inp_target.block_input_when_occluded
403
+ and self.is_mouse_over(inp_target)
404
+ and not self.is_mouse_top(inp_target)
405
+ ):
406
+ continue
407
+ inp_target.handle_input(self)
408
+
409
+ for draw_target in [*base_order, *modal_order]:
410
+ clip = self._clip_for_element(draw_target)
411
+ if clip is not None:
412
+ prev_clip = self.screen.get_clip()
413
+ self.screen.set_clip(clip)
414
+ draw_target.draw(self)
415
+ self.screen.set_clip(prev_clip)
416
+ else:
417
+ draw_target.draw(self)
418
+
419
+ pg.display.flip()
420
+ self.input.next_frame(self.clock.tick(self.fps) / 1000)
421
+ self._scene_changed = False
422
+ self._modal_changed = False
423
+ pg.quit()
424
+
425
+ def quit(self):
426
+ self.running = False
427
+ self.input.quit = True
428
+
429
+ __all__ = [
430
+ "init",
431
+ "Workspace"
432
+ ]
@@ -0,0 +1,191 @@
1
+ Metadata-Version: 2.4
2
+ Name: PlayPy
3
+ Version: 0.2.1
4
+ Summary: PlayPy is a lightweight Python library for creating simple games and interactive applications with ease. It provides a straightforward API for handling graphics, input, and basic game mechanics, making it ideal for beginners and those looking to quickly prototype their ideas.
5
+ Author-email: angel <angyv2861@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Angel Ventura
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Requires-Python: >=3.14
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: pygame-ce>=2.5.6
31
+ Requires-Dist: colorama>=0.4.6
32
+ Dynamic: license-file
33
+
34
+ # PlayPy (0.2.1)
35
+
36
+ PlayPy is a lightweight Python library for creating simple games and interactive applications with ease. It provides a straightforward API for handling graphics, input, and basic game mechanics, making it ideal for beginners and those looking to quickly prototype their ideas.
37
+
38
+ ## Requirements
39
+
40
+ - Python `>=3.11`
41
+ - `pygame >=2.6.1`
42
+
43
+ ## Installation
44
+
45
+ To install PlayPy, enter the following line into your terminal: `pip install playpy`, or `python -m pip install playpy` if the first line does not work.
46
+
47
+ ## Core Concepts
48
+
49
+ ### Workspace
50
+
51
+ `Workspace` owns the window, render loop, input state, scene stack, and modal stack.
52
+
53
+ Key methods:
54
+
55
+ - `run()` starts runtime loop
56
+ - `quit()` stops runtime loop
57
+ - `queue_scene_change(scene)` replaces the current scene
58
+ - `queue_scene_push(scene)` / `queue_scene_pop()` stacks scenes
59
+ - `queue_modal_push(element)` / `queue_modal_pop()` manages overlays
60
+
61
+ ### Layout Values
62
+
63
+ PlayPy uses two rectangle value types:
64
+
65
+ - `FRect(*args)`
66
+ Relative scale (fraction of parent rectangle).
67
+ - `Rect(*args)`
68
+ Absolute pixel offsets applied on top of scale.
69
+ - Overloads are: `x, y, w, h` \ `topleft, size` \ `(x, y, w, h)` \ `(topleft, size)`
70
+
71
+ Final element rect = `scale * parent_size + offset`.
72
+
73
+ ### Parenting
74
+
75
+ Any `UIElement` can contain children. Assigning parent wires it automatically:
76
+
77
+ ```python
78
+ child.parent = parent
79
+ ```
80
+
81
+ Or use helpers:
82
+
83
+ ```python
84
+ parent.add_child(child)
85
+ ```
86
+
87
+ You can use the `parent`, `ancestors`, `children`, and `descendants` properties to find ancestors or descendants of an element.
88
+
89
+ You can use other built-in methods to tell descendance:
90
+ - `is_parent_of(child)`, `is_child_of(parent)`
91
+ - `is_ancestor_of(descendant)`, `is_descendant_of(ancestor)`
92
+
93
+ ## Built-in Elements
94
+
95
+ - `UIPanel`: colored rectangle container
96
+ - `UIScrollablePanel`: panel with wheel scrolling
97
+ - `UIText`: wrapped text rendering with alignment
98
+ - `UIButton`: clickable button with hover/pressed colors
99
+ - `UITextbox`: single-line text input with placeholder/caret
100
+ - `Scene`: root container for scene lifecycle
101
+
102
+ ## Modifiers
103
+
104
+ Modifiers attach style/behavior to a single element.
105
+
106
+ - `UIPadding(scale=0, offset=10)` - Adds padding to the element.
107
+ - `UIOutline(color, width, edge_type)` - Adds an outline to the element.
108
+ - `UIBorderRadius(radius)` - Adds a border radius (rounded corners) to the element.
109
+ - `UIGradient(start_color, end_color, direction)` - Converts the background color of the element to a gradient.
110
+ - `UIFont(font_path=None, font_size=None, bold=None, italic=None, antialias=None)` - Changes the font of the text.
111
+ - `GlobalElement()` - If added to a `Workspace` descended element, makes this object shown and handled even when another scene is running.
112
+
113
+ Attach/get/remove:
114
+
115
+ ```python
116
+ element.set_modifier(plp.UIOutline((0, 0, 0), 2, "middle"))
117
+ outline = element.get_modifier(plp.UIOutline)
118
+ element.remove_modifier(plp.UIOutline)
119
+ ```
120
+
121
+ ## Event Helpers
122
+
123
+ Decorator helpers create `Event` elements attached to a workspace, scene, or element.
124
+
125
+ - `@on_start(target)` - Runs when the scene/workspace starts running/
126
+ - `@on_update(target)`
127
+ - `@on_quit(target)`
128
+ - `@on_scene_change(target)`
129
+ - `@on_modal_change(target)`
130
+ - `@create_event(target, condition)` for custom conditions
131
+
132
+ Events created on the main workspace will trigger in all scenes. Pass in `global_event=false` to disable this.
133
+
134
+ Example:
135
+
136
+ ```python
137
+ @plp.on_update(ws)
138
+ def tick(w: plp.Workspace):
139
+ if plp.Key.ESCAPE in w.input.key_downs:
140
+ w.quit()
141
+
142
+ @plp.on_update(ws, global_event=false)
143
+ def tick(w: plp.Workspace):
144
+ if plp.Key.UP in w.input.key_downs:
145
+ w.push_scene(s)
146
+ ```
147
+
148
+ ## Scene and Modal Behavior
149
+
150
+ - If a modal is active, input is only routed to the modal tree
151
+ - Scene descendants are clipped to the scene rectangle for both drawing and hit-testing
152
+ - `Workspace` tracks scene/modal transitions with:
153
+ - `current_scene`, `previous_scene`, `scene_changed`
154
+ - `current_modal`, `previous_modal`, `modal_changed`
155
+ - Scenes can implement lifecycle hooks:
156
+ - `on_enter`, `on_exit`, `on_pause`, `on_resume`
157
+
158
+ ## Input State
159
+
160
+ Read per-frame input from `workspace.input`:
161
+
162
+ - `keys_pressed`, `key_downs`, `key_ups` contain `plp.Key` values
163
+ - `mouse_buttons_pressed`, `mouse_downs`, `mouse_ups` contain `plp.MouseButton` values
164
+ - `mouse_pos`, `mouse_delta`, `mouse_wheel`
165
+ - `text_input`
166
+ - `dt`, `runtime`, `quit`
167
+ - `key_held(key)`, `key_up(key)`, `key_down(key)`
168
+ - `mousebutton_held(mb)`, `mousebutton_up(mb)`, `mousebutton_down(mb)`
169
+
170
+ ### Hover State
171
+
172
+ You can check the hovered objects and top hovered object using the helper methods in `Workspace`.
173
+
174
+ - `is_mouse_top(element)`, `just_hovered(element)`, `just_unhovered(element)` are for checking only the top hovered object.
175
+ - `is_mouse_over(element)`, `just_hovered_inclusive(element)`, `just_unhovered_inclusive(element)` are for checking all of the hovered objects.
176
+
177
+ event helpers come with these too.
178
+
179
+ - `@on_hover(scope, hovered)`, `@on_unhover(scope, hovered)`, `while_hovered(scope, hovered)` for only top.
180
+ - `@on_hover_inclusive(scope, hovered)`, `@on_unhover_inclusive(scope, hovered)`, `while_hovered_inclusive(scope, hovered)` for all.
181
+
182
+ These are useful because they can then be used to check if the user is clicking an object
183
+
184
+ ```python
185
+ obj = plp.UIPanel(plp.FRect(1, 1, 0, 0), plp.empty_rect(), (0, 0, 0))
186
+
187
+ @while_hovered(ws, obj)
188
+ def click_check(w: plp.Workspace):
189
+ if w.input.mousebutton_down(plp.MouseButton.LEFT):
190
+ print("LMB down!")
191
+ ```
@@ -0,0 +1,12 @@
1
+ playpy/__init__.py,sha256=uWWOAXZGnk-h19MM8cxHBMqCbzI1A-52oB9k9uIyKfM,6539
2
+ playpy/builtin.py,sha256=i1c-3iHfBHEuaIzcFwm2TiFbTi9a1z2zTrNYN1T2_Oc,22206
3
+ playpy/elements.py,sha256=wKC5aCvHWSdyLBgUzjisx2_zKFDu2lL7zV5sya9qXB8,5777
4
+ playpy/resources.py,sha256=Xq5t_SIviZL-hk45pvzyH23IEvkIAhaZCBQCkUO_HeU,4258
5
+ playpy/state.py,sha256=lyK4NdSoW2wf_i15B0taSzkCZDRjo8f9ZSZjzRlwA2w,15609
6
+ playpy/workspace.py,sha256=vVv7FvrMVKbzq0z1bAsVlignazLmBsE8O4DzeApb-7Q,16261
7
+ playpy/data/default_icon.ppm,sha256=-NEiMj1mprrnvgQNJU8CWoOwCEO5r4pXRA7VOn68Occ,49
8
+ playpy-0.2.1.dist-info/licenses/LICENSE,sha256=8dZpItcQjCSUQRQPqIygCC-yEEMY7RyXdqfrEuzspb0,1089
9
+ playpy-0.2.1.dist-info/METADATA,sha256=q5dSN2qL4-y3YIF5vuvkq3e3PUYVx1TGeIXsVk4PzYk,7413
10
+ playpy-0.2.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
+ playpy-0.2.1.dist-info/top_level.txt,sha256=h_sU-O668sv_ikUtp4aFsgr4XO_3AWP1XKyWcTxqqRE,7
12
+ playpy-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Angel Ventura
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ playpy