batframework 1.0.9a11__py3-none-any.whl → 1.0.9a13__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 (73) hide show
  1. batFramework/__init__.py +3 -11
  2. batFramework/action.py +280 -279
  3. batFramework/actionContainer.py +105 -82
  4. batFramework/animatedSprite.py +80 -58
  5. batFramework/animation.py +91 -77
  6. batFramework/audioManager.py +156 -131
  7. batFramework/baseScene.py +249 -240
  8. batFramework/camera.py +245 -317
  9. batFramework/constants.py +57 -51
  10. batFramework/cutscene.py +239 -253
  11. batFramework/cutsceneManager.py +34 -34
  12. batFramework/drawable.py +107 -77
  13. batFramework/dynamicEntity.py +30 -30
  14. batFramework/easingController.py +58 -58
  15. batFramework/entity.py +130 -130
  16. batFramework/enums.py +171 -135
  17. batFramework/fontManager.py +65 -65
  18. batFramework/gui/__init__.py +28 -25
  19. batFramework/gui/animatedLabel.py +90 -89
  20. batFramework/gui/button.py +17 -17
  21. batFramework/gui/clickableWidget.py +244 -244
  22. batFramework/gui/collapseContainer.py +98 -0
  23. batFramework/gui/constraints/__init__.py +1 -1
  24. batFramework/gui/constraints/constraints.py +1066 -980
  25. batFramework/gui/container.py +220 -206
  26. batFramework/gui/debugger.py +140 -130
  27. batFramework/gui/draggableWidget.py +63 -44
  28. batFramework/gui/image.py +61 -58
  29. batFramework/gui/indicator.py +116 -113
  30. batFramework/gui/interactiveWidget.py +243 -239
  31. batFramework/gui/label.py +147 -344
  32. batFramework/gui/layout.py +442 -429
  33. batFramework/gui/meter.py +155 -96
  34. batFramework/gui/radioButton.py +43 -35
  35. batFramework/gui/root.py +228 -228
  36. batFramework/gui/scrollingContainer.py +282 -0
  37. batFramework/gui/selector.py +232 -250
  38. batFramework/gui/shape.py +286 -276
  39. batFramework/gui/slider.py +353 -397
  40. batFramework/gui/style.py +10 -10
  41. batFramework/gui/styleManager.py +49 -54
  42. batFramework/gui/syncedVar.py +43 -49
  43. batFramework/gui/textInput.py +331 -306
  44. batFramework/gui/textWidget.py +308 -0
  45. batFramework/gui/toggle.py +140 -128
  46. batFramework/gui/tooltip.py +35 -30
  47. batFramework/gui/widget.py +546 -521
  48. batFramework/manager.py +131 -134
  49. batFramework/particle.py +118 -118
  50. batFramework/propertyEaser.py +79 -79
  51. batFramework/renderGroup.py +34 -34
  52. batFramework/resourceManager.py +130 -130
  53. batFramework/scene.py +31 -31
  54. batFramework/sceneLayer.py +134 -138
  55. batFramework/sceneManager.py +200 -197
  56. batFramework/scrollingSprite.py +115 -115
  57. batFramework/sprite.py +46 -51
  58. batFramework/stateMachine.py +49 -54
  59. batFramework/templates/__init__.py +2 -1
  60. batFramework/templates/character.py +15 -0
  61. batFramework/templates/controller.py +158 -97
  62. batFramework/templates/stateMachine.py +39 -0
  63. batFramework/tileset.py +46 -46
  64. batFramework/timeManager.py +213 -213
  65. batFramework/transition.py +162 -162
  66. batFramework/triggerZone.py +22 -22
  67. batFramework/utils.py +306 -306
  68. {batframework-1.0.9a11.dist-info → batframework-1.0.9a13.dist-info}/LICENSE +20 -20
  69. {batframework-1.0.9a11.dist-info → batframework-1.0.9a13.dist-info}/METADATA +24 -17
  70. batframework-1.0.9a13.dist-info/RECORD +72 -0
  71. batframework-1.0.9a11.dist-info/RECORD +0 -67
  72. {batframework-1.0.9a11.dist-info → batframework-1.0.9a13.dist-info}/WHEEL +0 -0
  73. {batframework-1.0.9a11.dist-info → batframework-1.0.9a13.dist-info}/top_level.txt +0 -0
