batframework 1.0.8a13__py3-none-any.whl → 1.0.9a1__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 (42) hide show
  1. batFramework/__init__.py +11 -15
  2. batFramework/animatedSprite.py +1 -1
  3. batFramework/camera.py +1 -1
  4. batFramework/character.py +1 -1
  5. batFramework/constants.py +10 -0
  6. batFramework/cutscene.py +10 -10
  7. batFramework/drawable.py +75 -0
  8. batFramework/dynamicEntity.py +2 -4
  9. batFramework/entity.py +93 -56
  10. batFramework/enums.py +1 -0
  11. batFramework/fontManager.py +3 -3
  12. batFramework/gui/__init__.py +2 -2
  13. batFramework/gui/{dialogueBox.py → animatedLabel.py} +18 -36
  14. batFramework/gui/button.py +30 -0
  15. batFramework/gui/clickableWidget.py +6 -1
  16. batFramework/gui/constraints/constraints.py +90 -1
  17. batFramework/gui/container.py +84 -93
  18. batFramework/gui/debugger.py +1 -1
  19. batFramework/gui/indicator.py +3 -2
  20. batFramework/gui/interactiveWidget.py +43 -24
  21. batFramework/gui/label.py +43 -49
  22. batFramework/gui/layout.py +378 -42
  23. batFramework/gui/root.py +2 -9
  24. batFramework/gui/shape.py +2 -0
  25. batFramework/gui/textInput.py +115 -78
  26. batFramework/gui/toggle.py +1 -4
  27. batFramework/gui/widget.py +50 -38
  28. batFramework/manager.py +65 -53
  29. batFramework/particle.py +1 -1
  30. batFramework/scene.py +1 -34
  31. batFramework/sceneManager.py +6 -34
  32. batFramework/scrollingSprite.py +1 -1
  33. batFramework/sprite.py +2 -2
  34. batFramework/{time.py → timeManager.py} +0 -2
  35. batFramework/utils.py +118 -19
  36. {batframework-1.0.8a13.dist-info → batframework-1.0.9a1.dist-info}/METADATA +1 -1
  37. batframework-1.0.9a1.dist-info/RECORD +63 -0
  38. {batframework-1.0.8a13.dist-info → batframework-1.0.9a1.dist-info}/WHEEL +1 -1
  39. batFramework/object.py +0 -123
  40. batframework-1.0.8a13.dist-info/RECORD +0 -63
  41. {batframework-1.0.8a13.dist-info → batframework-1.0.9a1.dist-info}/LICENCE +0 -0
  42. {batframework-1.0.8a13.dist-info → batframework-1.0.9a1.dist-info}/top_level.txt +0 -0
batFramework/gui/label.py CHANGED
@@ -10,12 +10,14 @@ class Label(Shape):
10
10
  def __init__(self, text: str = "") -> None:
11
11
  self.text = text
12
12
 
13
+ self.scroll :pygame.Vector2 = pygame.Vector2(0,0)
14
+
13
15
  self.resized_flag: bool = False
14
16
 
15
17
  # Enable/Disable antialiasing
16
18
  self.antialias: bool = bf.FontManager().DEFAULT_ANTIALIAS
17
19
 
18
- self.text_size = bf.FontManager().DEFAULT_TEXT_SIZE
20
+ self.text_size = bf.FontManager().DEFAULT_FONT_SIZE
19
21
 
20
22
  self.auto_wraplength: bool = False
21
23
 
@@ -37,7 +39,6 @@ class Label(Shape):
37
39
  self.text_rect = pygame.FRect(0, 0, 0, 0)
38
40
  # text surface (result of font.render)
39
41
  self.text_surface: pygame.Surface = pygame.Surface((0, 0))
40
- self.do_caching: bool = False
41
42
 
42
43
  self.show_text_outline: bool = False
43
44
 
@@ -54,21 +55,10 @@ class Label(Shape):
54
55
  self.set_autoresize(True)
55
56
  self.set_font(force=True)
56
57
 
