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