simple-graphics-lib 0.0.7__tar.gz → 0.0.9__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simple-graphics-lib
3
- Version: 0.0.7
3
+ Version: 0.0.9
4
4
  Summary: A simple graphics library for python learners
5
5
  Author-email: Max-Hillyer <maxhillyer7012@gmail.com>
6
6
  License-Expression: MIT
@@ -63,6 +63,8 @@ Arguments:
63
63
  | height | int | the height of the rect | 10 |
64
64
  | color | str | the color of the rect | "black" |
65
65
  | outline | bool | whether the rect is just an outline | False |
66
+ | draggable | bool | whether the rect can be dragged | False |
67
+ | visible | bool | whether the rect is drawn on screen | True |
66
68
 
67
69
  Methods:
68
70
  | Method | Purpose | Arguments | Returns |
@@ -80,6 +82,8 @@ Arguments:
80
82
  | radius | int | the radius of the circle | 10 |
81
83
  | color | str | the color of the circle | "black" |
82
84
  | outline | bool | whether the circle is just an outline | False |
85
+ | draggable | bool | whether the circle can be dragged | False |
86
+ | visible | bool | whether the circle is drawn on screen | True |
83
87
 
84
88
  Methods:
85
89
  | Method | Purpose | Arguments | Returns |
@@ -94,6 +98,8 @@ Arguments:
94
98
  | points | list[tuple] | the vertices of the polygon | None |
95
99
  | color | str | the color of the polygon | "black" |
96
100
  | outline | bool | whther the polygon is just an outline | False |
101
+ | draggable | bool | whether the polygon can be dragged | False |
102
+ | visible | bool | whether the polygon is drawn on screen | True |
97
103
 
98
104
  Methods:
99
105
  | Method | Purpose | Arguments | Returns |
@@ -107,6 +113,8 @@ Arguments:
107
113
  | points | list[tuple] |the points that the line will connect | None |
108
114
  | width | int | the width of the line | 10 |
109
115
  | color | str | the color of the line | "black" |
116
+ | draggable | bool | whether the line can be dragged | False |
117
+ | visible | bool | whether the line is drawn on screen | True |
110
118
 
111
119
  Methods:
112
120
  | Method | Purpose | Arguments | Returns |
@@ -122,6 +130,8 @@ Arguments:
122
130
  | text | str | the text of the text | None |
123
131
  | color | str | the color of the text | "black" |
124
132
  | size | int | the size of the text | 36 |
133
+ | draggable | bool | whether the text can be dragged | False |
134
+ | visible | bool | whether the text is drawn on screen | True |
125
135
 
126
136
  Methods:
127
137
  | Method | Purpose | Arguments | Returns |
@@ -137,6 +147,8 @@ Arguments:
137
147
  | img_path | str | the path to the image | None |
138
148
  | width | int | the width of the image | the width from the image path |
139
149
  | height | int | the height of the image | the height from the image path |
150
+ | draggable | bool | whether the image can be dragged | False |
151
+ | visible | bool | whether the image is drawn on screen | True |
140
152
 
141
153
  Methods:
142
154
  | Method | Purpose | Arguments | Returns |
@@ -160,7 +172,8 @@ group.color = "green"
160
172
  Arguments:
161
173
  | Argument | Type | Role | Default |
162
174
  | :--- | :---: | :---: | ---: |
163
- | *shapes | list[Shape] | the shapes to group | None |
175
+ | *shapes | list[Shape] | the shapes to group | None |
176
+ | visible | bool | whether shapes in group are drawn on screen | True |
164
177
 
165
178
  Methods:
166
179
  | Method | Purpose | Arguments | Returns |
@@ -308,6 +321,33 @@ def print_click():
308
321
  run()
309
322
  ```
310
323
 
324
+ ### on_hover
325
+ functions defined with the `@on_hover` decorator will run whenever the specified object is being hovered over
326
+
327
+ the function below will run whenever the mouse is over the rectangle
328
+ ```python
329
+ r1 = Rect(200,200, 100, 100)
330
+
331
+ @on_hover(r1)
332
+ def p():
333
+ print("hovering")
334
+
335
+ run(400,400)
336
+ ```
337
+
338
+ ### on_drag
339
+ functions defined with the `@on_drag` decorator will run whenever the specified object is being dragged
340
+
341
+ the function below will run whenevr the r1 rect is being dragged
342
+ ```python
343
+ r1 = Rect(200,200,draggable = True)
344
+
345
+ @on_drag(r1)
346
+ def do_something():
347
+ print("dragging r1")
348
+
349
+ run(400,400)
350
+ ```
311
351
  ---
312
352
  ## The mouse
313
353
  A mouse object is already initialized in the Simple Graphics import
@@ -322,6 +362,15 @@ def main():
322
362
 
323
363
  run()
324
364
  ```
365
+
366
+ ## dragging
367
+
368
+ to make a shape draggable, add the keyword argument to the object initialization:
369
+ ```python
370
+ r = Rect(250,250, draggable = True)
371
+ ```
372
+ this works for all shapes including images and text
373
+
325
374
  ---
326
375
  ## Miscellaneous Functions
327
376
 
@@ -47,6 +47,8 @@ Arguments:
47
47
  | height | int | the height of the rect | 10 |
48
48
  | color | str | the color of the rect | "black" |
