batframework 1.0.9a10__py3-none-any.whl → 1.1.0__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 (76) hide show
  1. batFramework/__init__.py +52 -76
  2. batFramework/action.py +99 -126
  3. batFramework/actionContainer.py +9 -53
  4. batFramework/animatedSprite.py +114 -56
  5. batFramework/audioManager.py +36 -82
  6. batFramework/camera.py +69 -263
  7. batFramework/constants.py +53 -29
  8. batFramework/cutscene.py +109 -243
  9. batFramework/cutsceneBlocks.py +176 -0
  10. batFramework/debugger.py +48 -0
  11. batFramework/dynamicEntity.py +9 -16
  12. batFramework/easing.py +71 -0
  13. batFramework/entity.py +85 -92
  14. batFramework/gui/__init__.py +3 -14
  15. batFramework/gui/button.py +78 -12
  16. batFramework/gui/constraints.py +204 -0
  17. batFramework/gui/container.py +31 -183
  18. batFramework/gui/debugger.py +43 -126
  19. batFramework/gui/frame.py +19 -0
  20. batFramework/gui/image.py +20 -55
  21. batFramework/gui/indicator.py +22 -95
  22. batFramework/gui/interactiveWidget.py +12 -229
  23. batFramework/gui/label.py +77 -311
  24. batFramework/gui/layout.py +66 -411
  25. batFramework/gui/root.py +35 -203
  26. batFramework/gui/shape.py +57 -247
  27. batFramework/gui/toggle.py +48 -114
  28. batFramework/gui/widget.py +243 -457
  29. batFramework/manager.py +29 -113
  30. batFramework/particles.py +77 -0
  31. batFramework/scene.py +217 -22
  32. batFramework/sceneManager.py +129 -161
  33. batFramework/stateMachine.py +8 -11
  34. batFramework/time.py +75 -0
  35. batFramework/transition.py +124 -129
  36. batFramework/transitionManager.py +0 -0
  37. batFramework/triggerZone.py +4 -4
  38. batFramework/utils.py +144 -266
  39. {batframework-1.0.9a10.dist-info → batframework-1.1.0.dist-info}/METADATA +24 -22
  40. batframework-1.1.0.dist-info/RECORD +43 -0
  41. batFramework/animation.py +0 -77
  42. batFramework/baseScene.py +0 -240
  43. batFramework/cutsceneManager.py +0 -34
  44. batFramework/drawable.py +0 -77
  45. batFramework/easingController.py +0 -58
  46. batFramework/enums.py +0 -135
  47. batFramework/fontManager.py +0 -65
  48. batFramework/gui/animatedLabel.py +0 -89
  49. batFramework/gui/clickableWidget.py +0 -245
  50. batFramework/gui/constraints/__init__.py +0 -1
  51. batFramework/gui/constraints/constraints.py +0 -980
  52. batFramework/gui/draggableWidget.py +0 -44
  53. batFramework/gui/meter.py +0 -96
  54. batFramework/gui/radioButton.py +0 -35
  55. batFramework/gui/selector.py +0 -250
  56. batFramework/gui/slider.py +0 -397
  57. batFramework/gui/style.py +0 -10
  58. batFramework/gui/styleManager.py +0 -54
  59. batFramework/gui/syncedVar.py +0 -49
  60. batFramework/gui/textInput.py +0 -306
  61. batFramework/gui/tooltip.py +0 -30
  62. batFramework/particle.py +0 -118
  63. batFramework/propertyEaser.py +0 -79
  64. batFramework/renderGroup.py +0 -34
  65. batFramework/resourceManager.py +0 -130
  66. batFramework/sceneLayer.py +0 -138
  67. batFramework/scrollingSprite.py +0 -115
  68. batFramework/sprite.py +0 -51
  69. batFramework/templates/__init__.py +0 -1
  70. batFramework/templates/controller.py +0 -97
  71. batFramework/tileset.py +0 -46
  72. batFramework/timeManager.py +0 -213
  73. batframework-1.0.9a10.dist-info/RECORD +0 -67
  74. {batframework-1.0.9a10.dist-info → batframework-1.1.0.dist-info}/LICENSE +0 -0
  75. {batframework-1.0.9a10.dist-info → batframework-1.1.0.dist-info}/WHEEL +0 -0
  76. {batframework-1.0.9a10.dist-info → batframework-1.1.0.dist-info}/top_level.txt +0 -0