@@ -1,521 +1,546 @@
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 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
-
60
- def set_clip_children(self, value: bool) -> Self:
61
- self.clip_children = value
62
- self.dirty_surface = True
63
- return self
64
-
65
- def __str__(self) -> str:
66
- return "Widget"
67
-
68
- def set_autoresize(self, value: bool) -> Self:
69
- self.autoresize_w = self.autoresize_h = value
70
- self.dirty_shape = True
71
- return self
72
-
73
- def set_autoresize_w(self, value: bool) -> Self:
74
- self.autoresize_w = value
75
- self.dirty_shape = True
76
- return self
77
-
78
- def set_autoresize_h(self, value: bool) -> Self:
79
- self.autoresize_h = value
80
- self.dirty_shape = True
81
- return self
82
-
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
88
-
89
- def expand_rect_with_padding(
90
- self, rect: pygame.Rect | pygame.FRect
91
- ) -> pygame.Rect | pygame.FRect:
92
- 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],
97
- )
98
-
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
125
-
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)
130
-
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
140
-
141
- def set_parent(self, parent: "Widget") -> Self:
142
- if parent == self.parent:
143
- 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
161
-
162
- self.dirty_shape = True
163
- return self
164
-
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
171
-
172
- def get_min_required_size(self) -> tuple[float, float]:
173
- return self.rect.size
174
-
175
- def get_inner_width(self) -> float:
176
- return self.rect.w - self.padding[0] - self.padding[2]
177
-
178
- def get_inner_height(self) -> float:
179
- return self.rect.h - self.padding[1] - self.padding[3]
180
-
181
- def get_inner_left(self) -> float:
182
- return self.rect.left + self.padding[0]
183
-
184
- def get_inner_right(self) -> float:
185
- return self.rect.right - self.padding[2]
186
-
187
- def get_inner_center(self) -> tuple[float, float]:
188
- return self.get_inner_rect().center
189
-
190
- def get_inner_top(self) -> float:
191
- return self.rect.y + self.padding[1]
192
-
193
- def get_inner_bottom(self) -> float:
194
- return self.rect.bottom - self.padding[3]
195
-
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)
202
- for child in self.children:
203
- yield from child.get_debug_outlines()
204
-
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)
210
-
211
- # Sort constraints by priority
212
- self.constraints.sort(key=lambda c: c.priority)
213
-
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
219
-
220
- # Clear ignored constraints
221
- self._constraints_to_ignore = []
222
-
223
- return self
224
-
225
-
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
-
235
- return self
236
-
237
-
238
- def resolve_constraints(self, size_only: bool = False, position_only: bool = False) -> None:
239
- """
240
- Resolve constraints affecting size and/or position independently.
241
-
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
251
-
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
-
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
310
-
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
-
315
-
316
- def has_constraint(self, name: str) -> bool:
317
- return any(c.name == name for c in self.constraints)
318
-
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
325
-
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
354
-
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
-
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
-
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
-
382
- def process_event(self, event: pygame.Event) -> None:
383
- # First propagate to children
384
- for child in self.children:
385
- child.process_event(event)
386
- super().process_event(event)
387
-
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
-
416
- def visit(self, func: Callable[["Widget"],Any], top_down: bool = True, *args, **kwargs) -> None:
417
- if top_down:
418
- func(self, *args, **kwargs)
419
- for child in self.children:
420
- child.visit(func, top_down,*args,**kwargs)
421
- if not top_down:
422
- func(self, *args, **kwargs)
423
-
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
477
-
478
- def apply_post_updates(self, skip_draw: bool = False):
479
- """
480
- BOTTOM TO TOP
481
- Resolves position-related constraints after propagating updates from children.
482
- """
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)
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)