49
49
  | outline | bool | whether the rect is just an outline | False |
50
+ | draggable | bool | whether the rect can be dragged | False |
51
+ | visible | bool | whether the rect is drawn on screen | True |
50
52
 
51
53
  Methods:
52
54
  | Method | Purpose | Arguments | Returns |
@@ -64,6 +66,8 @@ Arguments:
64
66
  | radius | int | the radius of the circle | 10 |
65
67
  | color | str | the color of the circle | "black" |
66
68
  | outline | bool | whether the circle is just an outline | False |
69
+ | draggable | bool | whether the circle can be dragged | False |
70
+ | visible | bool | whether the circle is drawn on screen | True |
67
71
 
68
72
  Methods:
69
73
  | Method | Purpose | Arguments | Returns |
@@ -78,6 +82,8 @@ Arguments:
78
82
  | points | list[tuple] | the vertices of the polygon | None |
79
83
  | color | str | the color of the polygon | "black" |
80
84
  | outline | bool | whther the polygon is just an outline | False |
85
+ | draggable | bool | whether the polygon can be dragged | False |
86
+ | visible | bool | whether the polygon is drawn on screen | True |
81
87
 
82
88
  Methods:
83
89
  | Method | Purpose | Arguments | Returns |
@@ -91,6 +97,8 @@ Arguments:
91
97
  | points | list[tuple] |the points that the line will connect | None |
92
98
  | width | int | the width of the line | 10 |
93
99
  | color | str | the color of the line | "black" |
100
+ | draggable | bool | whether the line can be dragged | False |
101
+ | visible | bool | whether the line is drawn on screen | True |
94
102
 
95
103
  Methods:
96
104
  | Method | Purpose | Arguments | Returns |
@@ -106,6 +114,8 @@ Arguments:
106
114
  | text | str | the text of the text | None |
107
115
  | color | str | the color of the text | "black" |
108
116
  | size | int | the size of the text | 36 |
117
+ | draggable | bool | whether the text can be dragged | False |
118
+ | visible | bool | whether the text is drawn on screen | True |
109
119
 
110
120
  Methods:
111
121
  | Method | Purpose | Arguments | Returns |
@@ -121,6 +131,8 @@ Arguments:
121
131
  | img_path | str | the path to the image | None |
122
132
  | width | int | the width of the image | the width from the image path |
123
133
  | height | int | the height of the image | the height from the image path |
134
+ | draggable | bool | whether the image can be dragged | False |
135
+ | visible | bool | whether the image is drawn on screen | True |
124
136
 
125
137
  Methods:
126
138
  | Method | Purpose | Arguments | Returns |
@@ -144,7 +156,8 @@ group.color = "green"
144
156
  Arguments:
145
157
  | Argument | Type | Role | Default |
146
158
  | :--- | :---: | :---: | ---: |
147
- | *shapes | list[Shape] | the shapes to group | None |
159
+ | *shapes | list[Shape] | the shapes to group | None |
160
+ | visible | bool | whether shapes in group are drawn on screen | True |
148
161
 
149
162
  Methods:
150
163
  | Method | Purpose | Arguments | Returns |
@@ -292,6 +305,33 @@ def print_click():
292
305
  run()
293
306
  ```
294
307
 
308
+ ### on_hover
309
+ functions defined with the `@on_hover` decorator will run whenever the specified object is being hovered over
310
+
311
+ the function below will run whenever the mouse is over the rectangle
312
+ ```python
313
+ r1 = Rect(200,200, 100, 100)
314
+
315
+ @on_hover(r1)
316
+ def p():
317
+ print("hovering")
318
+
319
+ run(400,400)
320
+ ```
321
+
322
+ ### on_drag
323
+ functions defined with the `@on_drag` decorator will run whenever the specified object is being dragged
324
+
325
+ the function below will run whenevr the r1 rect is being dragged
326
+ ```python
327
+ r1 = Rect(200,200,draggable = True)
328
+
329
+ @on_drag(r1)
330
+ def do_something():
331
+ print("dragging r1")
332
+
333
+ run(400,400)
334
+ ```
295
335
  ---
296
336
  ## The mouse
297
337
  A mouse object is already initialized in the Simple Graphics import
@@ -306,6 +346,15 @@ def main():
306
346
 
307
347
  run()
308
348
  ```
349
+
350
+ ## dragging
351
+
352
+ to make a shape draggable, add the keyword argument to the object initialization:
353
+ ```python
354
+ r = Rect(250,250, draggable = True)
355
+ ```
356
+ this works for all shapes including images and text
357
+
309
358
  ---
310
359
  ## Miscellaneous Functions
311
360
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "simple-graphics-lib"
3
- version = "0.0.7"
3
+ version = "0.0.9"
4
4
  authors = [
5
5
  { name="Max-Hillyer", email="maxhillyer7012@gmail.com" },
6
6
  ]
@@ -11,6 +11,9 @@ _ontick = []
11
11
  _key_funcs = {}
12
12
  _key_hold_funcs = {}
13
13
  _mouse_click_funcs = {}
14
+ _hover_funcs = {}
15
+ _dragging_funcs = {}
16
+ _dragging_shape = None
14
17
  _bgcolor = "white"
15
18
  _font = "Arial"
16
19
 
@@ -18,6 +21,9 @@ pygame.init()
18
21
 
19
22
 