@@ -1,521 +1,307 @@
1
- from typing import TYPE_CHECKING, Self, Callable, Any
2
- from collections.abc import Iterable
3
- import batFramework as bf
4
- import pygame
5
-
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
6
3
  if TYPE_CHECKING:
7
- from .constraints.constraints import Constraint
4
+ from .constraints import Constraint
8
5
  from .root import Root
6
+ from typing import Self
9
7
 
10
- MAX_ITERATIONS = 10
11
-
12
- class WidgetMeta(type):
13
- def __call__(cls, *args, **kwargs):
14
- obj = type.__call__(cls, *args, **kwargs)
15
- bf.gui.StyleManager().register_widget(obj)
16
- return obj
17
-
18
-
19
- class Widget(bf.Drawable, metaclass=WidgetMeta):
20
- def __init__(self, *args, **kwargs) -> None:
21
- super().__init__(*args, **kwargs)
22
- self.children: list["Widget"] = []
23
- self.constraints: list[Constraint] = []
24
- self.parent: "Widget" = None
25
- self.do_sort_children = False
26
- self.clip_children: bool = True
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_position_constraints: bool = True # Flag for position-related constraints
31
- self.dirty_size_constraints: bool = True # Flag for size-related constraints
32
-
33
- self.tooltip_text: str | None = None # If not None, will display a text when hovered
34
- self.is_root: bool = False
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
43
-
44
- def show(self) -> Self:
45
- self.visit(lambda w: w.set_visible(True))
46
- return self
47
-
48
- def hide(self) -> Self:
49
- self.visit(lambda w: w.set_visible(False))
50
- return self
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()
8
+ import batFramework as bf
9
+ import pygame
10
+ from math import ceil
11
+
12
+
13
+
14
+ class Widget(bf.Entity):
15
+ def __init__(self,convert_alpha=True)->None:
16
+ super().__init__(convert_alpha=convert_alpha)
17
+ self.autoresize = False
18
+ self.parent : None|Self = None
19
+ self.is_root :bool = False
20
+ self.children : list["Widget"] = []
21
+ self.focusable :bool= False
22
+ self.constraints : list[Constraint] = []
23
+ self.gui_depth : int = 0
24
+ if self.surface : self.surface.fill("white")
25
+ self.set_debug_color("red")
26
+ self.padding :tuple[float|int,...]= (0,0,0,0)
27
+
28
+ def set_padding(self,value : float|int|tuple|list)->Self:
29
+ old_raw_size = (
30
+ self.rect.w - self.padding[0] - self.padding[2],
31
+ self.rect.h - self.padding[1] - self.padding[3]
32
+ )
33
+ if isinstance(value,list) or isinstance(value,tuple):
34
+ if len(value) > 4 : return self
35
+ if any(v<0 for v in value) : return self
36
+ if len(value) == 2:
37
+ self.padding = (value[0],value[1],value[0],value[1])
38
+ else:
39
+ self.padding = (*value, *self.padding[len(value):])
40
+ else:
41
+ self.padding = (value,)*4
59
42
 
60
- def set_clip_children(self, value: bool) -> Self:
61
- self.clip_children = value
62
- self.dirty_surface = True
43
+ self.set_size(
44
+ old_raw_size[0] + self.padding[0] + self.padding[2],
45
+ old_raw_size[1] + self.padding[1] + self.padding[3],
46
+ )
63
47
  return self
48
+
49
+ def inflate_rect_by_padding(self,rect:pygame.FRect)->pygame.FRect:
50
+ return pygame.FRect(
51
+ rect[0] - self.padding[0],
52
+ rect[1] - self.padding[1],
53
+ rect[2] + self.padding[0]+self.padding[2],
54
+ rect[3] + self.padding[1]+self.padding[3]
55
+ )
56
+ def get_content_left(self)->float:
57
+ return self.rect.left + self.padding[0]
64
58
 
