batframework 1.0.9a7__py3-none-any.whl → 1.0.9a9__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 (62) hide show
  1. batFramework/__init__.py +20 -11
  2. batFramework/action.py +1 -1
  3. batFramework/animatedSprite.py +47 -116
  4. batFramework/animation.py +30 -5
  5. batFramework/audioManager.py +8 -5
  6. batFramework/baseScene.py +240 -0
  7. batFramework/camera.py +4 -0
  8. batFramework/constants.py +6 -2
  9. batFramework/cutscene.py +221 -21
  10. batFramework/cutsceneManager.py +5 -2
  11. batFramework/drawable.py +7 -5
  12. batFramework/easingController.py +10 -11
  13. batFramework/entity.py +21 -2
  14. batFramework/enums.py +48 -33
  15. batFramework/gui/__init__.py +6 -3
  16. batFramework/gui/animatedLabel.py +10 -2
  17. batFramework/gui/button.py +4 -31
  18. batFramework/gui/clickableWidget.py +63 -50
  19. batFramework/gui/constraints/constraints.py +212 -136
  20. batFramework/gui/container.py +77 -58
  21. batFramework/gui/debugger.py +12 -17
  22. batFramework/gui/draggableWidget.py +21 -17
  23. batFramework/gui/image.py +3 -10
  24. batFramework/gui/indicator.py +56 -1
  25. batFramework/gui/interactiveWidget.py +127 -108
  26. batFramework/gui/label.py +73 -64
  27. batFramework/gui/layout.py +286 -445
  28. batFramework/gui/meter.py +42 -20
  29. batFramework/gui/radioButton.py +20 -69
  30. batFramework/gui/root.py +99 -29
  31. batFramework/gui/selector.py +250 -0
  32. batFramework/gui/shape.py +13 -5
  33. batFramework/gui/slider.py +262 -107
  34. batFramework/gui/syncedVar.py +49 -0
  35. batFramework/gui/textInput.py +46 -22
  36. batFramework/gui/toggle.py +70 -52
  37. batFramework/gui/tooltip.py +30 -0
  38. batFramework/gui/widget.py +222 -135
  39. batFramework/manager.py +7 -8
  40. batFramework/particle.py +4 -1
  41. batFramework/propertyEaser.py +79 -0
  42. batFramework/renderGroup.py +17 -50
  43. batFramework/resourceManager.py +43 -13
  44. batFramework/scene.py +15 -335
  45. batFramework/sceneLayer.py +138 -0
  46. batFramework/sceneManager.py +31 -36
  47. batFramework/scrollingSprite.py +8 -3
  48. batFramework/sprite.py +1 -1
  49. batFramework/templates/__init__.py +1 -2
  50. batFramework/templates/controller.py +97 -0
  51. batFramework/timeManager.py +76 -22
  52. batFramework/transition.py +37 -103
  53. batFramework/utils.py +125 -66
  54. {batframework-1.0.9a7.dist-info → batframework-1.0.9a9.dist-info}/METADATA +24 -3
  55. batframework-1.0.9a9.dist-info/RECORD +67 -0
  56. {batframework-1.0.9a7.dist-info → batframework-1.0.9a9.dist-info}/WHEEL +1 -1
  57. batFramework/character.py +0 -27
  58. batFramework/templates/character.py +0 -43
  59. batFramework/templates/states.py +0 -166
  60. batframework-1.0.9a7.dist-info/RECORD +0 -63
  61. /batframework-1.0.9a7.dist-info/LICENCE → /batframework-1.0.9a9.dist-info/LICENSE +0 -0
  62. {batframework-1.0.9a7.dist-info → batframework-1.0.9a9.dist-info}/top_level.txt +0 -0
@@ -4,6 +4,7 @@ from .constraints.constraints import *
4
4
  from typing import Self, TYPE_CHECKING
5
5
  from abc import ABC,abstractmethod
6
6
  import pygame
7
+ from .interactiveWidget import InteractiveWidget
7
8
 
8
9
  if TYPE_CHECKING:
9
10
  from .container import Container
@@ -15,8 +16,15 @@ class Layout(ABC):
15
16
  self.child_constraints: list[Constraint] = []
16
17
  self.children_rect = pygame.FRect(0, 0, 0, 0)
17
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
+
18
25
  def set_child_constraints(self, *constraints) -> Self:
19
26
  self.child_constraints = list(constraints)
27
+ self.update_child_constraints()
20
28
  self.notify_parent()
21
29
  return self
22
30
 
@@ -28,14 +36,35 @@ class Layout(ABC):
28
36
  if self.parent:
29
37
  self.parent.dirty_layout = True
30
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
+
31
55
  def arrange(self) -> None:
56
+ """
57
+ updates the position of children in the parent
58
+ """
32
59
  return
33
60
 
34
- def get_raw_size(self) -> tuple[float, float]:
61
+ def get_raw_size(self):
35
62
  """
36
- Returns the supposed size the container should have to encapsulate perfectly all of its widgets
63
+ Returns the size the container should have to encapsulate perfectly all of its widgets
37
64
  """
38
- return self.parent.rect.size if self.parent else (0, 0)
65
+ # print(self,self.parent,len(self.parent.get_layout_children()))
66
+ self.update_children_rect()
67
+ return self.children_rect.size
39
68
 
40
69
  def get_auto_size(self) -> tuple[float, float]:
41
70
  """
@@ -43,342 +72,229 @@ class Layout(ABC):
43
72
  """
44
73
  target_size = list(self.get_raw_size())
45
74
  if not self.parent.autoresize_w:
46
- target_size[0] = self.parent.rect.w
75
+ target_size[0] = self.parent.get_inner_width()
47
76
  if not self.parent.autoresize_h:
48
- target_size[1] = self.parent.rect.h
49
- return target_size
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
50
128
 
51
- def scroll_to_widget(self, widget: Widget) -> None:
52
- padded = self.parent.get_padded_rect() # le carré intérieur
53
- r = widget.rect # le carré du widget = Button
54
- if padded.contains(r): # le widget ne depasse pas -> OK
55
- return
56
- clamped = r.clamp(padded)
57
- # clamped.move_ip(-self.parent.rect.x,-self.parent.rect.y)
58
- dx,dy = clamped.x - r.x,clamped.y-r.y
59
129
 
60
- self.parent.scroll_by((-dx, -dy)) # on scroll la différence pour afficher le bouton en entier
61
-
62
130
  def handle_event(self, event):
63
131
  pass
64
132
 
133
+ class FreeLayout(Layout):...
134
+
65
135
  class SingleAxisLayout(Layout):
136
+
137
+ def __init__(self, parent = None):
138
+ super().__init__(parent)
139
+
66
140
  def focus_next_child(self) -> None:
67
141
  l = self.parent.get_interactive_children()
68
142
  self.parent.focused_index = min(self.parent.focused_index + 1, len(l) - 1)
69
143
  focused = l[self.parent.focused_index]
70
144
  focused.get_focus()
71
- self.scroll_to_widget(focused)
72
145
 
73
146
  def focus_prev_child(self) -> None:
74
147
  l = self.parent.get_interactive_children()
75
148
  self.parent.focused_index = max(self.parent.focused_index - 1, 0)
76
149
  focused = l[self.parent.focused_index]
77
150
  focused.get_focus()
78
- self.scroll_to_widget(focused)
79
151
 
80
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
+
81
161
 
82
162
  class Column(SingleAxisLayout):
83
- def __init__(self, gap: int = 0, spacing: bf.spacing = bf.spacing.MANUAL):
163
+ def __init__(self, gap: int = 0):
84
164
  super().__init__()
85
165
  self.gap = gap
86
- self.spacing = spacing
87
166
 
88
- def set_gap(self, value: float) -> Self:
89
- self.gap = value
90
- self.notify_parent()
91
- return self
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)
92
171
 