20
23
  class Mouse:
24
+ def __init__(self, hiding = False):
25
+ self.hiding = hiding
26
+
21
27
  @property
22
28
  def x(self):
23
29
  return pygame.mouse.get_pos()[0]
@@ -26,20 +32,46 @@ class Mouse:
26
32
  def y(self):
27
33
  return pygame.mouse.get_pos()[1]
28
34
 
35
+ def set_hiding(self, state: bool):
36
+ """Sets the mouse visibility"""
37
+ self.hiding = state
38
+ pygame.mouse.set_visible(self.hiding)
39
+
29
40
 
30
41
  mouse = Mouse()
31
42
 
32
43
 
33
44
  class Shape:
34
- def __init__(self, x, y, color="black", outline=False):
45
+ """Base class for all drawable shapes. Do not instantiate directly."""
46
+ def __init__(self, x, y, color="black", outline=False, draggable=False, visible = True):
47
+ """Initialize a shape at position (x, y).
48
+
49
+ Args:
50
+ x: X coordinate (pixels from left)
51
+ y: Y coordinate (pixels from top)
52
+ color: Color as hex string (#RRGGBB) or name (default: "black")
53
+ outline: If True, draw outline only; if False, fill shape (default: False)
54
+ draggable: If True, shape can be moved by mouse (default: False)
55
+ """
35
56
  self.x = x
36
57
  self.y = y
37
58
  self.color = color
38
59
  self.outline = int(outline)
60
+ self.draggable = draggable
61
+ self.visible = visible
39
62
  _shapes.append(self)
40
63
 
41
64
  def __str__(self):
42
- return self.__class__.__name__.lower()
65
+ """Return string representation showing shape type and parameters"""
66
+ class_name = self.__class__.__name__.lower()
67
+ sig = signature(self.__init__)
68
+ params = [p for p in sig.parameters if p != "self"]
69
+
70
+ values = {}
71
+ for p in params:
72
+ if hasattr(self, p):
73
+ values[p] = getattr(self, p)
74
+ return f"{class_name}: {values}"
43
75
 
44
76
 
45
77
  class Rect(Shape):
@@ -51,8 +83,10 @@ class Rect(Shape):
51
83
  height: int = 10,
52
84
  color: str = "black",
53
85
  outline: bool = False,
86
+ draggable: bool = False,
87
+ visible : bool = True
54
88
  ):
55
- super().__init__(x, y, color, outline)
89
+ super().__init__(x, y, color, outline, draggable, visible)
56
90
  self.width = width
57
91
  self.height = height
58
92
 
@@ -61,6 +95,15 @@ class Rect(Shape):
61
95
  return (self.x, self.y, self.width, self.height)
62
96
 
63
97
  def is_obj_over(self, ox=None, oy=None):
98
+ """Check if a point is inside this rectangle.
99
+
100
+ Args:
101
+ ox: X coordinate (default: current mouse x)
102
+ oy: Y coordinate (default: current mouse y)
103
+
104
+ Returns:
105
+ bool: True if point is inside rectangle, False otherwise
106
+ """
64
107
  if ox == None:
65
108
  ox = pygame.mouse.get_pos()[0]
66
109
  if oy == None:
@@ -70,9 +113,11 @@ class Rect(Shape):
70
113
  return x_over and y_over
71
114
 
72
115
  def get_area(self):
116
+ """Calculate and return the area of the rectangle"""
73
117
  return self.width * self.height
74
118
 
75
119
  def get_perimeter(self):
120
+ """Calculate and return the perimeter of the rectangle"""
76
121
  return (2 * self.width) + (2 * self.height)
77
122
 
78
123
 
@@ -84,11 +129,22 @@ class Circle(Shape):
84
129
  radius: int = 10,
85
130
  color: str = "black",
86
131
  outline: bool = False,
132
+ draggable: bool = False,
133
+ visible: bool = True
87
134
  ):
88
- super().__init__(x, y, color, outline)
135
+ super().__init__(x, y, color, outline, draggable, visible)
89
136
  self.radius = radius
90
137
 
91
138
  def is_obj_over(self, ox=None, oy=None):
139
+ """Check if a point is inside this circle.
140
+
141
+ Args:
142
+ ox: X coordinate (default: current mouse x)
143
+ oy: Y coordinate (default: current mouse y)
144
+
145
+ Returns:
146
+ bool: True if point is inside circle, False otherwise
147
+ """
92
148
  if ox == None:
93
149
  ox = pygame.mouse.get_pos()[0]
94
150
  if oy == None:
@@ -97,19 +153,36 @@ class Circle(Shape):
97
153
  return inside
98
154
 
99
155
  def get_area(self):
156
+ """Calculate and return the area of the circle."""
100
157
  return pi * self.radius**2
101
158
 
102
159
 
103
160
  class Polygon(Shape):
104
161
  def __init__(
105
- self, points: list[tuple], color: str = "black", outline: bool = False
162
+ self,
163
+ points: list[tuple],
164
+ color: str = "black",
165
+ outline: bool = False,
166
+ draggable: bool = False,
167
+ visible: bool = True
106
168
  ):
107
169
  self.color = color
108
170
  self.points = points
109
171
  self.outline = int(outline)
172
+ self.draggable = draggable
173
+ self.visible = visible
110
174
  _shapes.append(self)