65
- def __str__(self) -> str:
66
- return "Widget"
59
+ def get_content_top(self)->float:
60
+ return self.rect.top + self.padding[1]
67
61
 
68
- def set_autoresize(self, value: bool) -> Self:
69
- self.autoresize_w = self.autoresize_h = value
70
- self.dirty_shape = True
71
- return self
62
+ def get_content_right(self)->float:
63
+ return self.rect.right - self.padding[2]
72
64
 
73
- def set_autoresize_w(self, value: bool) -> Self:
74
- self.autoresize_w = value
75
- self.dirty_shape = True
76
- return self
65
+ def get_content_bottom(self)->float:
66
+ return self.rect.bottom - self.padding[3]
77
67
 
78
- def set_autoresize_h(self, value: bool) -> Self:
79
- self.autoresize_h = value
80
- self.dirty_shape = True
81
- return self
68
+ def get_content_width(self)->float:
69
+ return self.rect.w - self.padding[0] - self.padding[2]
82
70
 
83
- def set_render_order(self, render_order: int) -> Self:
84
- super().set_render_order(render_order)
85
- if self.parent:
86
- self.parent.do_sort_children = True
87
- return self
71
+ def get_content_height(self)->float:
72
+ return self.rect.h - self.padding[1] - self.padding[3]
88
73
 
89
- def expand_rect_with_padding(
90
- self, rect: pygame.Rect | pygame.FRect
91
- ) -> pygame.Rect | pygame.FRect:
74
+ def get_content_rect(self)->pygame.FRect:
92
75
  return pygame.FRect(
93
- rect[0] - self.padding[0],
94
- rect[1] - self.padding[1],
95
- rect[2] + self.padding[0] + self.padding[2],
96
- rect[3] + self.padding[1] + self.padding[3],
76
+ self.rect.left + self.padding[0],
77
+ self.rect.top + self.padding[1],
78
+ self.get_content_width(),
79
+ self.get_content_height()
97
80
  )
98
81
 
99
- def set_position(self, x, y) -> Self:
100
- if x is None:
101
- x = self.rect.x
102
- if y is None:
103
- y = self.rect.y
104
- if (x, y) == self.rect.topleft:
105
- return self
106
- dx, dy = x - self.rect.x, y - self.rect.y
107
- self.rect.topleft = x, y
108
- _ = [c.set_position(c.rect.x + dx, c.rect.y + dy) for c in self.children]
109
- return self
110
-
111
- def set_center(self, x, y) -> Self:
112
- if x is None:
113
- x = self.rect.centerx
114
- if y is None:
115
- y = self.rect.centery
116
- if (x, y) == self.rect.center:
117
- return self
118
- dx, dy = x - self.rect.centerx, y - self.rect.centery
119
- self.rect.center = x, y
120
- _ = [
121
- c.set_center(c.rect.centerx + dx, c.rect.centery + dy)
122
- for c in self.children
123
- ]
124
- return self
82
+ def get_content_rect_rel(self)->pygame.FRect:
83
+ return self.get_content_rect().move(-self.rect.left,-self.rect.top)
125
84
 
126
- def set_parent_scene(self, parent_scene: bf.Scene | None) -> Self:
127
- super().set_parent_scene(parent_scene)
128
- if parent_scene is None:
129
- bf.gui.StyleManager().remove_widget(self)
85
+ def get_content_center(self)->tuple[float,float]:
86
+ return self.get_content_rect().center
130
87
 
131
- for c in self.children:
132
- c.set_parent_scene(parent_scene)
133
- return self
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
88
+ def get_depth(self)->int:
89
+ if self.is_root or self.parent is None :
90
+ self.gui_depth = 0
91
+ else:
92
+ self.gui_depth = self.parent.get_depth() + 1
93
+ return self.gui_depth
140
94
 
141
- def set_parent(self, parent: "Widget") -> Self:
142
- if parent == self.parent:
95
+ def top_at(self, x: float, y: float) -> "None|Widget":
96
+ if self.children:
97
+ for child in reversed(self.children):
98
+ r = child.top_at(x,y)
99
+ if r is not None:
100
+ return r
101
+ if self.rect.collidepoint(x,y) and self.visible:
143
102
  return self