93
- def set_spacing(self, spacing: bf.spacing) -> Self:
94
- self.spacing = spacing
95
- self.notify_parent()
96
- return self
97
-
98
- def get_raw_size(self) -> tuple[float, float]:
99
- len_children = len(self.parent.children)
100
- if not len_children:
101
- return self.parent.rect.size
102
- parent_height = sum(c.get_min_required_size()[1] for c in self.parent.children)
103
- parent_width = max(c.get_min_required_size()[0] for c in self.parent.children)
104
- if self.gap:
105
- parent_height += (len_children - 1) * self.gap
106
- target_rect = self.parent.inflate_rect_by_padding(
107
- (0, 0, parent_width, parent_height)
108
- )
109
- return target_rect.size
110
-
111
- def get_auto_size(self) -> tuple[float, float]:
112
- target_size = list(self.get_raw_size())
113
- if not self.parent.autoresize_w:
114
- target_size[0] = self.parent.rect.w
115
- if not self.parent.autoresize_h:
116
- target_size[1] = self.parent.rect.h
117
- return target_size
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)
118
174
 
119
- def arrange(self) -> None:
120
- if not self.parent or not self.parent.children:
121
- return
122
- if self.child_constraints:
123
- for child in self.parent.children:
124
- child.add_constraints(*self.child_constraints)
125
- self.children_rect = self.parent.get_padded_rect()
126
175
 
127
- width, height = self.get_auto_size()
128
- if self.parent.autoresize_w and self.parent.rect.w !=width:
129
- self.parent.set_size((width,None))
130
- if self.parent.autoresize_h and self.parent.rect.h !=height:
131
- self.parent.set_size((None,height))
132
-
133
- # if self.parent.dirty_shape:
134
- # print("parent set dirty shape")
135
- # self.parent.dirty_layout = True
136
- # self.parent.apply_updates()
137
- # self.arrange()
138
- # return
139
-
140
- self.children_rect.move_ip(-self.parent.scroll.x, -self.parent.scroll.y)
141
- y = self.children_rect.top
142
- for child in self.parent.children:
143
- child.set_position(self.children_rect.x, y)
144
- y += child.get_min_required_size()[1] + self.gap
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)
145
180
 
146
181
  def handle_event(self, event):
147
- if not self.parent.children:
182
+ if not self.parent.get_layout_children() or not self.parent.children_has_focus():
148
183
  return
149
184
 
150
185
  if event.type == pygame.KEYDOWN:
151
- if event.key == pygame.K_DOWN:
152
- self.focus_next_child()
153
- elif event.key == pygame.K_UP:
154
- self.focus_prev_child()
155
- else:
156
- return
157
- elif event.type == pygame.MOUSEBUTTONDOWN:
158
- r = self.parent.get_root()
159
- if not r:
160
- return
161
-
162
- if self.parent.rect.collidepoint(
163
- r.drawing_camera.screen_to_world(pygame.mouse.get_pos())
164
- ):
165
- if event.button == 4:
166
- self.parent.scroll_by((0, -10))
167
- elif event.button == 5:
168
- self.parent.scroll_by((0, 10))
169
- else:
170
- return
171
- self.parent.clamp_scroll()
172
- else:
173
- return
174
- else:
175
- return
176
- event.consumed = True
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
177
198
 
178
199
  class Row(SingleAxisLayout):
179
- def __init__(self, gap: int = 0, spacing: bf.spacing = bf.spacing.MANUAL):
200
+ def __init__(self, gap: int = 0):
180
201
  super().__init__()
181
202
  self.gap = gap
182
- self.spacing = spacing
183
203
 