111
175
 
112
176
  def is_obj_over(self, ox=None, oy=None):
177
+ """Check if a point is inside this polygon using ray casting algorithm.
178
+
179
+ Args:
180
+ ox: X coordinate (default: current mouse x)
181
+ oy: Y coordinate (default: current mouse y)
182
+
183
+ Returns:
184
+ bool: True if point is inside polygon, False otherwise
185
+ """
113
186
  if ox == None:
114
187
  ox = pygame.mouse.get_pos()[0]
115
188
  if oy == None:
@@ -133,13 +206,31 @@ class Polygon(Shape):
133
206
 
134
207
 
135
208
  class Line(Shape):
136
- def __init__(self, points: list[tuple], width: int = 10, color: str = "black"):
209
+ def __init__(
210
+ self,
211
+ points: list[tuple],
212
+ width: int = 10,
213
+ color: str = "black",
214
+ draggable: bool = False,
215
+ visible: bool = True
216
+ ):
137
217
  self.color = color
138
218
  self.points = points
139
219
  self.width = width
220
+ self.draggable = draggable
221
+ self.visible = visible
140
222
  _shapes.append(self)
141
223
 
142
224
  def is_obj_over(self, ox=None, oy=None):
225
+ """Check if a point is near this line (within line width).
226
+
227
+ Args:
228
+ ox: X coordinate (default: current mouse x)
229
+ oy: Y coordinate (default: current mouse y)
230
+
231
+ Returns:
232
+ bool: True if point is within line width, False otherwise
233
+ """
143
234
  if ox == None:
144
235
  ox = pygame.mouse.get_pos()[0]
145
236
  if oy == None:
@@ -157,6 +248,7 @@ class Line(Shape):
157
248
  return False
158
249
 
159
250
  def _distance_to_segment(self, px, py, x1, y1, x2, y2):
251
+ """Calculate shortest distance from a point to a line segment."""
160
252
  dx = x2 - x1
161
253
  dy = y2 - y1
162
254
 
@@ -173,9 +265,16 @@ class Line(Shape):
173
265
 
174
266
  class Image(Shape):
175
267
  def __init__(
176
- self, x: int, y: int, img_path: str, width: int = None, height: int = None
268
+ self,
269
+ x: int,
270
+ y: int,
271
+ img_path: str,
272
+ width: int = None,
273
+ height: int = None,
274
+ draggable: bool = False,
275
+ visible: bool = True
177
276
  ):
178
- super().__init__(x, y)
277
+ super().__init__(x, y, draggable, visible)
179
278
  self.img_path = img_path
180
279
  self.original_img = pygame.image.load(self.img_path)
181
280
 
@@ -188,6 +287,15 @@ class Image(Shape):
188
287
  )
189
288
 
190
289
  def is_obj_over(self, ox=None, oy=None):
290
+ """Check if a point is inside this image rectangle.
291
+
292
+ Args:
293
+ ox: X coordinate (default: current mouse x)
294
+ oy: Y coordinate (default: current mouse y)
295
+
296
+ Returns:
297
+ bool: True if point is inside image bounds, False otherwise
298
+ """
191
299
  if ox is None:
192
300
  ox = pygame.mouse.get_pos()[0]
193
301
  if oy is None:
@@ -207,17 +315,29 @@ class Text(Shape):
207
315
  color: str = "black",
208
316
  font=_font,
209
317
  size: int = 36,
318
+ draggable: bool = False,
319
+ visible: bool = True
210
320
  ):
211
- super().__init__(x, y, color)
321
+ super().__init__(x, y, color, False, draggable, visible)
212
322
  self.text = text
213
323
  self.size = size
214
324
  self.font = pygame.font.SysFont(font, size)
215
325
 
216
326
  @property
217
327
  def txtsurf(self):
328
+ """Render and return the text surface for drawing on screen."""
218
329
  return self.font.render(str(self.text), True, self.color)
219
330
 
220
331
  def is_obj_over(self, ox=None, oy=None):
332
+ """Check if a point is inside this text's bounding box.
333
+
334
+ Args:
335
+ ox: X coordinate (default: current mouse x)
336
+ oy: Y coordinate (default: current mouse y)
337
+
338
+ Returns:
339
+ bool: True if point is inside text bounds, False otherwise
340
+ """
221
341
  if ox == None:
222
342
  ox = pygame.mouse.get_pos()[0]
223
343
  if oy == None:
@@ -232,7 +352,17 @@ class Text(Shape):
232
352
 
233
353
 
234
354
  class Group:
235
- def __init__(self, *shapes: list[Shape]):
355
+ """Container for grouping multiple shapes for batch operations.
356
+
357
+ Allows moving, coloring, and interacting with multiple shapes as one unit.
358
+ Implements the full Python container protocol (__len__, __iter__, etc.).
359
+ """
360
+ def __init__(self, *shapes: list[Shape], visible:bool = True):
361
+ """Initialize group with optional initial shapes.
362
+
363
+ Args:
364
+ *shapes: Variable number of Shape objects to add initially
365
+ """
236
366
  self.grouped = []
237
367
  self.add(*shapes)
238
368
 
@@ -271,37 +401,98 @@ class Group:
271
401
  for child in self.grouped:
272
402
  child.y += delta
273
403
 