144
- # if self.parent is not None and self.parent != parent:
145
- # self.parent.remove(self)
146
- self.parent = parent
147
- return self
148
-
149
- def set_padding(self, value: float | int | tuple | list) -> Self:
150
- if isinstance(value, Iterable):
151
- if len(value) > 4:
152
- pass
153
- elif any(v < 0 for v in value):
154
- pass
155
- elif len(value) == 2:
156
- self.padding = (value[0], value[1], value[0], value[1])
157
- else:
158
- self.padding = (*value, *self.padding[len(value) :])
159
- else:
160
- self.padding = (value,) * 4
103
+ return None
104
+
105
+ def get_constraint(self,name:str)->Constraint|None:
106
+ return next((c for c in self.constraints if c.name == name), None)
161
107
 
162
- self.dirty_shape = True
108
+ def add_constraints(self,*constraints:Constraint)->Self:
109
+ for c in constraints:
110
+ self.add_constraint(c,False)
111
+ self.apply_constraints()
112
+ return self
113
+
114
+ def add_constraint(self,constraint:Constraint,apply:bool=True)->Self:
115
+ c = self.get_constraint(constraint.name)
116
+ if c is not None:
117
+ self.constraints.remove(c)
118
+ self.constraints.append(constraint)
119
+ self.apply_constraints()
163
120
  return self
164
121
 
165
- def get_inner_rect(self) -> pygame.FRect:
166
- r = self.rect.inflate(
167
- -self.padding[0] - self.padding[2], -self.padding[1] - self.padding[3]
168
- )
169
- # r.normalize()
170
- return r
122
+ def has_constraint(self,name:str)->bool:
123
+ return any(c.name == name for c in self.constraints)
171
124
 
172
- def get_min_required_size(self) -> tuple[float, float]:
173
- return self.rect.size
125
+ def apply_all_constraints(self)->None:
126
+ # print("APPLY ALL CONSTRAINTS IN ",self.to_string())
127
+ self.apply_constraints()
128
+ for child in self.children : child.apply_all_constraints()
129
+
130
+ def apply_constraints(self, max_iterations: int = 10) -> None:
131
+ if not self.parent:
132
+ # print(f"Warning : can't apply constraints on {self.to_string()} without parent widget")
133
+ return
134
+ if not self.constraints:
135
+ return
136
+ # Sort constraints based on priority
137
+ self.constraints.sort(key=lambda c: c.priority)
174
138
 
175
- def get_inner_width(self) -> float:
176
- return self.rect.w - self.padding[0] - self.padding[2]
139
+ for iteration in range(max_iterations):
140
+ unsatisfied = [] # Initialize a flag
177
141
 
178
- def get_inner_height(self) -> float:
179
- return self.rect.h - self.padding[1] - self.padding[3]
142
+ for constraint in self.constraints:
143
+ if not constraint.evaluate(self.parent, self):
144
+ unsatisfied.append(constraint)
145
+ constraint.apply(self.parent, self)
146
+ if not unsatisfied:
147
+ # data = ''.join(f"\n\t->{c.to_string()}" for c in self.constraints)
148
+ # print(self.get_depth()*'\t'+f"Following constraints of {self.to_string()} were all satisfied :{data}")
149
+ break
150
+ # print(f"pass {iteration}/{max_iterations} : unsatisfied = {';'.join(c.to_string() for c in unsatisfied)}")
151
+ if iteration == max_iterations - 1:
152
+ raise ValueError(f"[WARNING] Following constraints for {self.to_string()} were not satisfied : \n\t{';'.join([c.to_string() for c in unsatisfied])}")
180
153
 
181
- def get_inner_left(self) -> float:
182
- return self.rect.left + self.padding[0]
154
+ # GETTERS
155
+ def get_root(self)-> Self|Root|None:
156
+ if self.is_root: return self
157
+ if self.parent_scene is not None : return self.parent_scene.root
158
+ return None if self.parent is None else self.parent.get_root()
183
159
 
184
- def get_inner_right(self) -> float:
185
- return self.rect.right - self.padding[2]
160
+ def get_size_int(self)->tuple[int,int]:
161
+ return (ceil(self.rect.width),ceil(self.rect.height))
186
162
 