184
- def set_gap(self, value: float) -> Self:
185
- self.gap = value
186
- self.notify_parent()
187
- return self
188
-
189
- def set_spacing(self, spacing: bf.spacing) -> Self:
190
- self.spacing = spacing
191
- self.notify_parent()
192
- return self
193
-
194
- def get_raw_size(self) -> tuple[float, float]:
195
- len_children = len(self.parent.children)
196
- if not len_children:
197
- return self.parent.rect.size
198
- parent_width = sum(c.get_min_required_size()[0] for c in self.parent.children)
199
- parent_height = max(c.get_min_required_size()[1] for c in self.parent.children)
200
- if self.gap:
201
- parent_width += (len_children - 1) * self.gap
202
- target_rect = self.parent.inflate_rect_by_padding(
203
- (0, 0, parent_width, parent_height)
204
- )
205
-
206
- return target_rect.size
207
-
208
- def get_auto_size(self) -> tuple[float, float]:
209
- target_size = list(self.get_raw_size())
210
- if not self.parent.autoresize_w:
211
- target_size[0] = self.parent.rect.w
212
- if not self.parent.autoresize_h:
213
- target_size[1] = self.parent.rect.h
214
- return target_size
215
-
216
- def arrange(self) -> None:
217
- if not self.parent or not self.parent.children:
218
- return
219
- if self.child_constraints:
220
- for child in self.parent.children:
221
- child.add_constraints(*self.child_constraints)
222
- self.children_rect = self.parent.get_padded_rect()
223
-
224
- if self.parent.autoresize_w or self.parent.autoresize_h:
225
- width, height = self.get_auto_size()
226
- if self.parent.rect.size != (width, height):
227
- self.parent.set_size((width, height))
228
- self.parent.build()
229
- self.arrange()
230
- return
231
- self.children_rect.move_ip(-self.parent.scroll.x, -self.parent.scroll.y)
232
- x = self.children_rect.left
233
- for child in self.parent.children:
234
- child.set_position(x, self.children_rect.y)
235
- x += child.get_min_required_size()[0] + self.gap
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)
236
212
 
237
213
  def handle_event(self, event):
238
- if not self.parent.children:
214
+ if not self.parent.get_layout_children() or not self.parent.children_has_focus():
239
215
  return
240
216
 
241
217
  if event.type == pygame.KEYDOWN:
242
- if event.key == pygame.K_RIGHT:
243
- self.focus_next_child()
244
- elif event.key == pygame.K_LEFT:
245
- self.focus_prev_child()
246
- else:
247
- return
248
-
249
- elif event.type == pygame.MOUSEBUTTONDOWN:
250
- r = self.parent.get_root()
251
- if not r:
252
- return
253
- if self.parent.rect.collidepoint(
254
- r.drawing_camera.screen_to_world(pygame.mouse.get_pos())
255
- ):
256
- if event.button == 4:
257
- self.parent.scroll_by((-10, 0))
258
- elif event.button == 5:
259
- self.parent.scroll_by((10, 0))
260
- else:
261
- return
262
- self.parent.clamp_scroll()
263
- else:
264
- return
265
- else:
266
- return
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
267
221
 
268
222
 
269
- event.consumed = True
270
-
271
- class RowFill(Row):
272
- def __init__(self, gap: int = 0, spacing: bf.spacing = bf.spacing.MANUAL):
273
- super().__init__(gap, spacing)
274
-
275
223
  def arrange(self) -> None:
276
- if self.parent.autoresize_w :
277
- super().arrange()
278
- return
279
- if not self.parent or not self.parent.children:
280
- return
281
-
282
- if self.child_constraints:
283
- for child in self.parent.children:
284
- child.add_constraints(*self.child_constraints)
285
- self.children_rect = self.parent.get_padded_rect()
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
286
229
 
287
- if self.parent.autoresize_w or self.parent.autoresize_h:
288
- width, height = self.get_auto_size()
289
- if self.parent.rect.size != (width, height):
290
- self.parent.set_size((width, height))
291
- self.parent.build()
292
- self.arrange()
293
- return
294
230
 
295
- self.children_rect.move_ip(-self.parent.scroll.x, -self.parent.scroll.y)
296
-
297
- # Calculate the width each child should fill
298
- available_width = self.children_rect.width - (len(self.parent.children) - 1) * self.gap
299
- child_width = available_width / len(self.parent.children)
300
-
301
- x = self.children_rect.left
302
- for child in self.parent.children:
303
- child.set_position(x, self.children_rect.y)
304
- child.set_autoresize_w(False)
305
- child.set_size((child_width, None))
306
- x += child_width + self.gap
307
231
 