404
+ def __getitem__(self, key):
405
+ return self.grouped[key]
406
+
407
+ def __len__(self):
408
+ return len(self.grouped)
409
+
410
+ def __iter__(self):
411
+ return iter(self.grouped)
412
+
413
+ def __contains__(self, item):
414
+ return item in self.grouped
415
+
416
+ def __setitem__(self, key, value):
417
+ self.grouped[key] = value
418
+
419
+ def __delitem__(self, key):
420
+ del self.grouped[key]
421
+
274
422
  def add(self, *shapes):
423
+ """Add one or more shapes to the group.
424
+
425
+ Args:
426
+ *shapes: One or more Shape objects to add
427
+
428
+ Raises:
429
+ TypeError: If any argument is not a Shape
430
+ """
275
431
  for s in shapes:
276
432
  if not isinstance(s, Shape):
277
433
  raise TypeError(f"{s} must be Shape")
278
434
  self.grouped.append(s)
279
435
 
280
436
  def remove(self, *shapes):
437
+ """Remove one or more shapes from the group.
438
+
439
+ Args:
440
+ *shapes: One or more Shape objects to remove
441
+ """
281
442
  for s in shapes:
282
443
  self.grouped.remove(s)
283
444
  _shapes.remove(s)
284
445
 
285
446
  def clear(self):
447
+ """Remove all shapes from the group."""
286
448
  self.grouped.clear()
287
449
 
288
450
  def is_obj_over(self, x, y):
451
+ """Check if any shape in group contains the point.
452
+
453
+ Args:
454
+ x: X coordinate
455
+ y: Y coordinate
456
+
457
+ Returns:
458
+ bool: True if any child shape contains point, False otherwise
459
+ """
289
460
  for i in self.grouped:
290
461
  if i.is_obj_over(x, y):
291
462
  return True
292
463
  return False
293
464
 
294
- def __str__(self):
295
- return "group"
465
+ def __repr__(self):
466
+ shapes_str = ", ".join(str(s) for s in self.grouped)
467
+ return f"Group([{shapes_str}])"
468
+
469
+ # def __str__(self):
470
+ # return "group"
296
471
 
297
472
 
298
473
  class CollisionManager:
474
+ """Static utility class containing collision detection for all shape combinations."""
299
475
  @staticmethod
300
476
  def rect_rect(rect1: Rect, rect2: Rect):
301
- return pygame.Rect(*rect1.rect).colliderect(pygame.Rect(*rect2.rect))
477
+ """Check if two rectangles are colliding."""
478
+ for rect in [rect1, rect2]:
479
+ corners = [
480
+ (rect.x, rect.y),
481
+ (rect.x + rect.width, rect.y),
482
+ (rect.x, rect.y + rect.height),
483
+ (rect.x + rect.width, rect.y + rect.height),
484
+ ]
485
+ for other in [rect1, rect2]:
486
+ if other is rect:
487
+ continue
488
+ for point in corners:
489
+ if other.is_obj_over(*point):
490
+ return True
491
+ return False
302
492
 
303
493
  @staticmethod
304
494
  def rect_circle(rect: Rect, circle: Circle):
495
+ """Check if a rectangle and circle are colliding."""
305
496
  closest_x = max(rect.x, min(circle.x, rect.x + rect.width))
306
497
  closest_y = max(rect.y, min(circle.y, rect.y + rect.height))
307
498
  dist = ((circle.x - closest_x) ** 2 + (circle.y - closest_y) ** 2) ** 0.5
@@ -309,11 +500,13 @@ class CollisionManager:
309
500
 
310
501
  @staticmethod
311
502
  def circle_circle(circle1: Circle, circle2: Circle):
503
+ """Check if two circles are colliding."""
312
504
  dist = ((circle1.x - circle2.x) ** 2 + (circle1.y - circle2.y) ** 2) ** 0.5
313
505
  return dist < circle1.radius + circle2.radius
314
506
 
315
507
  @staticmethod
316
508
  def rect_polygon(rect: Rect, polygon: Polygon):