187
- def get_inner_center(self) -> tuple[float, float]:
188
- return self.get_inner_rect().center
189
163
 
190
- def get_inner_top(self) -> float:
191
- return self.rect.y + self.padding[1]
164
+ def get_center(self)->tuple[float,float]:
165
+ return self.rect.center
192
166
 
193
- def get_inner_bottom(self) -> float:
194
- return self.rect.bottom - self.padding[3]
195
167
 
196
- def get_debug_outlines(self):
197
- if self.visible:
198
- if any(self.padding):
199
- yield (self.get_inner_rect(), self.debug_color)
200
- # else:
201
- yield (self.rect, self.debug_color)
168
+ def get_bounding_box(self):
169
+ yield (self.rect,self._debug_color)
170
+ yield (self.get_content_rect(),"yellow")
202
171
  for child in self.children:
203
- yield from child.get_debug_outlines()
172
+ yield from child.get_bounding_box()
204
173
 
205
- def add_constraints(self, *constraints: "Constraint") -> Self:
206
- # Add constraints without duplicates
207
- existing_names = {c.name for c in self.constraints}
208
- new_constraints = [c for c in constraints if c.name not in existing_names]
209
- self.constraints.extend(new_constraints)
174
+ def set_autoresize(self,value:bool)-> Self:
175
+ self.autoresize = value
176
+ self.build()
177
+ return self
210
178
 
211
- # Sort constraints by priority
212
- self.constraints.sort(key=lambda c: c.priority)
179
+ def set_parent(self,parent:Self|None)->None:
180
+ if self.parent:
181
+ self.parent.remove_child(self)
182
+ self.parent = parent
183
+ self.apply_all_constraints()
184
+ # SETTERS
185
+
186
+ def set_root(self) -> Self:
187
+ self.is_root = True
188
+ return self
213
189
 
214
- # Update dirty flags based on the new constraints
215
- if any(c.affects_size for c in new_constraints):
216
- self.dirty_size_constraints = True
217
- if any(c.affects_position for c in new_constraints):
218
- self.dirty_position_constraints = True
190
+ def set_parent_scene(self,scene)->None:
191
+ super().set_parent_scene(scene)
192
+ for child in self.children :
193
+ child.set_parent_scene(scene)
219
194
 
220
- # Clear ignored constraints
221
- self._constraints_to_ignore = []
195
+ def set_x(self,x:float)->Self:
196
+ delta = x - self.rect.x
197
+ self.rect.x = x
198
+ for child in self.children:
199
+ child.set_x(child.rect.x + delta)
200
+ self.apply_constraints()
201
+ return self
202
+
203
+ def set_y(self,y:float)->Self:
204
+ delta = y - self.rect.y
205
+ self.rect.y = y
206
+ for child in self.children:
207
+ child.set_y(child.rect.y + delta)
208
+ self.apply_constraints()
222
209
 
223
210
  return self
224
211
 
212
+ def set_position(self,x:float,y:float)->Self:
213
+ delta_x = x - self.rect.x
214
+ delta_y = y - self.rect.y
215
+ self.rect.topleft = x,y
216
+ for child in self.children:
217
+ child.set_position(child.rect.x + delta_x,child.rect.y+delta_y)
218
+ self.apply_constraints()
219
+ return self
220
+
221
+ def set_center(self,x:float,y:float)->Self:
222
+ delta_x = x - self.rect.centerx
223
+ delta_y = y - self.rect.centery
224
+ self.rect.center = x,y
225
+ for child in self.children:
226
+ child.set_position(child.rect.x + delta_x,child.rect.y+delta_y)
227
+ self.apply_constraints()
228
+ return self
225
229
 
226
- def remove_constraints(self, *names: str) -> Self:
227
- for c in self.constraints:
228
- if c.name in names:
229
- c.on_removal(self)
230
- self.constraints = [c for c in self.constraints if c.name not in names]
231
- self._constraints_to_ignore = []
232
- self.dirty_size_constraints = True
233
- self.dirty_position_constraints= True
234
230
 
231
+ def set_size(self, width : float, height: float) -> Self:
232
+ self.rect.size = (width,height)
233
+ self.build()
235
234
  return self