308
- def get_raw_size(self) -> tuple[float, float]:
309
- """Calculate total size with children widths filling the available space."""
310
- if self.parent.autoresize_h :
311
- return super().get_raw_size()
312
- len_children = len(self.parent.children)
313
- if not len_children:
314
- return self.parent.rect.size
315
- parent_height = max(c.get_min_required_size()[1] for c in self.parent.children)
316
- target_rect = self.parent.inflate_rect_by_padding((0, 0, self.children_rect.width, parent_height))
317
- return target_rect.size
232
+ class RowFill(Row):
318
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)
319
243
 
320
- class ColumnFill(Column):
321
- def __init__(self, gap: int = 0, spacing: bf.spacing = bf.spacing.MANUAL):
322
- super().__init__(gap, spacing)
323
244
 
324
245
  def arrange(self) -> None:
325
- if self.parent.autoresize_h :
326
- super().arrange()
327
- return
328
- if not self.parent or not self.parent.children:
329
- return
330
- if self.child_constraints:
331
- for child in self.parent.children:
332
- child.add_constraints(*self.child_constraints)
333
- self.children_rect = self.parent.get_padded_rect()
334
-
335
- if self.parent.autoresize_w or self.parent.autoresize_h:
336
- width, height = self.get_auto_size()
337
- if self.parent.rect.size != (width, height):
338
- self.parent.set_size((width, height))
339
- self.parent.build()
340
- self.arrange()
341
- return
342
-
343
- self.children_rect.move_ip(-self.parent.scroll.x, -self.parent.scroll.y)
344
-
345
- # Calculate the height each child should fill
346
- available_height = self.children_rect.height - (len(self.parent.children) - 1) * self.gap
347
- child_height = available_height / len(self.parent.children)
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
348
255
 
349
- y = self.children_rect.top
350
- for child in self.parent.children:
351
- child.set_position(self.children_rect.x, y)
352
- child.set_autoresize_h(False)
353
- child.set_size((None, child_height))
354
- y += child_height + self.gap
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
355
260
 
356
- def get_raw_size(self) -> tuple[float, float]:
357
- """Calculate total size with children heights filling the available space."""
358
- if self.parent.autoresize_w :
359
- return super().get_raw_size()
360
- len_children = len(self.parent.children)
361
- if not len_children:
362
- return self.parent.rect.size
363
- parent_width = max(c.get_min_required_size()[0] for c in self.parent.children)
364
- target_rect = self.parent.inflate_rect_by_padding((0, 0, parent_width, self.children_rect.height))
365
- return target_rect.size
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
366
265
 
367
266
 
267
+ class ColumnFill(Column):
368
268
 
369
- class DoubleAxisLayout(Layout):
370
- """Abstract layout class for layouts that arrange widgets in two dimensions."""
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)
371
278
 
372
- @abstractmethod
373
279
  def arrange(self) -> None:
374
- """Arrange child widgets across both axes, implementation required in subclasses."""
375
- pass
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
376
288
 
377
- def focus_up_child(self) -> None:...
378
- def focus_down_child(self) -> None:...
379
- def focus_right_child(self) -> None:...
380
- def focus_left_child(self) -> None:...
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
381
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
382
298
 
383
299
 
384
300
  class Grid(DoubleAxisLayout):
@@ -388,198 +304,123 @@ class Grid(DoubleAxisLayout):
388
304
  self.cols = cols
389
305
  self.gap = gap
390
306
 
