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