235
+
236
236
 
237
+ # Other Methods
237
238
 
238
- def resolve_constraints(self, size_only: bool = False, position_only: bool = False) -> None:
239
- """
240
- Resolve constraints affecting size and/or position independently.
239
+ def print_tree(self,ident:int=0)->None:
240
+ print('\t'*ident+self.to_string()+(':' if self.children else ''))
241
+ for child in self.children :
242
+ child.print_tree(ident+1)
241
243
 
242
- This system attempts to apply constraints iteratively until a stable solution is found,
243
- or until MAX_ITERATIONS is reached.
244
- """
245
- if self.parent is None or not self.constraints:
246
- if size_only:
247
- self.dirty_size_constraints = False
248
- if position_only:
249
- self.dirty_position_constraints = False
250
- return
244
+ def to_string(self)->str:
245
+
246
+ return f"{self.to_string_id()}@{*self.rect.topleft,* self.rect.size}"
251
247
 
252
- # If not currently resolving constraints, reset tracking lists
253
- if not self._constraint_iteration:
254
- self._constraints_capture = []
255
- else:
256
- # Detect constraint priority changes since last resolution
257
- current_priorities = [c.priority for c in self.constraints]
258
- if current_priorities != self._constraints_capture:
259
- self._constraints_capture = current_priorities
260
- self._constraints_to_ignore = []
261
-
262
- # Filter constraints based on what needs resolving
263
- def is_relevant(c: "Constraint") -> bool:
264
- return (
265
- c.affects_size if size_only else
266
- c.affects_position if position_only else
267
- True
268
- )
269
-
270
- active_constraints = [c for c in self.constraints if is_relevant(c)]
271
- active_constraints.sort(key=lambda c: c.priority, reverse=True)
272
-
273
- resolved = []
274
- for iteration in range(MAX_ITERATIONS):
275
- self._constraint_iteration += 1
276
- changed = False
277
-
278
- for constraint in active_constraints:
279
- if constraint in resolved:
280
- # Re-evaluate to confirm the constraint is still satisfied
281
- if not constraint.evaluate(self.parent, self):
282
- resolved.remove(constraint)
283
- changed = True
284
- else:
285
- # Try applying unresolved constraint
286
- if constraint.apply(self.parent, self):
287
- resolved.append(constraint)
288
- changed = True
289
-
290
- if not changed:
291
- break # All constraints stable — done
292
-
293
- # If solution is still unstable, record the unresolved ones
294
- if self._constraint_iteration >= MAX_ITERATIONS:
295
- self._constraints_to_ignore += [
296
- c for c in active_constraints if c not in resolved
297
- ]
298
-
299
- # Record final resolved constraints for debugging/tracking
300
- self._constraints_capture.clear()
301
- self._constraints_capture.extend(
302
- (c, self._constraint_iteration) for c in resolved
303
- )
304
248
 
305
- # Clear appropriate dirty flags
306
- if size_only:
307
- self.dirty_size_constraints = False
308
- if position_only:
309
- self.dirty_position_constraints = False
249
+ def to_string_id(self)->str:
250
+ return "Widget"
310
251
 
311
- # Debug print for ignored constraints
312
- # if self._constraints_to_ignore:
313
- # print(f"{self} ignored constraints: {[str(c) for c in self._constraints_to_ignore]}")
314
252
 
253
+ def do_when_removed(self)->None:
254
+ if self.parent_scene and self.parent == self.parent_scene.root:
255
+ self.set_parent(None)
315
256
 
316
- def has_constraint(self, name: str) -> bool:
317
- return any(c.name == name for c in self.constraints)
257
+ # Methods on children
318
258
 
319
- def get_root(self) -> "Root":
320
- if self.is_root:
321
- return self
322
- if self.parent:
323
- return self.parent.get_root()
324
- return None
259
+ def add_child(self,*child:"Widget")->None:
260
+ for c in child :
261
+ self.children.append(c)
262
+ c.set_parent(self)
263
+ c.set_parent_scene(self.parent_scene)
264
+ c.apply_constraints()
265
+ self.children_modified()
325
266
 
