batframework 1.0.9a10__py3-none-any.whl → 1.0.9a12__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.
Files changed (73) hide show
  1. batFramework/__init__.py +2 -0
  2. batFramework/action.py +280 -279
  3. batFramework/actionContainer.py +105 -82
  4. batFramework/animatedSprite.py +80 -58
  5. batFramework/animation.py +91 -77
  6. batFramework/audioManager.py +156 -131
  7. batFramework/baseScene.py +249 -240
  8. batFramework/camera.py +245 -317
  9. batFramework/constants.py +57 -51
  10. batFramework/cutscene.py +239 -253
  11. batFramework/cutsceneManager.py +34 -34
  12. batFramework/drawable.py +107 -77
  13. batFramework/dynamicEntity.py +30 -30
  14. batFramework/easingController.py +58 -58
  15. batFramework/entity.py +130 -130
  16. batFramework/enums.py +171 -135
  17. batFramework/fontManager.py +65 -65
  18. batFramework/gui/__init__.py +28 -25
  19. batFramework/gui/animatedLabel.py +90 -89
  20. batFramework/gui/button.py +17 -17
  21. batFramework/gui/clickableWidget.py +244 -245
  22. batFramework/gui/collapseContainer.py +98 -0
  23. batFramework/gui/constraints/__init__.py +1 -1
  24. batFramework/gui/constraints/constraints.py +1066 -980
  25. batFramework/gui/container.py +220 -201
  26. batFramework/gui/debugger.py +140 -130
  27. batFramework/gui/draggableWidget.py +63 -44
  28. batFramework/gui/image.py +61 -58
  29. batFramework/gui/indicator.py +116 -113
  30. batFramework/gui/interactiveWidget.py +243 -239
  31. batFramework/gui/label.py +147 -344
  32. batFramework/gui/layout.py +442 -426
  33. batFramework/gui/meter.py +155 -96
  34. batFramework/gui/radioButton.py +43 -35
  35. batFramework/gui/root.py +228 -228
  36. batFramework/gui/scrollingContainer.py +282 -0
  37. batFramework/gui/selector.py +232 -250
  38. batFramework/gui/shape.py +286 -276
  39. batFramework/gui/slider.py +353 -397
  40. batFramework/gui/style.py +10 -10
  41. batFramework/gui/styleManager.py +49 -54
  42. batFramework/gui/syncedVar.py +43 -49
  43. batFramework/gui/textInput.py +331 -306
  44. batFramework/gui/textWidget.py +308 -0
  45. batFramework/gui/toggle.py +140 -128
  46. batFramework/gui/tooltip.py +35 -30
  47. batFramework/gui/widget.py +546 -521
  48. batFramework/manager.py +131 -134
  49. batFramework/particle.py +118 -118
  50. batFramework/propertyEaser.py +79 -79
  51. batFramework/renderGroup.py +34 -34
  52. batFramework/resourceManager.py +130 -130
  53. batFramework/scene.py +31 -31
  54. batFramework/sceneLayer.py +134 -138
  55. batFramework/sceneManager.py +200 -197
  56. batFramework/scrollingSprite.py +115 -115
  57. batFramework/sprite.py +46 -51
  58. batFramework/stateMachine.py +49 -54
  59. batFramework/templates/__init__.py +2 -1
  60. batFramework/templates/character.py +15 -0
  61. batFramework/templates/controller.py +158 -97
  62. batFramework/templates/stateMachine.py +39 -0
  63. batFramework/tileset.py +46 -46
  64. batFramework/timeManager.py +213 -213
  65. batFramework/transition.py +162 -162
  66. batFramework/triggerZone.py +22 -22
  67. batFramework/utils.py +306 -306
  68. {batframework-1.0.9a10.dist-info → batframework-1.0.9a12.dist-info}/LICENSE +20 -20
  69. {batframework-1.0.9a10.dist-info → batframework-1.0.9a12.dist-info}/METADATA +24 -17
  70. batframework-1.0.9a12.dist-info/RECORD +72 -0
  71. batframework-1.0.9a10.dist-info/RECORD +0 -67
  72. {batframework-1.0.9a10.dist-info → batframework-1.0.9a12.dist-info}/WHEEL +0 -0
  73. {batframework-1.0.9a10.dist-info → batframework-1.0.9a12.dist-info}/top_level.txt +0 -0