509
+ """Check if a rectangle and polygon are colliding."""
317
510
  corners = [
318
511
  (rect.x, rect.y),
319
512
  (rect.x + rect.width, rect.y),
@@ -330,6 +523,7 @@ class CollisionManager:
330
523
 
331
524
  @staticmethod
332
525
  def circle_polygon(circle: Circle, polygon: Polygon):
526
+ """Check if a circle and polygon are colliding."""
333
527
  if polygon.is_obj_over(circle.x, circle.y):
334
528
  return True
335
529
  for px, py in polygon.points:
@@ -340,6 +534,7 @@ class CollisionManager:
340
534
 
341
535
  @staticmethod
342
536
  def rect_text(rect, text):
537
+ """Check if a rectangle and text are colliding."""
343
538
  text_width = text.txtsurf.get_width()
344
539
  text_height = text.txtsurf.get_height()
345
540
  return pygame.Rect(*rect.rect).colliderect(
@@ -348,6 +543,7 @@ class CollisionManager:
348
543
 
349
544
  @staticmethod
350
545
  def circle_text(circle, text):
546
+ """Check if a circle and text are colliding."""
351
547
  text_width = text.txtsurf.get_width()
352
548
  text_height = text.txtsurf.get_height()
353
549
  closest_x = max(text.x, min(circle.x, text.x + text_width))
@@ -357,12 +553,14 @@ class CollisionManager:
357
553
 
358
554
  @staticmethod
359
555
  def rect_image(rect, image):
556
+ """Check if a rectangle and image are colliding."""
360
557
  return pygame.Rect(*rect.rect).colliderect(
361
558
  pygame.Rect(image.x, image.y, image.width, image.height)
362
559
  )
363
560
 
364
561
  @staticmethod
365
562
  def circle_image(circle, image):
563
+ """Check if a circle and image are colliding."""
366
564
  closest_x = max(image.x, min(circle.x, image.x + image.width))
367
565
  closest_y = max(image.y, min(circle.y, image.y + image.height))
368
566
  dist = ((circle.x - closest_x) ** 2 + (circle.y - closest_y) ** 2) ** 0.5
@@ -370,6 +568,7 @@ class CollisionManager:
370
568
 
371
569
  @staticmethod
372
570
  def groupcollision(group, shape):
571
+ """Check if a group and any shape are colliding."""
373
572
  for child in group.grouped:
374
573
  if is_colliding(child, shape):
375
574
  return True
@@ -380,6 +579,15 @@ class CollisionManager:
380
579
 
381
580
 
382
581
  def check_args(func, name):
582
+ """Validate that a decorator function takes no arguments.
583
+
584
+ Args:
585
+ func: The function to validate
586
+ name: Name of the decorator (for error messages)
587
+
588
+ Raises:
589
+ ValueError: If function has any parameters
590
+ """
383
591
  sig = signature(func)
384
592
  param_names = list(sig.parameters)
385
593
  if str(sig) != "()":
@@ -391,6 +599,13 @@ def check_args(func, name):
391
599
  # decorartors like this get weird: if you want to be able to pass arguments in you need to handle 2 cases
392
600
  # if theres no arguments then it just takes the function as the argument
393
601
  def on_tick(func: Callable):
602
+ """Decorator: call function every frame (60 times per second).
603
+
604
+ Usage:
605
+ @on_tick
606
+ def update():
607
+ pass # Runs every frame
608
+ """
394
609
  name = currentframe().f_code.co_name
395
610
  check_args(func, name)
396
611
  _ontick.append(func)
@@ -398,6 +613,15 @@ def on_tick(func: Callable):
398
613
 
399
614
 
400
615
  def on_press(target: Callable | str):
616
+ """Decorator: call function when a key is pressed.
617
+
618
+ Usage:
619
+ @on_press # Any key
620
+ def any_key(): pass
621
+
622
+ @on_press("space, enter") # Specific keys
623
+ def special(): pass
624
+ """
401
625
  if callable(target):
402
626
  func = target
403
627
  name = currentframe().f_code.co_name
@@ -435,6 +659,13 @@ def on_press(target: Callable | str):
435
659
 
436
660
 
437
661
  def on_hold(target: Callable | str):
662
+ """Decorator: call function every frame while a key is held down.
663
+
664
+ Usage:
665
+ @on_hold("w")
666
+ def move_up():
667
+ pass # Runs each frame while 'w' is held
668
+ """
438
669
  if callable(target):
439
670
  func = target
440
671
  name = currentframe().f_code.co_name
@@ -472,7 +703,15 @@ def on_hold(target: Callable | str):
472
703
 
473
704
 
474
705
  def on_click(target: Callable | Shape):
475
-
706
+ """Decorator: call function when mouse is clicked.
707
+
708
+ Usage:
709
+ @on_click # Any click
710
+ def click(): pass
711
+
712
+ @on_click(my_shape) # Click on specific shape
713
+ def shape_click(): pass
714
+ """
476
715
  if callable(target):
477
716
  name = currentframe().f_code.co_name
478
717
  check_args(target, name)
@@ -489,28 +728,82 @@ def on_click(target: Callable | Shape):
489
728
  return decorator
490
729
 
491
730
 
731
+ def on_hover(shape: Shape):
732
+ """Decorator: call function every frame mouse hovers over a shape.
733
+
734
+ Usage:
735
+ @on_hover(my_circle)
736
+ def hover(): pass
737
+ """
738
+ def decorator(func: Callable):
739
+ name = currentframe().f_code.co_name
740
+ check_args(func, name)
741
+ _hover_funcs[shape] = func
742
+ return func
743
+
744
+ return decorator
745
+
746
+
747
+ def on_drag(shape: Shape):
748
+ """Decorator: call function every frame a draggable shape is being dragged.
749
+
750
+ Usage:
751
+ @on_drag(my_rect)
752
+ def dragging(): pass
753
+ """
754
+ def decorator(func: Callable):
755
+ name = currentframe().f_code.co_name
756
+ check_args(func, name)
757
+ _dragging_funcs[shape] = func
758
+ return func
759
+
760
+ return decorator
761
+
762
+
492
763
  def set_bg(color: str):
764
+ """Set the background color of the window.
765
+
766
+ Args:
767
+ color: Color as hex string (#RRGGBB) or name (e.g., 'white', 'red')
768
+ """
493
769
  global _bgcolor
494
770
  _bgcolor = color
495
771
 
496
772
 
497
773
  def clear_screen():
774
+ """Remove all shapes from the screen."""
498
775
  _shapes.clear()
499
776
 
500
777
 
501
778
  def erase(obj):
779
+ """Remove a specific shape from the screen.
780
+
781
+ Args:
782
+ obj: The shape object to remove
783
+ """
502
784
  _shapes.remove(obj)
503
785
 
504
786
 
505
787
  def is_colliding(shape1: Shape, shape2: Shape) -> bool:
506
- if "group" == str(shape1) or "group" == str(shape2):
788
+ """Check if two shapes are colliding.
789
+
790
+ Args:
791
+ shape1: First shape
792
+ shape2: Second shape
793
+
794
+ Returns:
795
+ bool: True if shapes overlap, False otherwise
796
+ """
797
+ shapeName1 = shape1.__class__.__name__.lower()
798
+ shapeName2 = shape2.__class__.__name__.lower()
799
+ if "group" == shapeName1 or "group" == shapeName2:
507
800
  method_name = "groupcollision"
508
801
  method = getattr(CollisionManager, method_name, None)
509
802
  return method(shape1, shape2)
510
- method_name = f"{str(shape1)}_{str(shape2)}"
803
+ method_name = f"{shapeName1}_{shapeName2}"
511
804
  method = getattr(CollisionManager, method_name, None)
512
805
  if method is None:
513
- method_name = f"{str(shape2)}_{str(shape1)}"
806
+ method_name = f"{shapeName2}_{shapeName1}"
514
807
  method = getattr(CollisionManager, method_name, None)
515
808
  if method is None:
516
809
  raise Exception(
@@ -526,7 +819,16 @@ def run(
526
819
  resizable: bool = True,
527
820
  caption: str = "SG window",
528
821
  ):
529
-
822
+ """Start the graphics window and begin the main game loop.
823
+
824
+ This function blocks until the window is closed.
825
+
826
+ Args:
827
+ width: Window width in pixels (default: 200)
828
+ height: Window height in pixels (default: 200)
829
+ resizable: Whether the window can be resized by user (default: True)
830
+ caption: Window title bar text (default: "SG window")
831
+ """
530
832
  global _bgcolor
531
833
 
532
834
  if resizable:
@@ -540,6 +842,7 @@ def run(
540
842
 
541
843
  def handle_events():
542
844
  nonlocal running
845
+ global _dragging_shape
543
846
 
544
847
  for event in pygame.event.get():
545
848
  if event.type == pygame.QUIT:
@@ -564,6 +867,27 @@ def run(
564
867
  for shape in _shapes:
565
868
  if shape.is_obj_over() and shape in _mouse_click_funcs:
566
869
  _mouse_click_funcs[shape]()
870
+ if shape.draggable and shape.is_obj_over():
871
+ _dragging_shape = shape
872
+ break
873
+
874
+ if event.type == pygame.MOUSEBUTTONUP:
875
+ _dragging_shape = None
876
+
877
+ def _move_shape(_dragging_shape):
878
+ if _dragging_shape is not None:
879
+ if hasattr(_dragging_shape, "points"):
880
+ dx = mouse.x - _dragging_shape.points[0][0]
881
+ dy = mouse.y - _dragging_shape.points[0][1]
882
+ _dragging_shape.points = [
883
+ (p[0] + dx, p[1] + dy) for p in _dragging_shape.points
884
+ ]
885
+ else:
886
+ _dragging_shape.x = mouse.x
887
+ _dragging_shape.y = mouse.y
888
+ _dragging_funcs[_dragging_shape]()
889
+
890
+ _move_shape(_dragging_shape)
567
891
 
568
892
  keys_pressed = pygame.key.get_pressed()
569
893
  for pygame_key, func in _key_hold_funcs.items():
@@ -577,6 +901,9 @@ def run(
577
901
  func(pygame.key.name(pygame_key))
578
902
  else:
579
903
  func()
904
+ for shape, func in _hover_funcs.items():
905
+ if shape.is_obj_over(mouse.x, mouse.y):
906
+ func()
580
907
 
581
908
  for func in _ontick:
582
909
  func()
@@ -587,20 +914,23 @@ def run(
587
914
  screen.fill(_bgcolor)
588
915
 
589
916
  for shape in _shapes:
590
- if str(shape) == "circle":
917
+ if not shape.visible:
918
+ continue
919
+ shapeName = shape.__class__.__name__.lower()
920
+ if shapeName == "circle":
591
921
  pygame.draw.circle(
592
922
  screen, shape.color, (shape.x, shape.y), shape.radius, shape.outline
593
923
  )
594
- elif str(shape) == "rect":
924
+ elif shapeName == "rect":
595
925
  pygame.draw.rect(
596
926
  screen,
597
927
  shape.color,
598
928
  shape.rect,
599
929
  shape.outline,
600
930
  )
601
- elif str(shape) == "polygon":
931
+ elif shapeName == "polygon":
602
932
  pygame.draw.polygon(screen, shape.color, shape.points, shape.outline)
603
- elif str(shape) == "line":
933
+ elif shapeName == "line":
604
934
  for i in range(1, len(shape.points)):
605
935
  pygame.draw.line(
606
936
  screen,
@@ -609,11 +939,11 @@ def run(
609
939
  shape.points[i],
610
940
  shape.width,
611
941
  )
612
- elif str(shape) == "text":
942
+ elif shapeName == "text":
613
943
  screen.blit(shape.txtsurf, (shape.x, shape.y))
614
- elif str(shape) == "image":
944
+ elif shapeName == "image":
615
945
  screen.blit(shape.surface, (shape.x, shape.y))
616
- elif str(shape) == "group":
946
+ elif shapeName == "group":
617
947
  pass # this stops weird edgecases while keeping good rendering
618
948
 
619
949
  pygame.display.flip()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simple-graphics-lib
3
- Version: 0.0.7
3
+ Version: 0.0.9
4
4
  Summary: A simple graphics library for python learners
5
5
  Author-email: Max-Hillyer <maxhillyer7012@gmail.com>
6
6
  License-Expression: MIT
@@ -63,6 +63,8 @@ Arguments:
63
63
  | height | int | the height of the rect | 10 |
64
64
  | color | str | the color of the rect | "black" |
65
65
  | outline | bool | whether the rect is just an outline | False |
66
+ | draggable | bool | whether the rect can be dragged | False |
67
+ | visible | bool | whether the rect is drawn on screen | True |
66
68
 
67
69
  Methods:
68
70
  | Method | Purpose | Arguments | Returns |
@@ -80,6 +82,8 @@ Arguments:
80
82
  | radius | int | the radius of the circle | 10 |
81
83
  | color | str | the color of the circle | "black" |
82
84
  | outline | bool | whether the circle is just an outline | False |
85
+ | draggable | bool | whether the circle can be dragged | False |
86
+ | visible | bool | whether the circle is drawn on screen | True |
83
87
 
84
88
  Methods:
85
89
  | Method | Purpose | Arguments | Returns |
@@ -94,6 +98,8 @@ Arguments:
94
98
  | points | list[tuple] | the vertices of the polygon | None |
95
99
  | color | str | the color of the polygon | "black" |
96
100
  | outline | bool | whther the polygon is just an outline | False |
101
+ | draggable | bool | whether the polygon can be dragged | False |
102
+ | visible | bool | whether the polygon is drawn on screen | True |
97
103
 
98
104
  Methods:
99
105
  | Method | Purpose | Arguments | Returns |
@@ -107,6 +113,8 @@ Arguments:
107
113
  | points | list[tuple] |the points that the line will connect | None |
108
114
  | width | int | the width of the line | 10 |
109
115
  | color | str | the color of the line | "black" |
116
+ | draggable | bool | whether the line can be dragged | False |
117
+ | visible | bool | whether the line is drawn on screen | True |
110
118
 
111
119
  Methods:
112
120
  | Method | Purpose | Arguments | Returns |
@@ -122,6 +130,8 @@ Arguments:
122
130
  | text | str | the text of the text | None |
123
131
  | color | str | the color of the text | "black" |
124
132
  | size | int | the size of the text | 36 |
133
+ | draggable | bool | whether the text can be dragged | False |
134
+ | visible | bool | whether the text is drawn on screen | True |
125
135
 
126
136
  Methods:
127
137
  | Method | Purpose | Arguments | Returns |
@@ -137,6 +147,8 @@ Arguments:
137
147
  | img_path | str | the path to the image | None |
138
148
  | width | int | the width of the image | the width from the image path |
139
149
  | height | int | the height of the image | the height from the image path |
150
+ | draggable | bool | whether the image can be dragged | False |
151
+ | visible | bool | whether the image is drawn on screen | True |
140
152
 
141
153
  Methods:
142
154
  | Method | Purpose | Arguments | Returns |
@@ -160,7 +172,8 @@ group.color = "green"
160
172
  Arguments:
161
173
  | Argument | Type | Role | Default |
162
174
  | :--- | :---: | :---: | ---: |
163
- | *shapes | list[Shape] | the shapes to group | None |
175
+ | *shapes | list[Shape] | the shapes to group | None |
176
+ | visible | bool | whether shapes in group are drawn on screen | True |
164
177
 
165
178
  Methods:
166
179
  | Method | Purpose | Arguments | Returns |
@@ -308,6 +321,33 @@ def print_click():
308
321
  run()
309
322
  ```
310
323
 
324
+ ### on_hover
325
+ functions defined with the `@on_hover` decorator will run whenever the specified object is being hovered over
326
+
327
+ the function below will run whenever the mouse is over the rectangle
328
+ ```python
329
+ r1 = Rect(200,200, 100, 100)
330
+
331
+ @on_hover(r1)
332
+ def p():
333
+ print("hovering")
334
+
335
+ run(400,400)
336
+ ```
337
+
338
+ ### on_drag
339
+ functions defined with the `@on_drag` decorator will run whenever the specified object is being dragged
340
+
341
+ the function below will run whenevr the r1 rect is being dragged
342
+ ```python
343
+ r1 = Rect(200,200,draggable = True)
344
+
345
+ @on_drag(r1)
346
+ def do_something():
347
+ print("dragging r1")
348
+
349
+ run(400,400)
350
+ ```
311
351
  ---
312
352
  ## The mouse
313
353
  A mouse object is already initialized in the Simple Graphics import
@@ -322,6 +362,15 @@ def main():
322
362
 
323
363
  run()
324
364
  ```
365
+
366
+ ## dragging
367
+
368
+ to make a shape draggable, add the keyword argument to the object initialization:
369
+ ```python
370
+ r = Rect(250,250, draggable = True)
371
+ ```
372
+ this works for all shapes including images and text
373
+
325
374
  ---
326
375
  ## Miscellaneous Functions
327
376