326
- def get_by_tags(self,*tags: str)->list["Widget"]:
327
- #use self.has_tags(*tags) for check
328
- result = []
329
- self.visit(lambda w: result.append(w) if w.has_tags(*tags) else None)
330
- return result
331
-
332
- def top_at(self, x: float | int, y: float | int) -> "None|Widget":
333
- if self.children:
334
- for child in reversed(self.children):
335
- if child.visible:
336
- r = child.top_at(x, y)
337
- if r is not None:
338
- return r
339
- return self if self.visible and self.rect.collidepoint(x, y) else None
340
-
341
- def add(self, *children: "Widget") -> Self:
342
- self.children.extend(children)
343
- i = len(self.children)
344
- for child in children:
345
- if child.render_order == 0:
346
- child.set_render_order(i+1)
347
- child.set_parent(self)
348
- child.set_parent_layer(self.parent_layer)
349
- child.set_parent_scene(self.parent_scene)
350
- i += 1
351
- if self.parent:
352
- self.parent.do_sort_children = True
353
- return self
267
+ def remove_child(self,child:Self)->None:
268
+ self.children.remove(child)
269
+ child.set_parent(None)
270
+ child.set_parent_scene(None)
271
+ self.children_modified()
354
272
 
355
- def remove(self, *children: "Widget") -> Self:
356
- for child in self.children.copy():
357
- if child in children:
358
- child.set_parent(None)
359
- child.set_parent_scene(None)
360
- child.set_parent_layer(None)
361
- self.children.remove(child)
362
- if self.parent:
363
- self.parent.do_sort_children = True
364
273
 
365
- def resolve_size(self, target_size):
366
- return (
367
- target_size[0] if self.autoresize_w else self.rect.w,
368
- target_size[1] if self.autoresize_h else self.rect.h
369
- )
370
274
 
371
- def set_size(self, size: tuple) -> Self:
372
- size = list(size)
373
- if size[0] is None:
374
- size[0] = self.rect.w
375
- if size[1] is None:
376
- size[1] = self.rect.h
377
- if size[0] == self.rect.w and size[1] == self.rect.h : return self
378
- self.rect.size = size
379
- self.dirty_shape = True
380
- return self
381
275
 
382
- def process_event(self, event: pygame.Event) -> None:
276
+ # if return True -> don't propagate to siblings or parents
277
+ def process_event(self, event: pygame.Event)->bool:
383
278
  # First propagate to children
384
279
  for child in self.children:
385
- child.process_event(event)
386
- super().process_event(event)
280
+ if child.process_event(event):
281
+ return True
282
+ #return True if the method is blocking (no propagation to next children of the scene)
283
+ return super().process_event(event)
387
284
 
388
- def update(self, dt) -> None:
389
- if self.do_sort_children:
390
- self.children.sort(key=lambda c: c.render_order)
391
- self.do_sort_children = False
392
- _ = [c.update(dt) for c in self.children]
393
- super().update(dt)
394
-
395
- def build(self) -> bool:
396
- """
397
- Updates the size of the widget.
398
- return True if size changed
399
- """
400
- new_size = tuple(map(int, self.rect.size))
401
- if self.surface.get_size() == new_size:
402
- return False
403
-
404
- old_alpha = self.surface.get_alpha()
405
- new_size = [max(0, i) for i in new_size]
406
- self.surface = pygame.Surface(new_size, self.surface_flags)
407
- if self.convert_alpha:
408
- self.surface = self.surface.convert_alpha()
409
- self.surface.set_alpha(old_alpha)
410
- return True
411
-
412
- def paint(self) -> None:
413
- self.surface.fill((0, 0, 0, 0))
414
- return self
415
285
 
416
- def visit(self, func: Callable[["Widget"],Any], top_down: bool = True, *args, **kwargs) -> None:
417
- if top_down:
418
- func(self, *args, **kwargs)
286
+ def update(self,dt:float):
419
287
  for child in self.children:
420
- child.visit(func, top_down,*args,**kwargs)
421
- if not top_down:
422
- func(self, *args, **kwargs)
288
+ child.update(dt)
423
289
 