391
- def set_gap(self, value: int) -> Self:
392
- self.gap = value
393
- self.notify_parent()
394
- return self
395
-
396
- def set_dimensions(self, rows: int, cols: int) -> Self:
397
- self.rows = rows
398
- self.cols = cols
399
- self.notify_parent()
400
- return self
401
-
402
- def get_raw_size(self) -> tuple[float, float]:
403
- """Calculate raw size based on the max width and height needed to fit all children."""
404
- if not self.parent.children:
405
- return self.parent.rect.size
406
-
407
- # Calculate necessary width and height for the grid
408
- max_child_width = max(child.get_min_required_size()[0] for child in self.parent.children)
409
- max_child_height = max(child.get_min_required_size()[1] for child in self.parent.children)
410
-
411
- grid_width = self.cols * max_child_width + (self.cols - 1) * self.gap
412
- grid_height = self.rows * max_child_height + (self.rows - 1) * self.gap
413
- target_rect = self.parent.inflate_rect_by_padding((0, 0, grid_width, grid_height))
414
-
415
- return target_rect.size
416
-
417
- def arrange(self) -> None:
418
- """Arrange widgets in a grid with specified rows and columns."""
419
- if not self.parent or not self.parent.children:
307
+ def focus_up_child(self) -> None:
308
+ l = self.parent.get_interactive_children()
309
+ if not l:
420
310
  return
421
-
422
- if self.child_constraints:
423
- for child in self.parent.children:
424
- child.add_constraints(*self.child_constraints)
425
-
426
-
427
- if self.parent.autoresize_w or self.parent.autoresize_h:
428
- width, height = self.get_auto_size()
429
- if self.parent.rect.size != (width, height):
430
- self.parent.set_size((width, height))
431
- self.parent.build()
432
- self.arrange()
433
- return
434
-
435
- self.child_rect = self.parent.get_padded_rect()
436
-
437
- # Calculate cell width and height based on parent size and gaps
438
- cell_width = (self.child_rect.width - (self.cols - 1) * self.gap) / self.cols
439
- cell_height = (self.child_rect.height - (self.rows - 1) * self.gap) / self.rows
440
-
441
- for i, child in enumerate(self.parent.children):
442
- row = i // self.cols
443
- col = i % self.cols
444
- x = self.child_rect.left + col * (cell_width + self.gap)
445
- y = self.child_rect.top + row * (cell_height + self.gap)
446
-
447
- child.set_position(x, y)
448
- child.set_size((cell_width, cell_height))
449
-
450
- def handle_event(self, event):
451
-
452
- if event.type == pygame.KEYDOWN:
453
- if event.key == pygame.K_DOWN:
454
- self.focus_down_child()
455
- elif event.key == pygame.K_UP:
456
- self.focus_up_child()
457
- elif event.key == pygame.K_LEFT:
458
- self.focus_left_child()
459
- elif event.key == pygame.K_RIGHT:
460
- self.focus_right_child()
461
- else:
462
- return
463
- elif event.type == pygame.MOUSEBUTTONDOWN:
464
- r = self.parent.get_root()
465
- if not r:
466
- return
467
-
468
- if self.parent.rect.collidepoint(
469
- r.drawing_camera.screen_to_world(pygame.mouse.get_pos())
470
- ):
471
- if event.button == 4:
472
- self.parent.scroll_by((0, -10))
473
- elif event.button == 5:
474
- self.parent.scroll_by((0, 10))
475
- else:
476
- return
477
- self.parent.clamp_scroll()
478
- else:
479
- return
480
- else:
311
+ current_index = self.parent.focused_index
312
+ if current_index == -1:
481
313
  return
482
- event.consumed = True
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()
483
319
 
484
320
  def focus_down_child(self) -> None:
485
321
  l = self.parent.get_interactive_children()
486
- new_index = self.parent.focused_index + self.cols
487
- if new_index >= len(l):
322
+ if not l:
488
323
  return
489
- self.parent.focused_index = new_index
490
- focused = l[self.parent.focused_index]
491
- focused.get_focus()
492
- self.scroll_to_widget(focused)
493
-
494
- def focus_up_child(self) -> None:
495
- l = self.parent.get_interactive_children()
496
- new_index = self.parent.focused_index - self.cols
497
- if new_index < 0:
324
+ current_index = self.parent.focused_index
325
+ if current_index == -1:
498
326
  return
499
- self.parent.focused_index = new_index
500
- focused = l[self.parent.focused_index]
501
- focused.get_focus()
502
- self.scroll_to_widget(focused)
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()
503
332
 