@@ -1,426 +1,442 @@
1
- import batFramework as bf
2
- from .widget import Widget
3
- from .constraints.constraints import *
4
- from typing import Self, TYPE_CHECKING
5
- from abc import ABC,abstractmethod
6
- import pygame
7
- from .interactiveWidget import InteractiveWidget
8
-
9
- if TYPE_CHECKING:
10
- from .container import Container
11
-
12
-
13
- class Layout(ABC):
14
- def __init__(self, parent: "Container" = None):
15
- self.parent = parent
16
- self.child_constraints: list[Constraint] = []
17
- self.children_rect = pygame.FRect(0, 0, 0, 0)
18
-
19
- def get_free_space(self)->tuple[float,float]:
20
- """
21
- return the space available for Growing widgets to use
22
- """
23
- return self.parent.get_inner_rect()
24
-
25
- def set_child_constraints(self, *constraints) -> Self:
26
- self.child_constraints = list(constraints)
27
- self.update_child_constraints()
28
- self.notify_parent()
29
- return self
30
-
31
- def set_parent(self, parent: Widget):
32
- self.parent = parent
33
- self.notify_parent()
34
-
35
- def notify_parent(self) -> None:
36
- if self.parent:
37
- self.parent.dirty_layout = True
38
-
39
- def update_children_rect(self):
40
- if self.parent.get_layout_children():
41
- self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft,*self.parent.get_layout_children()[0].get_min_required_size())
42
-
43
- self.children_rect.unionall(
44
- [pygame.FRect(0,0,*c.get_min_required_size()) for c in self.parent.get_layout_children()[1:]]
45
- )
46
- else:
47
- self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, 0, 0)
48
- self.children_rect.move_ip(-self.parent.scroll.x,-self.parent.scroll.y)
49
-
50
- def update_child_constraints(self):
51
- if self.parent:
52
- for child in self.parent.get_layout_children():
53
- child.add_constraints(*self.child_constraints)
54
-
55
- def arrange(self) -> None:
56
- """
57
- updates the position of children in the parent
58
- """
59
- return
60
-
61
- def get_raw_size(self):
62
- """
63
- Returns the size the container should have to encapsulate perfectly all of its widgets
64
- """
65
- # print(self,self.parent,len(self.parent.get_layout_children()))
66
- self.update_children_rect()
67
- return self.children_rect.size
68
-
69
- def get_auto_size(self) -> tuple[float, float]:
70
- """
71
- Returns the final size the container should have (while keeping the width and height if they are non-resizable)
72
- """
73
- target_size = list(self.get_raw_size())
74
- if not self.parent.autoresize_w:
75
- target_size[0] = self.parent.get_inner_width()
76
- if not self.parent.autoresize_h:
77
- target_size[1] = self.parent.get_inner_height()
78
-
79
- return self.parent.expand_rect_with_padding((0,0,*target_size)).size
80
- # return target_size
81
-
82
- def scroll_to_widget(self, widget: "Widget"):
83
- """
84
- Scrolls parent containers so that the widget becomes visible.
85
- Handles deeply nested widgets and large widgets gracefully.
86
- """
87
- target = widget
88
- container = self.parent
89
-
90
- while container and not container.is_root:
91
- target_rect = target.rect # global
92
- padded_rect = container.get_inner_rect() # global
93
-
94
- dx = dy = 0
95
-
96
- # --- Horizontal ---
97
- if target_rect.width <= padded_rect.width:
98
- if target_rect.left < padded_rect.left:
99
- dx = target_rect.left - padded_rect.left
100
- elif target_rect.right > padded_rect.right:
101
- dx = target_rect.right - padded_rect.right
102
- else:
103
- # Widget is wider than viewport: align left side
104
- if target_rect.left < padded_rect.left:
105
- dx = target_rect.left - padded_rect.left
106
- elif target_rect.right > padded_rect.right:
107
- dx = target_rect.right - padded_rect.right
108
-
109
- # --- Vertical ---
110
- if target_rect.height <= padded_rect.height:
111
- if target_rect.top < padded_rect.top:
112
- dy = target_rect.top - padded_rect.top
113
- elif target_rect.bottom > padded_rect.bottom:
114
- dy = target_rect.bottom - padded_rect.bottom
115
- else:
116
- # Widget is taller than viewport: align top side
117
- if target_rect.top < padded_rect.top:
118
- dy = target_rect.top - padded_rect.top
119
- elif target_rect.bottom > padded_rect.bottom:
120
- dy = target_rect.bottom - padded_rect.bottom
121
-
122
- # Convert global delta into local scroll delta for container
123
- container.scroll_by((dx, dy))
124
-
125
- # Now the target for the next iteration is the container itself
126
- target = container
127
- container = container.parent
128
-
129
-
130
- def handle_event(self, event):
131
- pass
132
-
133
- class FreeLayout(Layout):...
134
-
135
- class SingleAxisLayout(Layout):
136
-
137
- def __init__(self, parent = None):
138
- super().__init__(parent)
139
-
140
- def focus_next_child(self) -> None:
141
- l = self.parent.get_interactive_children()
142
- self.parent.focused_index = min(self.parent.focused_index + 1, len(l) - 1)
143
- focused = l[self.parent.focused_index]
144
- focused.get_focus()
145
-
146
- def focus_prev_child(self) -> None:
147
- l = self.parent.get_interactive_children()
148
- self.parent.focused_index = max(self.parent.focused_index - 1, 0)
149
- focused = l[self.parent.focused_index]
150
- focused.get_focus()
151
-
152
-
153
- class DoubleAxisLayout(Layout):
154
- """Abstract layout class for layouts that arrange widgets in two dimensions."""
155
-
156
- def focus_up_child(self) -> None:...
157
- def focus_down_child(self) -> None:...
158
- def focus_right_child(self) -> None:...
159
- def focus_left_child(self) -> None:...
160
-
161
-
162
- class Column(SingleAxisLayout):
163
- def __init__(self, gap: int = 0):
164
- super().__init__()
165
- self.gap = gap
166
-
167
- def update_children_rect(self):
168
- if self.parent.get_layout_children():
169
- width = max(child.get_min_required_size()[0] for child in self.parent.get_layout_children() )
170
- height = sum(child.get_min_required_size()[1] for child in self.parent.get_layout_children()) + self.gap * (len(self.parent.get_layout_children()) - 1)
171
-
172
- # width = max(child.rect.w for child in self.parent.get_layout_children() )
173
- # height = sum(child.rect.h for child in self.parent.get_layout_children()) + self.gap * (len(self.parent.get_layout_children()) - 1)
174
-
175
-
176
- self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, width, height)
177
- else:
178
- self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, 10, 10)
179
- self.children_rect.move_ip(-self.parent.scroll.x,-self.parent.scroll.y)
180
-
181
- def handle_event(self, event):
182
- if not self.parent.get_layout_children() or not self.parent.children_has_focus():
183
- return
184
-
185
- if event.type == pygame.KEYDOWN:
186
- if event.key in (pygame.K_DOWN, pygame.K_UP):
187
- self.focus_next_child() if event.key == pygame.K_DOWN else self.focus_prev_child()
188
- event.consumed = True
189
- event.consumed = True
190
-
191
-
192
- def arrange(self) -> None:
193
- self.update_children_rect()
194
- y = self.children_rect.y
195
- for child in self.parent.get_layout_children():
196
- child.set_position(self.children_rect.x, y)
197
- y += child.rect.height + self.gap
198
-
199
- class Row(SingleAxisLayout):
200
- def __init__(self, gap: int = 0):
201
- super().__init__()
202
- self.gap = gap
203
-
204
- def update_children_rect(self):
205
- if self.parent.get_layout_children():
206
- height = max(child.get_min_required_size()[1] for child in self.parent.get_layout_children())
207
- width = sum(child.get_min_required_size()[0] for child in self.parent.get_layout_children()) + self.gap * (len(self.parent.get_layout_children()) - 1)
208
- self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, width, height)
209
- else:
210
- self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, 10,10)
211
- self.children_rect.move_ip(-self.parent.scroll.x,-self.parent.scroll.y)
212
-
213
- def handle_event(self, event):
214
- if not self.parent.get_layout_children() or not self.parent.children_has_focus():
215
- return
216
-
217
- if event.type == pygame.KEYDOWN:
218
- if event.key in (pygame.K_RIGHT, pygame.K_LEFT):
219
- self.focus_next_child() if event.key == pygame.K_RIGHT else self.focus_prev_child()
220
- event.consumed = True
221
-
222
-
223
- def arrange(self) -> None:
224
- self.update_children_rect()
225
- x = self.children_rect.x
226
- for child in self.parent.get_layout_children():
227
- child.set_position(x,self.children_rect.y)
228
- x += child.rect.width + self.gap
229
-
230
-
231
-
232
- class RowFill(Row):
233
-
234
- def update_children_rect(self):
235
- parent_width = self.parent.get_inner_width()
236
- if self.parent.get_layout_children():
237
- height = max(child.get_min_required_size()[1] for child in self.parent.get_layout_children())
238
- width = parent_width
239
- self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, width, height)
240
- else:
241
- self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, parent_width,10)
242
- self.children_rect.move_ip(-self.parent.scroll.x,-self.parent.scroll.y)
243
-
244
-
245
- def arrange(self) -> None:
246
- """
247
- Arranges children in a row and resizes them to fill the parent's height,
248
- accounting for the gap between children.
249
- """
250
- self.update_children_rect()
251
- for child in self.parent.get_layout_children():
252
- child.set_autoresize_w(False)
253
- x = self.children_rect.x
254
- # available_height = self.children_rect.height
255
-
256
- # Calculate the width available for each child
257
- total_gap = self.gap * (len(self.parent.get_layout_children()) - 1)
258
- available_width = max(0, self.children_rect.width - total_gap)
259
- child_width = available_width / len(self.parent.get_layout_children()) if self.parent.get_layout_children() else 0
260
-
261
- for child in self.parent.get_layout_children():
262
- child.set_size((child_width, None)) # Resize child to fill height
263
- child.set_position(x, self.children_rect.y) # Position child
264
- x += child_width + self.gap
265
-
266
-
267
- class ColumnFill(Column):
268
-
269
- def update_children_rect(self):
270
- parent_height = self.parent.get_inner_height()
271
- if self.parent.get_layout_children():
272
- width = max(child.get_min_required_size()[0] for child in self.parent.get_layout_children())
273
- height = parent_height
274
- self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, width, height)
275
- else:
276
- self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, 10, parent_height)
277
- self.children_rect.move_ip(-self.parent.scroll.x, -self.parent.scroll.y)
278
-
279
- def arrange(self) -> None:
280
- """
281
- Arranges children in a column and resizes them to fill the parent's width,
282
- accounting for the gap between children.
283
- """
284
- self.update_children_rect()
285
- for child in self.parent.get_layout_children():
286
- child.set_autoresize_h(False)
287
- y = self.children_rect.y
288
-
289
- # Calculate the height available for each child
290
- total_gap = self.gap * (len(self.parent.get_layout_children()) - 1)
291
- available_height = max(0, self.children_rect.height - total_gap)
292
- child_height = available_height / len(self.parent.get_layout_children()) if self.parent.get_layout_children() else 0
293
-
294
- for child in self.parent.get_layout_children():
295
- child.set_size((None, child_height)) # Resize child to fill width
296
- child.set_position(self.children_rect.x, y) # Position child
297
- y += child_height + self.gap
298
-
299
-
300
- class Grid(DoubleAxisLayout):
301
- def __init__(self, rows: int, cols: int, gap: int = 0):
302
- super().__init__()
303
- self.rows = rows
304
- self.cols = cols
305
- self.gap = gap
306
-
307
- def focus_up_child(self) -> None:
308
- l = self.parent.get_interactive_children()
309
- if not l:
310
- return
311
- current_index = self.parent.focused_index
312
- if current_index == -1:
313
- return
314
- current_row = current_index // self.cols
315
- target_index = max(0, current_index - self.cols)
316
- if target_index // self.cols < current_row:
317
- self.parent.focused_index = target_index
318
- l[target_index].get_focus()
319
-
320
- def focus_down_child(self) -> None:
321
- l = self.parent.get_interactive_children()
322
- if not l:
323
- return
324
- current_index = self.parent.focused_index
325
- if current_index == -1:
326
- return
327
- current_row = current_index // self.cols
328
- target_index = min(len(l) - 1, current_index + self.cols)
329
- if target_index // self.cols > current_row:
330
- self.parent.focused_index = target_index
331
- l[target_index].get_focus()
332
-
333
- def focus_left_child(self) -> None:
334
- l = self.parent.get_interactive_children()
335
- if not l:
336
- return
337
- current_index = self.parent.focused_index
338
- if current_index == -1:
339
- return
340
- target_index = max(0, current_index - 1)
341
- if target_index // self.cols == current_index // self.cols:
342
- self.parent.focused_index = target_index
343
- l[target_index].get_focus()
344
-
345
- def focus_right_child(self) -> None:
346
- l = self.parent.get_interactive_children()
347
- if not l:
348
- return
349
- current_index = self.parent.focused_index
350
- if current_index == -1:
351
- return
352
- target_index = min(len(l) - 1, current_index + 1)
353
- if target_index // self.cols == current_index // self.cols:
354
- self.parent.focused_index = target_index
355
- l[target_index].get_focus()
356
-
357
- def handle_event(self, event):
358
- if not self.parent.get_layout_children() or not self.parent.children_has_focus():
359
- return
360
-
361
- if event.type == pygame.KEYDOWN:
362
- if event.key in (pygame.K_RIGHT, pygame.K_LEFT, pygame.K_UP, pygame.K_DOWN):
363
- if event.key == pygame.K_RIGHT:
364
- self.focus_right_child()
365
- elif event.key == pygame.K_LEFT:
366
- self.focus_left_child()
367
- elif event.key == pygame.K_UP:
368
- self.focus_up_child()
369
- elif event.key == pygame.K_DOWN:
370
- self.focus_down_child()
371
-
372
- event.consumed = True
373
-
374
- def update_children_rect(self):
375
- if self.parent.get_layout_children():
376
- cell_width = max(child.get_min_required_size()[0] for child in self.parent.get_layout_children())
377
- cell_height = max(child.get_min_required_size()[1] for child in self.parent.get_layout_children())
378
- width = self.cols * cell_width + self.gap * (self.cols - 1)
379
- height = self.rows * cell_height + self.gap * (self.rows - 1)
380
- self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, width, height)
381
- else:
382
- self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, 10, 10)
383
- self.children_rect.move_ip(-self.parent.scroll.x, -self.parent.scroll.y)
384
-
385
- def arrange(self) -> None:
386
- self.update_children_rect()
387
- if not self.parent.get_layout_children():
388
- return
389
-
390
- cell_width = (self.children_rect.width - self.gap * (self.cols - 1)) / self.cols
391
- cell_height = (self.children_rect.height - self.gap * (self.rows - 1)) / self.rows
392
-
393
- for i, child in enumerate(self.parent.get_layout_children()):
394
- row = i // self.cols
395
- col = i % self.cols
396
- x = self.children_rect.x + col * (cell_width + self.gap)
397
- y = self.children_rect.y + row * (cell_height + self.gap)
398
- child.set_size((cell_width, cell_height))
399
- child.set_position(x, y)
400
-
401
-
402
- class GridFill(Grid):
403
- def update_children_rect(self):
404
- self.children_rect = self.parent.get_inner_rect()
405
- def arrange(self) -> None:
406
- """
407
- Arranges children in a grid and resizes them to fill the parent's available space,
408
- accounting for the gap between children.
409
- """
410
- self.update_children_rect()
411
-
412
- if not self.parent.get_layout_children():
413
- return
414
- for child in self.parent.get_layout_children():
415
- child.set_autoresize(False)
416
-
417
- cell_width = (self.children_rect.width - self.gap * (self.cols - 1)) / self.cols
418
- cell_height = (self.children_rect.height - self.gap * (self.rows - 1)) / self.rows
419
-
420
- for i, child in enumerate(self.parent.get_layout_children()):
421
- row = i // self.cols
422
- col = i % self.cols
423
- x = self.children_rect.x + col * (cell_width + self.gap)
424
- y = self.children_rect.y + row * (cell_height + self.gap)
425
- child.set_size((cell_width, cell_height))
426
- child.set_position(x, y)
1
+ import batFramework as bf
2
+ from .widget import Widget
3
+ from .constraints.constraints import *
4
+ from typing import Self, TYPE_CHECKING
5
+ from abc import ABC,abstractmethod
6
+ import pygame
7
+ from .interactiveWidget import InteractiveWidget
8
+
9
+ if TYPE_CHECKING:
10
+ from .container import Container
11
+
12
+
13
+ class Layout(ABC):
14
+ def __init__(self, parent: "Container" = None):
15
+ self.parent = parent
16
+ self.child_constraints: list[Constraint] = []
17
+ self.children_rect = pygame.FRect(0, 0, 0, 0)
18
+
19
+ def get_free_space(self)->tuple[float,float]:
20
+ """
21
+ return the space available for Growing widgets to use
22
+ """
23
+ return self.parent.get_inner_rect()
24
+
25
+ def set_child_constraints(self, *constraints) -> Self:
26
+ self.child_constraints = list(constraints)
27
+ self.update_child_constraints()
28
+ self.notify_parent()
29
+ return self
30
+
31
+ def set_parent(self, parent: Widget):
32
+ self.parent = parent
33
+ self.notify_parent()
34
+
35
+ def notify_parent(self) -> None:
36
+ if self.parent:
37
+ self.parent.dirty_layout = True
38
+
39
+ def update_children_rect(self):
40
+ if self.parent.get_layout_children():
41
+ self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft,*self.parent.get_layout_children()[0].get_min_required_size())
42
+
43
+ self.children_rect.unionall(
44
+ [pygame.FRect(0,0,*c.get_min_required_size()) for c in self.parent.get_layout_children()[1:]]
45
+ )
46
+ else:
47
+ self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, 0, 0)
48
+ self.children_rect.move_ip(-self.parent.scroll.x,-self.parent.scroll.y)
49
+
50
+ def update_child_constraints(self):
51
+ if self.parent:
52
+ for child in self.parent.get_layout_children():
53
+ child.add_constraints(*self.child_constraints)
54
+
55
+ def arrange(self) -> None:
56
+ """
57
+ updates the position of children in the parent
58
+ """
59
+ return
60
+
61
+ def scroll_children(self)->None:
62
+ return
63
+
64
+ def get_raw_size(self):
65
+ """
66
+ Returns the size the container should have to encapsulate perfectly all of its widgets
67
+ """
68
+ # print(self,self.parent,len(self.parent.get_layout_children()))
69
+ self.update_children_rect() # TODO: find a way to call this fewer times
70
+ return self.children_rect.size
71
+
72
+ def get_auto_size(self) -> tuple[float, float]:
73
+ """
74
+ Returns the final size the container should have (while keeping the width and height if they are non-resizable)
75
+ """
76
+ target_size = list(self.get_raw_size())
77
+ if not self.parent.autoresize_w:
78
+ target_size[0] = self.parent.get_inner_width()
79
+ if not self.parent.autoresize_h:
80
+ target_size[1] = self.parent.get_inner_height()
81
+
82
+ return self.parent.expand_rect_with_padding((0,0,*target_size)).size
83
+ # return target_size
84
+
85
+ def scroll_to_widget(self, widget: "Widget"):
86
+ """
87
+ Scrolls parent container so that the widget becomes visible.
88
+ If the widget is bigger than the container, aligns top/left.
89
+ """
90
+ inner = self.parent.get_inner_rect()
91
+
92
+ if self.parent.clip_children and not inner.contains(widget.rect):
93
+ scroll = pygame.Vector2(0, 0)
94
+
95
+ # Horizontal
96
+ if widget.rect.w > inner.w:
97
+ # Widget is too wide: just align left
98
+ if widget.rect.left < inner.left:
99
+ scroll.x = inner.left - widget.rect.left
100
+ else:
101
+ if widget.rect.left < inner.left:
102
+ scroll.x = inner.left - widget.rect.left
103
+ elif widget.rect.right > inner.right:
104
+ scroll.x = inner.right - widget.rect.right
105
+
106
+ # Vertical
107
+ if widget.rect.h > inner.h:
108
+ # Widget is too tall: just align top
109
+ if widget.rect.top < inner.top:
110
+ scroll.y = inner.top - widget.rect.top
111
+ else:
112
+ if widget.rect.top < inner.top:
113
+ scroll.y = inner.top - widget.rect.top
114
+ elif widget.rect.bottom > inner.bottom:
115
+ scroll.y = inner.bottom - widget.rect.bottom
116
+
117
+ # Apply
118
+ self.parent.scroll_by(-scroll)
119
+
120
+ # stop at root
121
+ if self.parent.is_root:
122
+ return
123
+
124
+ # recurse upwards if parent has a layout
125
+ if hasattr(self.parent.parent, "layout"):
126
+ self.parent.parent.layout.scroll_to_widget(widget)
127
+ def handle_event(self, event):
128
+ pass
129
+
130
+ class FreeLayout(Layout):...
131
+
132
+ class SingleAxisLayout(Layout):
133
+
134
+ def __init__(self, parent = None):
135
+ super().__init__(parent)
136
+
137
+ def focus_next_child(self) -> None:
138
+ l = self.parent.get_interactive_children()
139
+ self.parent.focused_index = min(self.parent.focused_index + 1, len(l) - 1)
140
+ focused = l[self.parent.focused_index]
141
+ focused.get_focus()
142
+
143
+ def focus_prev_child(self) -> None:
144
+ l = self.parent.get_interactive_children()
145
+ self.parent.focused_index = max(self.parent.focused_index - 1, 0)
146
+ focused = l[self.parent.focused_index]
147
+ focused.get_focus()
148
+
149
+ class DoubleAxisLayout(Layout):
150
+ """Abstract layout class for layouts that arrange widgets in two dimensions."""
151
+
152
+ def focus_up_child(self) -> None:...
153
+ def focus_down_child(self) -> None:...
154
+ def focus_right_child(self) -> None:...
155
+ def focus_left_child(self) -> None:...
156
+
157
+ class Column(SingleAxisLayout):
158
+ def __init__(self, gap: int = 0):
159
+ super().__init__()
160
+ self.gap = gap
161
+
162
+ def handle_event(self, event):
163
+ if not self.parent.get_layout_children() or not self.parent.children_has_focus():
164
+ return
165
+
166
+ if event.type == pygame.KEYDOWN:
167
+ if event.key in (pygame.K_DOWN, pygame.K_UP):
168
+ self.focus_next_child() if event.key == pygame.K_DOWN else self.focus_prev_child()
169
+ event.consumed = True
170
+
171
+ def update_children_rect(self):
172
+ print("Update children rect")
173
+ layout_children = self.parent.get_layout_children()
174
+ if layout_children:
175
+ width = max(child.get_min_required_size()[0] if child.autoresize_h else child.rect.w for child in layout_children )
176
+ height = sum(child.get_min_required_size()[1]if child.autoresize_w else child.rect.h for child in layout_children) + self.gap * (len(layout_children) - 1)
177
+ self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, width, height)
178
+ else:
179
+ self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, 10, 10)
180
+ self.children_rect.move_ip(-self.parent.scroll.x,-self.parent.scroll.y)
181
+
182
+ def scroll_children(self):
183
+ topleft = list(self.parent.get_inner_rect().topleft)
184
+ topleft[0] -= round(self.parent.scroll.x)
185
+ topleft[1] -= round(self.parent.scroll.y)
186
+ layout_children = self.parent.get_layout_children()
187
+ for child in layout_children:
188
+ child.set_position(*topleft)
189
+ topleft[1] += child.rect.height + self.gap
190
+
191
+ def arrange(self) -> None:
192
+ self.update_children_rect()
193
+ self.scroll_children()
194
+
195
+ class Row(SingleAxisLayout):
196
+ def __init__(self, gap: int = 0):
197
+ super().__init__()
198
+ self.gap = gap
199
+
200
+ def handle_event(self, event):
201
+ if not self.parent.get_layout_children() or not self.parent.children_has_focus():
202
+ return
203
+
204
+ if event.type == pygame.KEYDOWN:
205
+ if event.key in (pygame.K_RIGHT, pygame.K_LEFT):
206
+ self.focus_next_child() if event.key == pygame.K_RIGHT else self.focus_prev_child()
207
+ event.consumed = True
208
+
209
+ def update_children_rect(self):
210
+ layout_children = self.parent.get_layout_children()
211
+ if layout_children:
212
+ width = sum(child.get_min_required_size()[0] if child.autoresize_w else child.rect.w for child in layout_children ) + self.gap * (len(layout_children) - 1)
213
+ height = max(child.get_min_required_size()[1] if child.autoresize_h else child.rect.h for child in layout_children )
214
+ self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, width, height)
215
+ else:
216
+ self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, 10,10)
217
+ self.children_rect.move_ip(-self.parent.scroll.x,-self.parent.scroll.y)
218
+
219
+ def scroll_children(self):
220
+ topleft = list(self.parent.get_inner_rect().topleft)
221
+ topleft[0] -= round(self.parent.scroll.x)
222
+ topleft[1] -= round(self.parent.scroll.y)
223
+ layout_children = self.parent.get_layout_children()
224
+ for child in layout_children:
225
+ child.set_position(*topleft)
226
+ topleft[0] += child.rect.width + self.gap
227
+
228
+ def arrange(self) -> None:
229
+ self.update_children_rect()
230
+ self.scroll_children()
231
+
232
+ class Grid(DoubleAxisLayout):
233
+ def __init__(self, rows: int, cols: int, gap: int = 0):
234
+ super().__init__()
235
+ self.rows = rows
236
+ self.cols = cols
237
+ self.gap = gap
238
+
239
+ def focus_up_child(self) -> None:
240
+ l = self.parent.get_interactive_children()
241
+ if not l:
242
+ return
243
+ current_index = self.parent.focused_index
244
+ if current_index == -1:
245
+ return
246
+ current_row = current_index // self.cols
247
+ target_index = max(0, current_index - self.cols)
248
+ if target_index // self.cols < current_row:
249
+ self.parent.focused_index = target_index
250
+ l[target_index].get_focus()
251
+
252
+ def focus_down_child(self) -> None:
253
+ l = self.parent.get_interactive_children()
254
+ if not l:
255
+ return
256
+ current_index = self.parent.focused_index
257
+ if current_index == -1:
258
+ return
259
+ current_row = current_index // self.cols
260
+ target_index = min(len(l) - 1, current_index + self.cols)
261
+ if target_index // self.cols > current_row:
262
+ self.parent.focused_index = target_index
263
+ l[target_index].get_focus()
264
+
265
+ def focus_left_child(self) -> None:
266
+ l = self.parent.get_interactive_children()
267
+ if not l:
268
+ return
269
+ current_index = self.parent.focused_index
270
+ if current_index == -1:
271
+ return
272
+ target_index = max(0, current_index - 1)
273
+ if target_index // self.cols == current_index // self.cols:
274
+ self.parent.focused_index = target_index
275
+ l[target_index].get_focus()
276
+
277
+ def focus_right_child(self) -> None:
278
+ l = self.parent.get_interactive_children()
279
+ if not l:
280
+ return
281
+ current_index = self.parent.focused_index
282
+ if current_index == -1:
283
+ return
284
+ target_index = min(len(l) - 1, current_index + 1)
285
+ if target_index // self.cols == current_index // self.cols:
286
+ self.parent.focused_index = target_index
287
+ l[target_index].get_focus()
288
+
289
+ def handle_event(self, event):
290
+ if not self.parent.get_layout_children() or not self.parent.children_has_focus():
291
+ return
292
+
293
+ if event.type == pygame.KEYDOWN:
294
+ if event.key in (pygame.K_RIGHT, pygame.K_LEFT, pygame.K_UP, pygame.K_DOWN):
295
+ if event.key == pygame.K_RIGHT:
296
+ self.focus_right_child()
297
+ elif event.key == pygame.K_LEFT:
298
+ self.focus_left_child()
299
+ elif event.key == pygame.K_UP:
300
+ self.focus_up_child()
301
+ elif event.key == pygame.K_DOWN:
302
+ self.focus_down_child()
303
+
304
+ event.consumed = True
305
+
306
+ def update_children_rect(self):
307
+ layout_children = self.parent.get_layout_children()
308
+ if layout_children:
309
+ cell_width = max(child.get_min_required_size()[0] for child in layout_children)
310
+ cell_height = max(child.get_min_required_size()[1] for child in layout_children)
311
+ width = self.cols * cell_width + self.gap * (self.cols - 1)
312
+ height = self.rows * cell_height + self.gap * (self.rows - 1)
313
+ self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, width, height)
314
+ else:
315
+ self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, 10, 10)
316
+ self.children_rect.move_ip(-self.parent.scroll.x, -self.parent.scroll.y)
317
+
318
+ def scroll_children(self):
319
+ topleft = list(self.parent.get_inner_rect().topleft)
320
+ topleft[0] -= round(self.parent.scroll.x)
321
+ topleft[1] -= round(self.parent.scroll.y)
322
+ layout_children = self.parent.get_layout_children()
323
+ cell_width = (self.children_rect.width - self.gap * (self.cols - 1)) / self.cols if self.cols else 0
324
+ cell_height = (self.children_rect.height - self.gap * (self.rows - 1)) / self.rows if self.rows else 0
325
+ for i, child in enumerate(layout_children):
326
+ row = i // self.cols
327
+ col = i % self.cols
328
+ x = topleft[0] + col * (cell_width + self.gap)
329
+ y = topleft[1] + row * (cell_height + self.gap)
330
+ child.set_position(x, y)
331
+
332
+ def arrange(self) -> None:
333
+ self.update_children_rect()
334
+ self.scroll_children()
335
+
336
+ class RowFill(Row):
337
+
338
+ def update_children_rect(self):
339
+ parent_width = self.parent.get_inner_width()
340
+ if self.parent.get_layout_children():
341
+ height = max(child.get_min_required_size()[1] for child in self.parent.get_layout_children())
342
+ width = parent_width
343
+ self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, width, height)
344
+ else:
345
+ self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, parent_width,10)
346
+ self.children_rect.move_ip(-self.parent.scroll.x,-self.parent.scroll.y)
347
+
348
+ def scroll_children(self):
349
+ topleft = list(self.parent.get_inner_rect().topleft)
350
+ topleft[0] -= round(self.parent.scroll.x)
351
+ topleft[1] -= round(self.parent.scroll.y)
352
+ layout_children = self.parent.get_layout_children()
353
+ total_gap = self.gap * (len(layout_children) - 1)
354
+ available_width = max(0, self.children_rect.width - total_gap)
355
+ child_width = available_width / len(layout_children) if layout_children else 0
356
+ for child in layout_children:
357
+ child.set_position(*topleft)
358
+ topleft[0] += child_width + self.gap
359
+
360
+ def resize_children(self):
361
+ layout_children = self.parent.get_layout_children()
362
+ total_gap = self.gap * (len(layout_children) - 1)
363
+ available_width = max(0, self.children_rect.width - total_gap)
364
+ child_width = available_width / len(layout_children) if layout_children else 0
365
+ for child in layout_children:
366
+ child.set_autoresize_w(False)
367
+ child.set_size((child_width, None))
368
+
369
+ def arrange(self) -> None:
370
+ self.update_children_rect()
371
+ self.resize_children()
372
+ self.scroll_children()
373
+
374
+ class ColumnFill(Column):
375
+
376
+ def update_children_rect(self):
377
+ parent_height = self.parent.get_inner_height()
378
+ if self.parent.get_layout_children():
379
+ width = max(child.get_min_required_size()[0] for child in self.parent.get_layout_children())
380
+ height = parent_height
381
+ self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, width, height)
382
+ else:
383
+ self.children_rect = pygame.FRect(*self.parent.get_inner_rect().topleft, 10, parent_height)
384
+ self.children_rect.move_ip(-self.parent.scroll.x, -self.parent.scroll.y)
385
+
386
+ def scroll_children(self):
387
+ topleft = list(self.parent.get_inner_rect().topleft)
388
+ topleft[0] -= round(self.parent.scroll.x)
389
+ topleft[1] -= round(self.parent.scroll.y)
390
+ layout_children = self.parent.get_layout_children()
391
+ total_gap = self.gap * (len(layout_children) - 1)
392
+ available_height = max(0, self.children_rect.height - total_gap)
393
+ child_height = available_height / len(layout_children) if layout_children else 0
394
+ for child in layout_children:
395
+ child.set_position(*topleft)
396
+ topleft[1] += child_height + self.gap
397
+
398
+ def resize_children(self):
399
+ layout_children = self.parent.get_layout_children()
400
+ total_gap = self.gap * (len(layout_children) - 1)
401
+ available_height = max(0, self.children_rect.height - total_gap)
402
+ child_height = available_height / len(layout_children) if layout_children else 0
403
+ for child in layout_children:
404
+ child.set_autoresize_h(False)
405
+ child.set_size((None, child_height))
406
+
407
+ def arrange(self) -> None:
408
+ self.update_children_rect()
409
+ self.resize_children()
410
+ self.scroll_children()
411
+
412
+ class GridFill(Grid):
413
+ def update_children_rect(self):
414
+ self.children_rect = self.parent.get_inner_rect()
415
+
416
+ def scroll_children(self):
417
+ topleft = list(self.parent.get_inner_rect().topleft)
418
+ topleft[0] -= round(self.parent.scroll.x)
419
+ topleft[1] -= round(self.parent.scroll.y)
420
+ layout_children = self.parent.get_layout_children()
421
+ cell_width = (self.children_rect.width - self.gap * (self.cols - 1)) / self.cols if self.cols else 0
422
+ cell_height = (self.children_rect.height - self.gap * (self.rows - 1)) / self.rows if self.rows else 0
423
+ for i, child in enumerate(layout_children):
424
+ row = i // self.cols
425
+ col = i % self.cols
426
+ x = round(topleft[0] + col * (cell_width + self.gap))
427
+ y = round(topleft[1] + row * (cell_height + self.gap))
428
+ child.set_position(x, y)
429
+
430
+ def resize_children(self):
431
+ layout_children = self.parent.get_layout_children()
432
+ cell_width = (self.children_rect.width - self.gap * (self.cols - 1)) / self.cols if self.cols else 0
433
+ cell_height = (self.children_rect.height - self.gap * (self.rows - 1)) / self.rows if self.rows else 0
434
+ for child in layout_children:
435
+ child.set_autoresize(False)
436
+ child.set_size((cell_width, cell_height))
437
+
438
+ def arrange(self) -> None:
439
+ print("arrange grid fill")
440
+ self.update_children_rect()
441
+ self.resize_children()
442
+ self.scroll_children()