57
- @staticmethod
58
- def clear_cache():
59
- Label._text_cache = {}
60
-
58
+
61
59
  def __str__(self) -> str:
62
60
  return f"Label({repr(self.text)})"
63
61
 
64
- def enable_caching(self) -> Self:
65
- self.do_caching = True
66
- return self
67
-
68
- def disable_caching(self) -> Self:
69
- self.do_caching = False
70
- return self
71
-
72
62
  def set_text_color(self, color) -> Self:
73
63
  self.text_color = color
74
64
  self.dirty_surface = True
@@ -148,7 +138,7 @@ class Label(Shape):
148
138
  def get_debug_outlines(self):
149
139
  if self.visible:
150
140
  offset = self._get_outline_offset() if self.show_text_outline else (0,0)
151
- yield (self.text_rect.move(self.rect.x - offset[0],self.rect.y - offset[1]), "purple")
141
+ yield (self.text_rect.move(self.rect.x - offset[0] - self.scroll.x,self.rect.y - offset[1] - self.scroll.y), "purple")
152
142
  yield from super().get_debug_outlines()
153
143
 
154
144
  def set_font(self, font_name: str = None, force: bool = False) -> Self:
@@ -196,7 +186,7 @@ class Label(Shape):
196
186
  if not self.text_rect:
197
187
  self.text_rect.size = self._get_text_rect_required_size()
198
188
  res = self.inflate_rect_by_padding((0, 0, *self.text_rect.size)).size
199
-
189
+
200
190
  return res[0] if self.autoresize_w else self.rect.w, (
201
191
  res[1] if self.autoresize_h else self.rect.h
202
192
  )
@@ -205,40 +195,32 @@ class Label(Shape):
205
195
  return self.text
206
196
 
207
197
  def _render_font(self, params: dict) -> pygame.Surface:
208
- key = tuple(params.values())
209
-
210
- cached_value = Label._text_cache.get(key, None)
211
198
 
212
199
  if self.draw_mode == bf.drawMode.SOLID:
213
- if cached_value is None:
214
- params.pop("font_name")
215
-
216
- # save old settings
217
- old_italic = self.font_object.get_italic()
218
- old_bold = self.font_object.get_bold()
219
- old_underline = self.font_object.get_underline()
220
-
221
- # setup font
222
- self.font_object.set_italic(self.is_italic)
223
- self.font_object.set_bold(self.is_bold)
224
- self.font_object.set_underline(self.is_underlined)
225
-
226
- surf = self.font_object.render(**params)
227
-
228
- # reset font
229
- self.font_object.set_italic(old_italic)
230
- self.font_object.set_bold(old_bold)
231
- self.font_object.set_underline(old_underline)
232
-
233
- if self.do_caching:
234
- Label._text_cache[key] = surf
235
- else:
236
- surf = cached_value
200
+ params.pop("font_name")
201
+
202
+ # save old settings
203
+ old_italic = self.font_object.get_italic()
204
+ old_bold = self.font_object.get_bold()
205
+ old_underline = self.font_object.get_underline()
206
+
207
+ # setup font
208
+ self.font_object.set_italic(self.is_italic)
209
+ self.font_object.set_bold(self.is_bold)
210
+ self.font_object.set_underline(self.is_underlined)
211
+
212
+ surf = self.font_object.render(**params)
213
+
214
+ # reset font
215
+ self.font_object.set_italic(old_italic)
216
+ self.font_object.set_bold(old_bold)
217
+ self.font_object.set_underline(old_underline)
237
218
  else:
238
219
  params.pop("font_name")
239
220
  surf = self.font_object.render(**params)
240
221
 
241
222
  return surf
223
+
242
224
  def _get_text_rect_required_size(self):
243
225
  font_height = self.font_object.get_linesize()
244
226
  if not self.text:
@@ -267,7 +249,6 @@ class Label(Shape):
267
249
  self.text_rect.size = self._get_text_rect_required_size()
268
250
  # self.text_rect.w = ceil(self.text_rect.w)
269
251
  # self.text_rect.h = ceil(self.text_rect.h)
