batframework 1.0.9a7__py3-none-any.whl → 1.0.9a8__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 (61) 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 -1
  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 +3 -1
  16. batFramework/gui/animatedLabel.py +10 -2
  17. batFramework/gui/button.py +4 -31
  18. batFramework/gui/clickableWidget.py +42 -30
  19. batFramework/gui/constraints/constraints.py +212 -136
  20. batFramework/gui/container.py +72 -48
  21. batFramework/gui/debugger.py +12 -17
  22. batFramework/gui/draggableWidget.py +8 -11
  23. batFramework/gui/image.py +3 -10
  24. batFramework/gui/indicator.py +73 -1
  25. batFramework/gui/interactiveWidget.py +117 -100
  26. batFramework/gui/label.py +73 -63
  27. batFramework/gui/layout.py +221 -452
  28. batFramework/gui/meter.py +21 -7
  29. batFramework/gui/radioButton.py +0 -1
  30. batFramework/gui/root.py +99 -29
  31. batFramework/gui/selector.py +257 -0
  32. batFramework/gui/shape.py +13 -5
  33. batFramework/gui/slider.py +260 -93
  34. batFramework/gui/textInput.py +45 -21
  35. batFramework/gui/toggle.py +70 -52
  36. batFramework/gui/tooltip.py +30 -0
  37. batFramework/gui/widget.py +203 -125
  38. batFramework/manager.py +7 -8
  39. batFramework/particle.py +4 -1
  40. batFramework/propertyEaser.py +79 -0
  41. batFramework/renderGroup.py +17 -50
  42. batFramework/resourceManager.py +43 -13
  43. batFramework/scene.py +15 -335
  44. batFramework/sceneLayer.py +138 -0
  45. batFramework/sceneManager.py +31 -36
  46. batFramework/scrollingSprite.py +8 -3
  47. batFramework/sprite.py +1 -1
  48. batFramework/templates/__init__.py +1 -2
  49. batFramework/templates/controller.py +97 -0
  50. batFramework/timeManager.py +76 -22
  51. batFramework/transition.py +37 -103
  52. batFramework/utils.py +121 -3
  53. {batframework-1.0.9a7.dist-info → batframework-1.0.9a8.dist-info}/METADATA +24 -3
  54. batframework-1.0.9a8.dist-info/RECORD +66 -0
  55. {batframework-1.0.9a7.dist-info → batframework-1.0.9a8.dist-info}/WHEEL +1 -1
  56. batFramework/character.py +0 -27
  57. batFramework/templates/character.py +0 -43
  58. batFramework/templates/states.py +0 -166
  59. batframework-1.0.9a7.dist-info/RECORD +0 -63
  60. /batframework-1.0.9a7.dist-info/LICENCE → /batframework-1.0.9a8.dist-info/LICENSE +0 -0
  61. {batframework-1.0.9a7.dist-info → batframework-1.0.9a8.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,8 @@
1
1
  from .button import Button
2
2
  from .indicator import Indicator, ToggleIndicator
3
+ from .shape import Shape
3
4
  import batFramework as bf
4
5
  from typing import Self,Callable,Any
5
- import pygame
6
-
7
6
 
8
7
  class Toggle(Button):
9
8
  def __init__(self, text: str = "", callback : Callable[[bool],Any]=None, default_value: bool = False) -> None:
@@ -54,57 +53,76 @@ class Toggle(Button):
54
53
  def get_min_required_size(self) -> tuple[float, float]:
55
54
  if not self.text_rect:
56
55
  self.text_rect.size = self._get_text_rect_required_size()
57
- size = (
58
- max(
59
- self.indicator.get_min_required_size()[0],
60
- self.text_rect.w + self.font_object.point_size + (self.gap if self.text else 0),
61
- ),
62
- self.text_rect.h+self.unpressed_relief,
63
- )
64
- return self.inflate_rect_by_padding((0, 0, *size)).size
65
56
 
66
- def _build_layout(self) -> None:
57
+ text_width, text_height = self.text_rect.size
58
+ indicator_size = self.indicator.get_min_required_size()[1]
67
59
  gap = self.gap if self.text else 0
60
+
61
+ total_width = text_width + gap + indicator_size
62
+ total_height = text_height + self.unpressed_relief
63
+
64
+ return self.expand_rect_with_padding((0, 0, total_width, total_height)).size
65
+
66
+
67
+ def _build_composed_layout(self,other:Shape):
68
+
69
+ gap = self.gap if self.text else 0
70
+ full_rect = self.text_rect.copy()
71
+
72
+ other_height = min(self.text_rect.h, self.font_object.get_height()+1)
73
+ other.set_size(other.resolve_size((other_height,other_height)))
74
+
75
+ full_rect.w += other.rect.w + gap
76
+ full_rect.h += self.unpressed_relief
77
+
78
+
79
+ # take into account the relief when calculating target size
80
+ inflated = self.expand_rect_with_padding((0, 0, *full_rect.size)).size
81
+ target_size = self.resolve_size(inflated)
82
+ if self.rect.size != target_size:
83
+ self.set_size(target_size)
84
+
85
+ self._align_composed(other)
86
+
87
+
88
+ def _align_composed(self,other:Shape):
89
+
90
+ full_rect = self.get_local_padded_rect()
91
+ left_rect = self.text_rect
92
+ right_rect = other.rect
93
+ gap = {
94
+ bf.spacing.MIN: 0,
95
+ bf.spacing.HALF: (full_rect.width - left_rect.width - right_rect.width) // 2,
96
+ bf.spacing.MAX: full_rect.width - left_rect.width - right_rect.width,
97
+ bf.spacing.MANUAL: self.gap
98
+ }.get(self.spacing, 0)
99
+
100
+ gap = max(0, gap)
101
+ combined_width = left_rect.width + right_rect.width + gap
102
+
103
+ group_x = {
104
+ bf.alignment.LEFT: full_rect.left,
105
+ bf.alignment.MIDLEFT: full_rect.left,
106
+ bf.alignment.RIGHT: full_rect.right - combined_width,
107
+ bf.alignment.MIDRIGHT: full_rect.right - combined_width,
108
+ bf.alignment.CENTER: full_rect.centerx - combined_width // 2
109
+ }.get(self.alignment, full_rect.left)
110
+
111
+ left_rect.x, right_rect.x = group_x, group_x + left_rect.width + gap
112
+
113
+ if self.alignment in {bf.alignment.TOP, bf.alignment.TOPLEFT, bf.alignment.TOPRIGHT}:
114
+ left_rect.top = right_rect.top = full_rect.top
115
+ elif self.alignment in {bf.alignment.BOTTOM, bf.alignment.BOTTOMLEFT, bf.alignment.BOTTOMRIGHT}:
116
+ left_rect.bottom = right_rect.bottom = full_rect.bottom
117
+ else:
118
+ left_rect.centery = right_rect.centery = full_rect.centery
119
+
120
+ right_rect.move_ip(*self.rect.topleft)
121
+
122
+
123
+
124
+ def _build_layout(self) -> None:
68
125
  self.text_rect.size = self._get_text_rect_required_size()
126
+ self._build_composed_layout(self.indicator)
127
+
69
128
 
70
- #right part size
71
- right_part_height = min(self.text_rect.h, self.font_object.point_size)
72
- self.indicator.set_size_if_autoresize((right_part_height,right_part_height))
73
-
74
- #join left and right
75
- joined_rect = pygame.FRect(
76
- 0, 0, self.text_rect.w + gap + self.indicator.rect.w, self.text_rect.h
77
- )
78
-
79
- if self.autoresize_h or self.autoresize_w:
80
- target_rect = self.inflate_rect_by_padding(joined_rect)
81
- target_rect.h += self.unpressed_relief
82
- if not self.autoresize_w:
83
- target_rect.w = self.rect.w
84
- if not self.autoresize_h:
85
- target_rect.h = self.rect.h
86
- if self.rect.size != target_rect.size:
87
- self.set_size(target_rect.size)
88
- self.apply_updates()
89
-
90
- # ------------------------------------ size is ok
91
-
92
- offset = self._get_outline_offset() if self.show_text_outline else (0,0)
93
- padded_rect = self.get_padded_rect()
94
- padded_relative = padded_rect.move(-self.rect.x, -self.rect.y)
95
-
96
- self.align_text(joined_rect, padded_relative.move( offset), self.alignment)
97
- self.text_rect.midleft = joined_rect.midleft
98
-
99
- if self.text:
100
- match self.spacing:
101
- case bf.spacing.MAX:
102
- gap = padded_relative.right - self.text_rect.right - self.indicator.rect.w
103
- case bf.spacing.MIN:
104
- gap = 0
105
-
106
- pos = self.text_rect.move(
107
- self.rect.x + gap -offset[0],
108
- self.rect.y + (self.text_rect.h / 2) - (right_part_height/ 2) -offset[1],
109
- ).topright
110
- self.indicator.rect.topleft = pos
@@ -0,0 +1,30 @@
1
+ from .label import Label
2
+ import batFramework as bf
3
+ import sys
4
+
5
+ class ToolTip(Label):
6
+ def __init__(self, text = ""):
7
+ super().__init__(text)
8
+ self.fade_in_duration : float = 0.1
9
+ self.fade_out_duration : float = 0.1
10
+ self.set_render_order(sys.maxsize)
11
+
12
+ def __str__(self):
13
+ return f"ToolTip('{self.text}')"
14
+
15
+ def top_at(self, x, y):
16
+ return None
17
+
18
+ def fade_in(self):
19
+ self.set_visible(True)
20
+ bf.PropertyEaser(
21
+ self.fade_in_duration,bf.easing.EASE_OUT,
22
+ 0,self.parent_scene.name
23
+ ).add_custom(self.get_alpha,self.set_alpha,255).start()
24
+
25
+ def fade_out(self):
26
+ bf.PropertyEaser(
27
+ self.fade_out_duration,bf.easing.EASE_IN,
28
+ 0,self.parent_scene.name,
29
+ lambda : self.set_visible(False)
30
+ ).add_custom(self.get_alpha,self.set_alpha,0).start()
@@ -1,4 +1,4 @@
1
- from typing import TYPE_CHECKING, Self, Callable
1
+ from typing import TYPE_CHECKING, Self, Callable, Any
2
2
  from collections.abc import Iterable
3
3
  import batFramework as bf
4
4
  import pygame
@@ -6,13 +6,13 @@ import pygame
6
6
  if TYPE_CHECKING:
7
7
  from .constraints.constraints import Constraint
8
8
  from .root import Root
9
- MAX_CONSTRAINTS = 10
10
9
 
10
+ MAX_ITERATIONS = 10
11
11
 
12
12
  class WidgetMeta(type):
13
13
  def __call__(cls, *args, **kwargs):
14
14
  obj = type.__call__(cls, *args, **kwargs)
15
- bf.StyleManager().register_widget(obj)
15
+ bf.gui.StyleManager().register_widget(obj)
16
16
  return obj
17
17
 
18
18
 
@@ -25,15 +25,21 @@ class Widget(bf.Drawable, metaclass=WidgetMeta):
25
25
  self.do_sort_children = False
26
26
  self.clip_children: bool = True
27
27
  self.padding = (0, 0, 0, 0)
28
- self.dirty_surface: bool = True # if true will call paint before drawing
29
- self.dirty_shape: bool = True # if true will call (build+paint) before drawing
30
- self.dirty_constraints: bool = False # if true will call resolve_constraints
28
+ self.dirty_surface: bool = True # If true, will call paint before drawing
29
+ self.dirty_shape: bool = True # If true, will call (build+paint) before drawing
30
+ self.dirty_position_constraints: bool = True # Flag for position-related constraints
31
+ self.dirty_size_constraints: bool = True # Flag for size-related constraints
31
32
 
33
+ self.tooltip_text: str | None = None # If not None, will display a text when hovered
32
34
  self.is_root: bool = False
33
- self.autoresize_w, self.autoresize_h = True, True
34
- self.__constraint_iteration = 0
35
- self.__constraints_to_ignore = []
36
- self.__constraints_capture = None
35
+ self.autoresize_w, self.autoresize_h = True, True # If True, the widget will have dynamic size depending on its contents
36
+ self._constraint_iteration = 0
37
+ self._constraints_to_ignore: list[Constraint] = []
38
+ self._constraints_capture: list[Constraint] = []
39
+
40
+ def set_tooltip_text(self,text:str|None)->Self:
41
+ self.tooltip_text = text
42
+ return self
37
43
 
38
44
  def show(self) -> Self:
39
45
  self.visit(lambda w: w.set_visible(True))
@@ -43,6 +49,14 @@ class Widget(bf.Drawable, metaclass=WidgetMeta):
43
49
  self.visit(lambda w: w.set_visible(False))
44
50
  return self
45
51
 
52
+ def kill(self):
53
+ if self.parent:
54
+ self.parent.remove(self)
55
+ if self.parent_scene:
56
+ self.parent_scene.remove(self.parent_layer.name,self)
57
+
58
+ return super().kill()
59
+
46
60
  def set_clip_children(self, value: bool) -> Self:
47
61
  self.clip_children = value
48
62
  self.dirty_surface = True
@@ -72,7 +86,7 @@ class Widget(bf.Drawable, metaclass=WidgetMeta):
72
86
  self.parent.do_sort_children = True
73
87
  return self
74
88
 
75
- def inflate_rect_by_padding(
89
+ def expand_rect_with_padding(
76
90
  self, rect: pygame.Rect | pygame.FRect
77
91
  ) -> pygame.Rect | pygame.FRect:
78
92
  return pygame.FRect(
@@ -112,12 +126,18 @@ class Widget(bf.Drawable, metaclass=WidgetMeta):
112
126
  def set_parent_scene(self, parent_scene: bf.Scene | None) -> Self:
113
127
  super().set_parent_scene(parent_scene)
114
128
  if parent_scene is None:
115
- bf.StyleManager().remove_widget(self)
129
+ bf.gui.StyleManager().remove_widget(self)
116
130
 
117
131
  for c in self.children:
118
132
  c.set_parent_scene(parent_scene)
119
133
  return self
120
134
 
135
+ def set_parent_layer(self, layer):
136
+ super().set_parent_layer(layer)
137
+ for c in self.children:
138
+ c.set_parent_layer(layer)
139
+ return self
140
+
121
141
  def set_parent(self, parent: "Widget") -> Self:
122
142
  if parent == self.parent:
123
143
  return self
@@ -142,7 +162,7 @@ class Widget(bf.Drawable, metaclass=WidgetMeta):
142
162
  self.dirty_shape = True
143
163
  return self
144
164
 
145
- def get_padded_rect(self) -> pygame.FRect:
165
+ def get_inner_rect(self) -> pygame.FRect:
146
166
  r = self.rect.inflate(
147
167
  -self.padding[0] - self.padding[2], -self.padding[1] - self.padding[3]
148
168
  )
@@ -152,33 +172,33 @@ class Widget(bf.Drawable, metaclass=WidgetMeta):
152
172
  def get_min_required_size(self) -> tuple[float, float]:
153
173
  return self.rect.size
154
174
 
155
- def get_padded_width(self) -> float:
175
+ def get_inner_width(self) -> float:
156
176
  return self.rect.w - self.padding[0] - self.padding[2]
157
177
 
158
- def get_padded_height(self) -> float:
178
+ def get_inner_height(self) -> float:
159
179
  return self.rect.h - self.padding[1] - self.padding[3]
160
180
 
161
- def get_padded_left(self) -> float:
181
+ def get_inner_left(self) -> float:
162
182
  return self.rect.left + self.padding[0]
163
183
 
164
- def get_padded_right(self) -> float:
165
- return self.rect.right + self.padding[2]
184
+ def get_inner_right(self) -> float:
185
+ return self.rect.right - self.padding[2]
166
186
 
167
- def get_padded_center(self) -> tuple[float, float]:
168
- return self.get_padded_rect().center
187
+ def get_inner_center(self) -> tuple[float, float]:
188
+ return self.get_inner_rect().center
169
189
 
170
- def get_padded_top(self) -> float:
190
+ def get_inner_top(self) -> float:
171
191
  return self.rect.y + self.padding[1]
172
192
 
173
- def get_padded_bottom(self) -> float:
193
+ def get_inner_bottom(self) -> float:
174
194
  return self.rect.bottom - self.padding[3]
175
195
 
176
196
  def get_debug_outlines(self):
177
197
  if self.visible:
178
198
  if any(self.padding):
179
- yield (self.get_padded_rect(), self.debug_color)
180
- else:
181
- yield (self.rect, self.debug_color)
199
+ yield (self.get_inner_rect(), self.debug_color)
200
+ # else:
201
+ yield (self.rect, self.debug_color)
182
202
  for child in self.children:
183
203
  yield from child.get_debug_outlines()
184
204
 
@@ -192,8 +212,12 @@ class Widget(bf.Drawable, metaclass=WidgetMeta):
192
212
  seen.add(c.name)
193
213
  self.constraints = result
194
214
  self.constraints.sort(key=lambda c: c.priority)
195
- self.dirty_constraints = True
196
- self.__constraint_to_ignore = []
215
+ size_c = any(c.affects_size for c in constraints)
216
+ position_c = any(c.affects_position for c in constraints)
217
+ if position_c : self.dirty_position_constraints = True
218
+ if size_c : self.dirty_size_constraints = True
219
+
220
+ self._constraints_to_ignore = []
197
221
 
198
222
  return self
199
223
 
@@ -203,60 +227,86 @@ class Widget(bf.Drawable, metaclass=WidgetMeta):
203
227
  if c.name in names:
204
228
  c.on_removal(self)
205
229
  self.constraints = [c for c in self.constraints if c.name not in names]
206
- self.__constraint_to_ignore = []
230
+ self._constraints_to_ignore = []
207
231
  return self
208
232
 
209
- def resolve_constraints(self) -> None:
233
+
234
+ def resolve_constraints(self, size_only: bool = False, position_only: bool = False) -> None:
235
+ """
236
+ Resolve constraints affecting size and/or position independently.
237
+
238
+ This system attempts to apply constraints iteratively until a stable solution is found,
239
+ or until MAX_ITERATIONS is reached.
240
+ """
210
241
  if self.parent is None or not self.constraints:
211
- self.dirty_constraints = False
242
+ if size_only:
243
+ self.dirty_size_constraints = False
244
+ if position_only:
245
+ self.dirty_position_constraints = False
212
246
  return
213
247
 
214
- if not self.__constraint_iteration:
215
- self.__constraints_capture = None
248
+ # If not currently resolving constraints, reset tracking lists
249
+ if not self._constraint_iteration:
250
+ self._constraints_capture = []
216
251
  else:
217
- capture = tuple([c.priority for c in self.constraints])
218
- if capture != self.__constraints_capture:
219
- self.__constraints_capture = capture
220
- self.__constraint_to_ignore = []
221
-
222
- constraints = self.constraints.copy()
223
- # If all are resolved early exit
224
- if all(c.evaluate(self.parent,self) for c in constraints if c not in self.__constraint_to_ignore):
225
- self.dirty_constraints = False
226
- return
227
-
228
- # # Here there might be a conflict between 2 or more constraints
229
- # we have to determine which ones causes conflict and ignore the one with least priority
230
-
231
- stop = False
232
-
233
- while True:
234
- stop = True
235
- # first pass with 2 iterations to sort out the transformative constraints
236
- for _ in range(2):
237
- for c in constraints:
238
- if c in self.__constraints_to_ignore:continue
239
- if not c.evaluate(self.parent,self) :
240
- # print(c," is applied")
241
- c.apply(self.parent,self)
242
- # second pass where we check conflicts
243
- for c in constraints:
244
- if c in self.__constraints_to_ignore:
245
- continue
246
- if not c.evaluate(self.parent,self):
247
- # first pass invalidated this constraint
248
- self.__constraints_to_ignore.append(c)
249
- stop = False
250
- break
251
-
252
- if stop:
253
- break
252
+ # Detect constraint priority changes since last resolution
253
+ current_priorities = [c.priority for c in self.constraints]
254
+ if current_priorities != self._constraints_capture:
255
+ self._constraints_capture = current_priorities
256
+ self._constraints_to_ignore = []
257
+
258
+ # Filter constraints based on what needs resolving
259
+ def is_relevant(c: "Constraint") -> bool:
260
+ return (
261
+ c.affects_size if size_only else
262
+ c.affects_position if position_only else
263
+ True
264
+ )
254
265
 
255
- if self.__constraints_to_ignore:
256
- print("Constraints ignored : ",[str(c) for c in self.__constraints_to_ignore])
257
-
258
- self.dirty_constraints = False
259
- # print(self,self.uid,"resolve constraints : Success")
266
+ active_constraints = [c for c in self.constraints if is_relevant(c)]
267
+ active_constraints.sort(key=lambda c: c.priority, reverse=True)
268
+
269
+ resolved = []
270
+ for iteration in range(MAX_ITERATIONS):
271
+ self._constraint_iteration += 1
272
+ changed = False
273
+
274
+ for constraint in active_constraints:
275
+ if constraint in resolved:
276
+ # Re-evaluate to confirm the constraint is still satisfied
277
+ if not constraint.evaluate(self.parent, self):
278
+ resolved.remove(constraint)
279
+ changed = True
280
+ else:
281
+ # Try applying unresolved constraint
282
+ if constraint.apply(self.parent, self):
283
+ resolved.append(constraint)
284
+ changed = True
285
+
286
+ if not changed:
287
+ break # All constraints stable — done
288
+
289
+ # If solution is still unstable, record the unresolved ones
290
+ if self._constraint_iteration >= MAX_ITERATIONS:
291
+ self._constraints_to_ignore += [
292
+ c for c in active_constraints if c not in resolved
293
+ ]
294
+
295
+ # Record final resolved constraints for debugging/tracking
296
+ self._constraints_capture.clear()
297
+ self._constraints_capture.extend(
298
+ (c, self._constraint_iteration) for c in resolved
299
+ )
300
+
301
+ # Clear appropriate dirty flags
302
+ if size_only:
303
+ self.dirty_size_constraints = False
304
+ if position_only:
305
+ self.dirty_position_constraints = False
306
+
307
+ # Debug print for ignored constraints
308
+ # if self._constraints_to_ignore:
309
+ # print(f"{self} ignored constraints: {[str(c) for c in self._constraints_to_ignore]}")
260
310
 
261
311
 
262
312
  def has_constraint(self, name: str) -> bool:
@@ -272,39 +322,41 @@ class Widget(bf.Drawable, metaclass=WidgetMeta):
272
322
  def top_at(self, x: float | int, y: float | int) -> "None|Widget":
273
323
  if self.children:
274
324
  for child in reversed(self.children):
275
- # if child.visible:
276
- r = child.top_at(x, y)
277
- if r is not None:
278
- return r
279
- return self if self.rect.collidepoint(x, y) else None
325
+ if child.visible:
326
+ r = child.top_at(x, y)
327
+ if r is not None:
328
+ return r
329
+ return self if self.visible and self.rect.collidepoint(x, y) else None
280
330
 
281
331
  def add(self, *children: "Widget") -> Self:
282
332
  self.children.extend(children)
283
333
  i = len(self.children)
284
334
  for child in children:
285
-
286
- child.set_render_order(i).set_parent(self).set_parent_scene(
287
- self.parent_scene
288
- )
335
+ if child.render_order == 0:
336
+ child.set_render_order(i+1)
337
+ child.set_parent(self)
338
+ child.set_parent_layer(self.parent_layer)
339
+ child.set_parent_scene(self.parent_scene)
289
340
  i += 1
290
341
  if self.parent:
291
342
  self.parent.do_sort_children = True
292
343
  return self
293
344
 
294
345
  def remove(self, *children: "Widget") -> Self:
295
- for child in self.children:
346
+ for child in self.children.copy():
296
347
  if child in children:
297
- child.set_parent(None).set_parent_scene(None)
348
+ child.set_parent(None)
349
+ child.set_parent_scene(None)
350
+ child.set_parent_layer(None)
298
351
  self.children.remove(child)
299
352
  if self.parent:
300
353
  self.parent.do_sort_children = True
301
354
 
302
- def set_size_if_autoresize(self, size: tuple[float, float]) -> Self:
303
- size = list(size)
304
- size[0] = size[0] if self.autoresize_w else None
305
- size[1] = size[1] if self.autoresize_h else None
306
- self.set_size(size)
307
- return self
355
+ def resolve_size(self, target_size):
356
+ return (
357
+ target_size[0] if self.autoresize_w else self.rect.w,
358
+ target_size[1] if self.autoresize_h else self.rect.h
359
+ )
308
360
 
309
361
  def set_size(self, size: tuple) -> Self:
310
362
  size = list(size)
@@ -312,8 +364,7 @@ class Widget(bf.Drawable, metaclass=WidgetMeta):
312
364
  size[0] = self.rect.w
313
365
  if size[1] is None:
314
366
  size[1] = self.rect.h
315
- if (int(size[0]),int(size[1])) == (int(self.rect.w),int(self.rect.h)):
316
- return self
367
+ if size[0] == self.rect.w and size[1] == self.rect.h : return self
317
368
  self.rect.size = size
318
369
  self.dirty_shape = True
319
370
  return self
@@ -331,21 +382,28 @@ class Widget(bf.Drawable, metaclass=WidgetMeta):
331
382
  _ = [c.update(dt) for c in self.children]
332
383
  super().update(dt)
333
384
 
334
- def build(self) -> None:
385
+ def build(self) -> bool:
386
+ """
387
+ Updates the size of the widget.
388
+ return True if size changed
389
+ """
335
390
  new_size = tuple(map(int, self.rect.size))
336
- if self.surface.get_size() != new_size:
337
- old_alpha = self.surface.get_alpha()
338
- new_size = [max(0, i) for i in new_size]
339
- self.surface = pygame.Surface(new_size, self.surface_flags)
340
- if self.convert_alpha:
341
- self.surface = self.surface.convert_alpha()
342
- self.surface.set_alpha(old_alpha)
391
+ if self.surface.size == new_size:
392
+ return False
393
+
394
+ old_alpha = self.surface.get_alpha()
395
+ new_size = [max(0, i) for i in new_size]
396
+ self.surface = pygame.Surface(new_size, self.surface_flags)
397
+ if self.convert_alpha:
398
+ self.surface = self.surface.convert_alpha()
399
+ self.surface.set_alpha(old_alpha)
400
+ return True
343
401
 
344
402
  def paint(self) -> None:
345
403
  self.surface.fill((0, 0, 0, 0))
346
404
  return self
347
405
 
348
- def visit(self, func: Callable, top_down: bool = True, *args, **kwargs) -> None:
406
+ def visit(self, func: Callable[["Widget"],Any], top_down: bool = True, *args, **kwargs) -> None:
349
407
  if top_down:
350
408
  func(self, *args, **kwargs)
351
409
  for child in self.children:
@@ -378,7 +436,7 @@ class Widget(bf.Drawable, metaclass=WidgetMeta):
378
436
  w = self
379
437
  tmp = w
380
438
  while not tmp.is_root:
381
- if tmp.dirty_constraints or tmp.dirty_shape:
439
+ if tmp.dirty_size_constraints or tmp.dirty_shape:
382
440
  w = tmp
383
441
  if not tmp.parent:
384
442
  break
@@ -386,49 +444,69 @@ class Widget(bf.Drawable, metaclass=WidgetMeta):
386
444
  return w
387
445
 
388
446
 
389
- def apply_updates(self) -> None:
390
-
391
- if self.dirty_constraints:
392
- self.resolve_constraints() # Finalize positioning based on final size
393
- self.dirty_constraints = False
447
+ def apply_updates(self,pass_type):
448
+ # print(f"Apply updates {pass_type} called on {self}")
449
+ if pass_type == "pre":
450
+ self.apply_pre_updates()
451
+ for child in self.children:
452
+ child.apply_updates("pre")
453
+ elif pass_type == "post":
454
+ for child in self.children:
455
+ child.apply_updates("post")
456
+ self.apply_post_updates()
457
+
458
+ def apply_pre_updates(self):
459
+ """
460
+ TOP TO BOTTOM
461
+ Resolves size-related constraints before propagating updates to children.
462
+ """
463
+ if self.dirty_size_constraints:
464
+ self.resolve_constraints(size_only=True)
465
+ self.dirty_size_constraints = False
466
+ self.dirty_position_constraints = True
467
+
468
+ def apply_post_updates(self, skip_draw: bool = False):
469
+ """
470
+ BOTTOM TO TOP
471
+ Resolves position-related constraints after propagating updates from children.
472
+ """
394
473
 
395
- # Build shape if needed
396
474
  if self.dirty_shape:
397
- self.build() # Finalize widget size
475
+
476
+ if self.build():
477
+ self.dirty_size_constraints = True
478
+ self.dirty_position_constraints = True
479
+ if self.parent :
480
+ # trigger layout or constraint updates in parent
481
+ from .container import Container
482
+ if self.parent and isinstance(self.parent, Container):
483
+ self.parent.dirty_layout = True
484
+ self.parent.dirty_shape = True
398
485
  self.dirty_shape = False
399
486
  self.dirty_surface = True
400
- self.dirty_constraints = True
401
- # Propagate dirty_constraints to children in case size affects their position
402
- for child in self.children:
403
- child.dirty_constraints = True
404
487
 
405
- # Resolve constraints now that size is finalized
406
- if self.dirty_constraints:
407
- self.resolve_constraints() # Finalize positioning based on final size
408
- self.dirty_constraints = False
409
488
 
489
+ if self.dirty_position_constraints:
490
+ self.resolve_constraints(position_only=True)
410
491
 
411
- # Step 3: Paint the surface if flagged as dirty
412
- if self.dirty_surface:
492
+ if self.dirty_surface and not skip_draw:
413
493
  self.paint()
414
494
  self.dirty_surface = False
415
-
416
495
 
417
496
 
418
497
  def draw(self, camera: bf.Camera) -> None:
419
- self.apply_updates()
420
498
  # Draw widget and handle clipping if necessary
421
499
  super().draw(camera)
422
500
 
423
501
  if self.clip_children:
424
- new_clip = camera.world_to_screen(self.get_padded_rect())
502
+ new_clip = camera.world_to_screen(self.get_inner_rect())
425
503
  old_clip = camera.surface.get_clip()
426
504
  new_clip = new_clip.clip(old_clip)
427
505
  camera.surface.set_clip(new_clip)
428
506
 
429
507
  # Draw each child widget, sorted by render order
430
508
  for child in sorted(self.children, key=lambda c: c.render_order):
431
- child.draw(camera)
432
-
509
+ if (not self.clip_children) or (child.rect.colliderect(self.rect) or not child.rect):
510
+ child.draw(camera)
433
511
  if self.clip_children:
434
512
  camera.surface.set_clip(old_clip)