424
- def visit_up(self, func, *args, **kwargs) -> None:
425
- if func(self, *args, **kwargs):
426
- return
427
- if self.parent:
428
- self.parent.visit_up(func, *args, **kwargs)
429
-
430
- """
431
- 1 :
432
- bottom up -> only build children
433
-
434
- """
435
- def update_children_size(self, widget: "Widget"):
436
- # print(widget,widget.uid,"constraints resolve in update size func")
437
-
438
- widget.resolve_constraints()
439
- if widget.dirty_shape:
440
- # print(widget,widget.uid,"build in update size func")
441
- widget.build()
442
- widget.dirty_shape = False
443
- widget.dirty_surface = True
444
-
445
- def find_highest_dirty_constraints_widget(self) -> "Widget":
446
- w = self
447
- tmp = w
448
- while not tmp.is_root:
449
- if tmp.dirty_size_constraints or tmp.dirty_shape:
450
- w = tmp
451
- if not tmp.parent:
452
- break
453
- tmp = tmp.parent
454
- return w
455
-
456
-
457
- def apply_updates(self,pass_type):
458
- # print(f"Apply updates {pass_type} called on {self}")
459
- if pass_type == "pre":
460
- self.apply_pre_updates()
461
- for child in self.children:
462
- child.apply_updates("pre")
463
- elif pass_type == "post":
464
- for child in self.children:
465
- child.apply_updates("post")
466
- self.apply_post_updates()
467
-
468
- def apply_pre_updates(self):
469
- """
470
- TOP TO BOTTOM
471
- Resolves size-related constraints before propagating updates to children.
472
- """
473
- if self.dirty_size_constraints:
474
- self.resolve_constraints(size_only=True)
475
- self.dirty_size_constraints = False
476
- self.dirty_position_constraints = True
290
+ def draw(self, camera: bf.Camera) -> int:
291
+ self.children.sort(key=lambda e: (e.z_depth,e.render_order))
292
+ return super().draw(camera) + sum([child.draw(camera) for child in self.children])
477
293
 
478
- def apply_post_updates(self, skip_draw: bool = False):
294
+ def build(self)->None:
479
295
  """
480
- BOTTOM TO TOP
481
- Resolves position-related constraints after propagating updates from children.
296
+ This function is called each time the widget's surface has to be updated
297
+ It usually has to be overriden if inherited to suit the needs of the new class
482
298
  """
483
-
484
- if self.dirty_shape:
485
-
486
- if self.build():
487
- self.dirty_size_constraints = True
488
- self.dirty_position_constraints = True
489
- if self.parent :
490
- # trigger layout or constraint updates in parent
491
- from .container import Container
492
- if self.parent and isinstance(self.parent, Container):
493
- self.parent.dirty_layout = True
494
- self.parent.dirty_shape = True
495
- self.dirty_shape = False
496
- self.dirty_surface = True
497
-
498
- if self.dirty_position_constraints:
499
- self.resolve_constraints(position_only=True)
500
-
501
- if self.dirty_surface and not skip_draw:
502
- self.paint()
503
- self.dirty_surface = False
504
-
505
-
506
- def draw(self, camera: bf.Camera) -> None:
507
- # Draw widget and handle clipping if necessary
508
- super().draw(camera)
509
-
510
- if self.clip_children:
511
- new_clip = camera.world_to_screen(self.get_inner_rect())
512
- old_clip = camera.surface.get_clip()
513
- new_clip = new_clip.clip(old_clip)
514
- camera.surface.set_clip(new_clip)
515
-
516
- # Draw each child widget, sorted by render order
517
- for child in sorted(self.children, key=lambda c: c.render_order):
518
- if (not self.clip_children) or (child.rect.colliderect(self.rect) or not child.rect):
519
- child.draw(camera)
520
- if self.clip_children:
521
- camera.surface.set_clip(old_clip)
299
+ if not self.surface: return
300
+ if self.surface.get_size() != self.get_size_int():
301
+ self.surface = pygame.Surface(self.get_size_int())
302
+ if self.parent : self.parent.children_modified()
303
+
304
+ def children_modified(self)->None:
305
+ self.apply_constraints()
306
+ if self.parent and not self.is_root:
307
+ self.parent.children_modified()