270
-
271
252
  if self.autoresize_h or self.autoresize_w:
272
253
  target_rect = self.inflate_rect_by_padding((0, 0, *self.text_rect.size))
273
254
  if not self.autoresize_w:
@@ -276,7 +257,7 @@ class Label(Shape):
276
257
  target_rect.h = self.rect.h
277
258
  if self.rect.size != target_rect.size:
278
259
  self.set_size(target_rect.size)
279
- self.build()
260
+ self.apply_updates()
280
261
  return
281
262
  offset = self._get_outline_offset() if self.show_text_outline else (0,0)
282
263
  padded = self.get_padded_rect().move(-self.rect.x + offset[0], -self.rect.y + offset[1])
@@ -301,7 +282,6 @@ class Label(Shape):
301
282
  }
302
283
 
303
284
  self.text_surface = self._render_font(params)
304
-
305
285
  if self.show_text_outline:
306
286
  self.text_outline_surface = (
307
287
  pygame.mask.from_surface(self.text_surface)
@@ -309,19 +289,28 @@ class Label(Shape):
309
289
  .to_surface(setcolor=self.text_outline_color, unsetcolor=(0, 0, 0, 0))
310
290
  )
311
291
 
312
- l = []
313
292
  outline_offset = self._get_outline_offset() if self.show_text_outline else (0,0)
314
293
 
294
+
295
+ # prepare fblit list
296
+ l = []
315
297
  if self.show_text_outline:
316
298
  l.append(
317
299
  (self.text_outline_surface,
318
- (self.text_rect.x - outline_offset[0],self.text_rect.y - outline_offset[1]))
300
+ (self.text_rect.x - outline_offset[0] - self.scroll.x,self.text_rect.y - outline_offset[1] - self.scroll.y))
319
301
  )
320
302
  l.append(
321
- (self.text_surface, self.text_rect)
303
+ (self.text_surface, self.text_rect.move(-self.scroll))
322
304
  )
305
+
306
+ # clip surface
307
+
308
+ old = self.surface.get_clip()
309
+ self.surface.set_clip(self.get_padded_rect().move(-self.rect.x,-self.rect.y))
323
310
  self.surface.fblits(l)
311
+ self.surface.set_clip(old)
324
312
 
313
+
325
314
  def align_text(
326
315
  self, text_rect: pygame.FRect, area: pygame.FRect, alignment: bf.alignment
327
316
  ):
@@ -334,6 +323,8 @@ class Label(Shape):
334
323
  text_rect.__setattr__(alignment.value, pos)
335
324
  text_rect.y = ceil(text_rect.y)
336
325
 
326
+
327
+
337
328
  def build(self) -> None:
338
329
  super().build()
339
330
  self._build_layout()
@@ -342,3 +333,6 @@ class Label(Shape):
342
333
  super().paint()
343
334
  if self.font_object:
344
335
  self._paint_text()
336
+
337
+ # def set_alignment(self, alignment: bf.alignment) -> Self:
338
+ # return self
@@ -2,7 +2,7 @@ import batFramework as bf
2
2
  from .widget import Widget
3
3
  from .constraints.constraints import *
4
4
  from typing import Self, TYPE_CHECKING
5
- from abc import ABC
5
+ from abc import ABC,abstractmethod
6
6
  import pygame
7
7
 
8
8
  if TYPE_CHECKING:
@@ -26,7 +26,7 @@ class Layout(ABC):
26
26
 
27
27
  def notify_parent(self) -> None:
28
28
  if self.parent:
29
- self.parent.dirty_children = True
29
+ self.parent.dirty_layout = True
30
30
 
31
31
  def arrange(self) -> None:
32
32
  return
@@ -39,7 +39,7 @@ class Layout(ABC):
39
39
 
40
40
  def get_auto_size(self) -> tuple[float, float]:
41
41
  """
42
- Returns the final size the container should have (while keeping the the width and height if they are non-resizable)
42
+ Returns the final size the container should have (while keeping the width and height if they are non-resizable)
43
43
  """