504
333
  def focus_left_child(self) -> None:
505
334
  l = self.parent.get_interactive_children()
506
- new_index = (self.parent.focused_index % self.cols) -1
507
- if new_index < 0:
335
+ if not l:
508
336
  return
509
- self.parent.focused_index -=1
510
- focused = l[self.parent.focused_index]
511
- focused.get_focus()
512
- self.scroll_to_widget(focused)
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()
513
344
 
514
345
  def focus_right_child(self) -> None:
515
346
  l = self.parent.get_interactive_children()
516
- new_index = (self.parent.focused_index % self.cols) +1
517
- if new_index >= self.cols or self.parent.focused_index+1 >= len(l):
347
+ if not l:
518
348
  return
519
- self.parent.focused_index += 1
520
- focused = l[self.parent.focused_index]
521
- focused.get_focus()
522
- self.scroll_to_widget(focused)
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()
523
356
 
357
+ def handle_event(self, event):
358
+ if not self.parent.get_layout_children() or not self.parent.children_has_focus():
359
+ return
524
360
 
525
- class GridFill(Grid):
526
- def __init__(self, rows: int, cols: int, gap: int = 0):
527
- super().__init__(rows,cols,gap)
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)
528
384
 
529
385
  def arrange(self) -> None:
530
- """Arrange widgets to fill each grid cell, adjusting to available space."""
531
- if not self.parent or not self.parent.children:
386
+ self.update_children_rect()
387
+ if not self.parent.get_layout_children():
532
388
  return
533
389
 
534
- if self.child_constraints:
535
- for child in self.parent.children:
536
- child.add_constraints(*self.child_constraints)
537
-
538
- self.child_rect = self.parent.get_padded_rect()
539
-
540
- # If autoresize is enabled, calculate required dimensions
541
- if self.parent.autoresize_w or self.parent.autoresize_h:
542
- width, height = self.get_auto_size()
543
- if self.parent.rect.size != (width, height):
544
- self.parent.set_size((width, height))
545
- self.parent.build()
546
- self.arrange()
547
- return
548
-
549
- # Adjust for scrolling offset
550
- self.child_rect.move_ip(-self.parent.scroll.x, -self.parent.scroll.y)
551
-
552
- # Calculate cell dimensions based on available space
553
- available_width = self.child_rect.width - (self.cols - 1) * self.gap
554
- available_height = self.child_rect.height - (self.rows - 1) * self.gap
555
- cell_width = available_width / self.cols
556
- cell_height = available_height / self.rows
557
-
558
- # Position each child in the grid
559
- for index, child in enumerate(self.parent.children):
560
- row = index // self.cols
561
- col = index % self.cols
562
- x = self.child_rect.left + col * (cell_width + self.gap)
563
- y = self.child_rect.top + row * (cell_height + self.gap)
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
564
392
 
565
- child.set_position(x, y)
566
- child.set_autoresize_w(False)
567
- child.set_autoresize_h(False)
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)
568
398
  child.set_size((cell_width, cell_height))
399
+ child.set_position(x, y)
569
400
 
570
- def get_raw_size(self) -> tuple[float, float]:
571
- """Calculate the grid’s raw size based on child minimums and the grid dimensions."""
572
- if not self.parent.children:
573
- return self.parent.rect.size
574
401
 
575
- # Determine minimum cell size required by the largest child
576
- max_child_width = max(child.get_min_required_size()[0] for child in self.parent.children)
577
- max_child_height = max(child.get_min_required_size()[1] for child in self.parent.children)
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)
578
416
 
579
- # Calculate total required size for the grid
580
- grid_width = self.cols * max_child_width + (self.cols - 1) * self.gap
581
- grid_height = self.rows * max_child_height + (self.rows - 1) * self.gap
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
582
419
 
583
- # Adjust for padding and return
584
- target_rect = self.parent.inflate_rect_by_padding((0, 0, grid_width, grid_height))
585
- return target_rect.size
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)