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.
- batFramework/__init__.py +11 -15
- batFramework/animatedSprite.py +1 -1
- batFramework/camera.py +1 -1
- batFramework/character.py +1 -1
- batFramework/constants.py +10 -0
- batFramework/cutscene.py +10 -10
- batFramework/drawable.py +75 -0
- batFramework/dynamicEntity.py +2 -4
- batFramework/entity.py +93 -56
- batFramework/enums.py +1 -0
- batFramework/fontManager.py +3 -3
- batFramework/gui/__init__.py +2 -2
- batFramework/gui/{dialogueBox.py → animatedLabel.py} +18 -36
- batFramework/gui/button.py +30 -0
- batFramework/gui/clickableWidget.py +6 -1
- batFramework/gui/constraints/constraints.py +90 -1
- batFramework/gui/container.py +84 -93
- batFramework/gui/debugger.py +1 -1
- batFramework/gui/indicator.py +3 -2
- batFramework/gui/interactiveWidget.py +43 -24
- batFramework/gui/label.py +43 -49
- batFramework/gui/layout.py +378 -42
- batFramework/gui/root.py +2 -9
- batFramework/gui/shape.py +2 -0
- batFramework/gui/textInput.py +115 -78
- batFramework/gui/toggle.py +1 -4
- batFramework/gui/widget.py +50 -38
- batFramework/manager.py +65 -53
- batFramework/particle.py +1 -1
- batFramework/scene.py +1 -34
- batFramework/sceneManager.py +6 -34
- batFramework/scrollingSprite.py +1 -1
- batFramework/sprite.py +2 -2
- batFramework/{time.py → timeManager.py} +0 -2
- batFramework/utils.py +118 -19
- {batframework-1.0.8a13.dist-info → batframework-1.0.9a1.dist-info}/METADATA +1 -1
- batframework-1.0.9a1.dist-info/RECORD +63 -0
- {batframework-1.0.8a13.dist-info → batframework-1.0.9a1.dist-info}/WHEEL +1 -1
- batFramework/object.py +0 -123
- batframework-1.0.8a13.dist-info/RECORD +0 -63
- {batframework-1.0.8a13.dist-info → batframework-1.0.9a1.dist-info}/LICENCE +0 -0
- {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().
|
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
|
-
|
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
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
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.
|
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
|
batFramework/gui/layout.py
CHANGED
@@ -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.
|
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
|
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
|
-
|
64
|
-
|
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.
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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.
|
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.
|
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.
|
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.
|
223
|
-
x = self.
|
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.
|
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.
|
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
|