44
44
  target_size = list(self.get_raw_size())
45
45
  if not self.parent.autoresize_w:
@@ -48,25 +48,20 @@ class Layout(ABC):
48
48
  target_size[1] = self.parent.rect.h
49
49
  return target_size
50
50
 
51
- def focus_next_child(self) -> None:
52
- pass
53
-
54
- def focus_prev_child(self) -> None:
55
- pass
56
-
57
51
  def scroll_to_widget(self, widget: Widget) -> None:
58
- padded = self.parent.get_padded_rect()
59
- r = widget.rect
60
- if padded.contains(r):
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
61
55
  return
62
56
  clamped = r.clamp(padded)
63
- dx, dy = clamped.move(-r.x, -r.y).topleft
64
- self.parent.scroll_by((-dx, -dy))
57
+ # clamped.move_ip(-self.parent.rect.x,-self.parent.rect.y)
58
+ dx,dy = clamped.x - r.x,clamped.y-r.y
65
59
 
60
+ self.parent.scroll_by((-dx, -dy)) # on scroll la différence pour afficher le bouton en entier
61
+
66
62
  def handle_event(self, event):
67
63
  pass
68
64
 
69
-
70
65
  class SingleAxisLayout(Layout):
71
66
  def focus_next_child(self) -> None:
72
67
  l = self.parent.get_interactive_children()
@@ -83,6 +78,7 @@ class SingleAxisLayout(Layout):
83
78
  self.scroll_to_widget(focused)
84
79
 
85
80
 
81
+
86
82
  class Column(SingleAxisLayout):
87
83
  def __init__(self, gap: int = 0, spacing: bf.spacing = bf.spacing.MANUAL):
88
84
  super().__init__()
@@ -126,29 +122,39 @@ class Column(SingleAxisLayout):
126
122
  if self.child_constraints:
127
123
  for child in self.parent.children:
128
124
  child.add_constraints(*self.child_constraints)
129
- self.child_rect = self.parent.get_padded_rect()
130
-
131
- if self.parent.autoresize_w or self.parent.autoresize_h:
132
- width, height = self.get_auto_size()
133
- if self.parent.rect.size != (width, height):
134
- self.parent.set_size((width, height))
135
- self.parent.build()
136
- self.arrange()
137
- return
138
- self.child_rect.move_ip(-self.parent.scroll.x, -self.parent.scroll.y)
139
- y = self.child_rect.top
125
+ self.children_rect = self.parent.get_padded_rect()
126
+
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
140
142
  for child in self.parent.children:
141
- child.set_position(self.child_rect.x, y)
143
+ child.set_position(self.children_rect.x, y)
142
144
  y += child.get_min_required_size()[1] + self.gap
143
145
 
144
146
  def handle_event(self, event):
145
- if self.parent.autoresize_h or not self.parent.visible:
146
- return
147
-
148
147
  if not self.parent.children:
149
148
  return
150
149
 
151
- if event.type == pygame.MOUSEBUTTONDOWN:
150
+ 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:
152
158
  r = self.parent.get_root()
153
159
  if not r:
154
160
  return
@@ -162,9 +168,12 @@ class Column(SingleAxisLayout):
162
168
  self.parent.scroll_by((0, 10))
163
169
  else:
164
170
  return
165
- event.consumed = True
166
171
  self.parent.clamp_scroll()
167
-
172
+ else:
173
+ return
174
+ else:
175
+ return
176
+ event.consumed = True
168
177
 
169
178
  class Row(SingleAxisLayout):
170
179
  def __init__(self, gap: int = 0, spacing: bf.spacing = bf.spacing.MANUAL):
@@ -210,7 +219,7 @@ class Row(SingleAxisLayout):
210
219
  if self.child_constraints:
211
220
  for child in self.parent.children:
212
221
  child.add_constraints(*self.child_constraints)
213
- self.child_rect = self.parent.get_padded_rect()
222
+ self.children_rect = self.parent.get_padded_rect()
214
223
 
215
224
  if self.parent.autoresize_w or self.parent.autoresize_h:
216
225
  width, height = self.get_auto_size()
@@ -219,20 +228,25 @@ class Row(SingleAxisLayout):
219
228
  self.parent.build()
220
229
  self.arrange()
221
230
  return
222
- self.child_rect.move_ip(-self.parent.scroll.x, -self.parent.scroll.y)
223
- x = self.child_rect.left
231
+ self.children_rect.move_ip(-self.parent.scroll.x, -self.parent.scroll.y)
232
+ x = self.children_rect.left
224
233
  for child in self.parent.children:
225
- child.set_position(x, self.child_rect.y)
234
+ child.set_position(x, self.children_rect.y)
226
235
  x += child.get_min_required_size()[0] + self.gap
227
236
 
228
237
  def handle_event(self, event):
229
- if self.parent.autoresize_w or not self.parent.visible:
230
- return
231
-
232
238
  if not self.parent.children:
233
239
  return
234
240
 
235
- if event.type == pygame.MOUSEBUTTONDOWN:
241
+ 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:
236
250
  r = self.parent.get_root()
237
251
  if not r:
238
252
  return
@@ -245,5 +259,327 @@ class Row(SingleAxisLayout):
245
259
  self.parent.scroll_by((10, 0))
246
260
  else:
247
261
  return
248
- event.consumed = True
249
262
  self.parent.clamp_scroll()
263
+ else:
264
+ return
265
+ else:
266
+ return
267
+
268
+
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
+ def arrange(self) -> None:
276
+ if self.parent.autoresize_h :
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()
286
+
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
+
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
+
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
318
+
319
+
320
+ class ColumnFill(Column):
321
+ def __init__(self, gap: int = 0, spacing: bf.spacing = bf.spacing.MANUAL):
322
+ super().__init__(gap, spacing)
323
+
324
+ 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)
348
+
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
355
+
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
366
+
367
+
368
+
369
+ class DoubleAxisLayout(Layout):
370
+ """Abstract layout class for layouts that arrange widgets in two dimensions."""
371
+
372
+ @abstractmethod
373
+ def arrange(self) -> None:
374
+ """Arrange child widgets across both axes, implementation required in subclasses."""
375
+ pass
376
+
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:...
381
+
382
+
383
+
384
+ class Grid(DoubleAxisLayout):
385
+ def __init__(self, rows: int, cols: int, gap: int = 0):
386
+ super().__init__()
387
+ self.rows = rows
388
+ self.cols = cols
389
+ self.gap = gap
390
+
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:
420
+ 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:
481
+ return
482
+ event.consumed = True
483
+
484
+ def focus_down_child(self) -> None:
485
+ l = self.parent.get_interactive_children()
486
+ new_index = self.parent.focused_index + self.cols
487
+ if new_index >= len(l):
488
+ 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:
498
+ 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)
503
+
504
+ def focus_left_child(self) -> None:
505
+ l = self.parent.get_interactive_children()
506
+ new_index = (self.parent.focused_index % self.cols) -1
507
+ if new_index < 0:
508
+ return
509
+ self.parent.focused_index -=1
510
+ focused = l[self.parent.focused_index]
511
+ focused.get_focus()
512
+ self.scroll_to_widget(focused)
513
+
514
+ def focus_right_child(self) -> None:
515
+ 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):
518
+ return
519
+ self.parent.focused_index += 1
520
+ focused = l[self.parent.focused_index]
521
+ focused.get_focus()
522
+ self.scroll_to_widget(focused)
523
+
524
+
525
+ class GridFill(Grid):
526
+ def __init__(self, rows: int, cols: int, gap: int = 0):
527
+ super().__init__(rows,cols,gap)
528
+
529
+ 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:
532
+ return
533
+
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)
564
+
565
+ child.set_position(x, y)
566
+ child.set_autoresize_w(False)
567
+ child.set_autoresize_h(False)
568
+ child.set_size((cell_width, cell_height))
569
+
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
+
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)
578
+
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
582
+
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