batframework 1.0.9a7__py3-none-any.whl → 1.0.9a9__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 +8 -5
- batFramework/baseScene.py +240 -0
- batFramework/camera.py +4 -0
- batFramework/constants.py +6 -2
- 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 +6 -3
- batFramework/gui/animatedLabel.py +10 -2
- batFramework/gui/button.py +4 -31
- batFramework/gui/clickableWidget.py +63 -50
- batFramework/gui/constraints/constraints.py +212 -136
- batFramework/gui/container.py +77 -58
- batFramework/gui/debugger.py +12 -17
- batFramework/gui/draggableWidget.py +21 -17
- batFramework/gui/image.py +3 -10
- batFramework/gui/indicator.py +56 -1
- batFramework/gui/interactiveWidget.py +127 -108
- batFramework/gui/label.py +73 -64
- batFramework/gui/layout.py +286 -445
- batFramework/gui/meter.py +42 -20
- batFramework/gui/radioButton.py +20 -69
- batFramework/gui/root.py +99 -29
- batFramework/gui/selector.py +250 -0
- batFramework/gui/shape.py +13 -5
- batFramework/gui/slider.py +262 -107
- batFramework/gui/syncedVar.py +49 -0
- batFramework/gui/textInput.py +46 -22
- batFramework/gui/toggle.py +70 -52
- batFramework/gui/tooltip.py +30 -0
- batFramework/gui/widget.py +222 -135
- 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 +125 -66
- {batframework-1.0.9a7.dist-info → batframework-1.0.9a9.dist-info}/METADATA +24 -3
- batframework-1.0.9a9.dist-info/RECORD +67 -0
- {batframework-1.0.9a7.dist-info → batframework-1.0.9a9.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.9a7.dist-info/RECORD +0 -63
- /batframework-1.0.9a7.dist-info/LICENCE → /batframework-1.0.9a9.dist-info/LICENSE +0 -0
- {batframework-1.0.9a7.dist-info → batframework-1.0.9a9.dist-info}/top_level.txt +0 -0
batFramework/gui/slider.py
CHANGED
@@ -1,69 +1,94 @@
|
|
1
1
|
import batFramework as bf
|
2
|
-
from .meter import
|
2
|
+
from .meter import BarMeter
|
3
3
|
from .button import Button
|
4
4
|
from .indicator import *
|
5
|
-
from .meter import
|
5
|
+
from .meter import BarMeter
|
6
6
|
from .shape import Shape
|
7
7
|
from .interactiveWidget import InteractiveWidget
|
8
8
|
|
9
|
-
|
10
9
|
class SliderHandle(Indicator, DraggableWidget):
|
11
10
|
def __init__(self):
|
12
11
|
super().__init__()
|
13
12
|
self.set_color(bf.color.CLOUD_SHADE)
|
14
13
|
self.old_key_repeat: tuple = (0, 0)
|
15
14
|
self.parent : bf.ClickableWidget = self.parent
|
15
|
+
self.set_click_mask(1)
|
16
16
|
def __str__(self) -> str:
|
17
17
|
return "SliderHandle"
|
18
18
|
|
19
|
-
def on_click_down(self, button: int) ->
|
20
|
-
if not self.parent.is_enabled
|
21
|
-
return
|
22
|
-
super().on_click_down(button)
|
23
|
-
if
|
19
|
+
def on_click_down(self, button: int) -> bool:
|
20
|
+
if not self.parent.is_enabled:
|
21
|
+
return True
|
22
|
+
res = super().on_click_down(button)
|
23
|
+
if res :
|
24
24
|
self.parent.get_focus()
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
self.
|
29
|
-
|
25
|
+
return res
|
26
|
+
|
27
|
+
def on_exit(self):
|
28
|
+
before = self.is_clicked_down
|
29
|
+
super().on_exit()
|
30
|
+
self.is_clicked_down = before
|
31
|
+
|
30
32
|
def do_on_drag(
|
31
33
|
self, drag_start: tuple[float, float], drag_end: tuple[float, float]
|
32
34
|
) -> None:
|
33
|
-
if not self.parent.is_enabled
|
35
|
+
if not self.parent.is_enabled:
|
34
36
|
return
|
35
37
|
super().do_on_drag(drag_start, drag_end)
|
36
|
-
m:
|
37
|
-
r = m.
|
38
|
-
|
38
|
+
m: BarMeter = self.parent.meter
|
39
|
+
r = m.get_inner_rect()
|
40
|
+
|
41
|
+
position = self.rect.centerx if self.parent.axis == bf.axis.HORIZONTAL else self.rect.centery
|
39
42
|
self.rect.clamp_ip(r)
|
40
43
|
# Adjust handle position to value
|
41
44
|
new_value = self.parent.position_to_value(position)
|
42
45
|
self.parent.set_value(new_value)
|
43
|
-
self.
|
46
|
+
if self.parent.axis == bf.axis.HORIZONTAL:
|
47
|
+
self.rect.centerx = self.parent.value_to_position(new_value)
|
48
|
+
else:
|
49
|
+
self.rect.centery = self.parent.value_to_position(new_value)
|
44
50
|
|
45
51
|
def top_at(self, x, y):
|
46
52
|
return Widget.top_at(self, x, y)
|
47
53
|
|
48
|
-
|
49
|
-
class SliderMeter(Meter, InteractiveWidget):
|
54
|
+
class SliderMeter(BarMeter, InteractiveWidget):
|
50
55
|
def __str__(self) -> str:
|
51
56
|
return "SliderMeter"
|
52
57
|
|
53
|
-
def
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
58
|
+
def __init__(self, min_value = 0, max_value = 1, step = 0.1):
|
59
|
+
super().__init__(min_value, max_value, step)
|
60
|
+
self.set_padding(0)
|
61
|
+
|
62
|
+
def get_min_required_size(self):
|
63
|
+
size = list(super().get_min_required_size())
|
64
|
+
if self.parent.axis == bf.axis.HORIZONTAL:
|
65
|
+
size[0] = size[1]*3
|
66
|
+
else:
|
67
|
+
size[1] = size[0]*3
|
68
|
+
return self.resolve_size(size)
|
69
|
+
|
70
|
+
def on_click_down(self, button: int) -> bool:
|
71
|
+
if not self.parent.is_enabled:
|
72
|
+
return False
|
73
|
+
if button != 1:
|
74
|
+
return False
|
75
|
+
|
76
|
+
self.parent.get_focus()
|
77
|
+
pos = self.parent_layer.camera.screen_to_world(pygame.mouse.get_pos())
|
78
|
+
if self.parent.axis == bf.axis.HORIZONTAL:
|
79
|
+
pos = pos[0]
|
80
|
+
else:
|
81
|
+
pos = pos[1]
|
82
|
+
new_value = self.parent.position_to_value(pos)
|
83
|
+
self.parent.set_value(new_value)
|
62
84
|
self.do_on_click_down(button)
|
85
|
+
return True
|
63
86
|
|
64
87
|
class Slider(Button):
|
88
|
+
|
65
89
|
def __init__(self, text: str, default_value: float = 1.0) -> None:
|
66
90
|
super().__init__(text, None)
|
91
|
+
self.axis : bf.axis = bf.axis.HORIZONTAL
|
67
92
|
self.gap: float | int = 0
|
68
93
|
self.spacing: bf.spacing = bf.spacing.MANUAL
|
69
94
|
self.modified_callback : Callable[[float],Any] = None
|
@@ -73,6 +98,19 @@ class Slider(Button):
|
|
73
98
|
self.meter.set_debug_color(bf.color.RED)
|
74
99
|
self.set_value(default_value, True)
|
75
100
|
|
101
|
+
def set_tooltip_text(self, text):
|
102
|
+
return super().set_tooltip_text(text)
|
103
|
+
|
104
|
+
def set_fill_color(self,color)->Self:
|
105
|
+
self.meter.content.set_color(color)
|
106
|
+
return self
|
107
|
+
|
108
|
+
def set_axis(self,axis:bf.axis)->Self:
|
109
|
+
self.axis = axis
|
110
|
+
self.meter.set_axis(axis)
|
111
|
+
self.dirty_shape = True
|
112
|
+
return self
|
113
|
+
|
76
114
|
def set_visible(self, value: bool) -> Self:
|
77
115
|
self.handle.set_visible(value)
|
78
116
|
self.meter.set_visible(value)
|
@@ -122,21 +160,39 @@ class Slider(Button):
|
|
122
160
|
self.dirty_shape = True
|
123
161
|
if self.modified_callback and (not no_callback):
|
124
162
|
self.modified_callback(self.meter.value)
|
163
|
+
self.handle.set_tooltip_text(str(self.get_value()))
|
164
|
+
self.meter.set_tooltip_text(str(self.get_value()))
|
165
|
+
|
125
166
|
return self
|
126
167
|
|
127
168
|
def get_value(self) -> float:
|
128
169
|
return self.meter.get_value()
|
129
170
|
|
130
|
-
def
|
131
|
-
if
|
132
|
-
return
|
133
|
-
if
|
134
|
-
|
135
|
-
|
136
|
-
|
171
|
+
def on_key_down(self, key):
|
172
|
+
if super().on_key_down(key):
|
173
|
+
return True
|
174
|
+
if not self.is_enabled:
|
175
|
+
return False
|
176
|
+
if self.axis == bf.axis.HORIZONTAL:
|
177
|
+
if key == pygame.K_RIGHT:
|
178
|
+
self.set_value(self.meter.get_value() + self.meter.step)
|
179
|
+
elif key == pygame.K_LEFT:
|
180
|
+
self.set_value(self.meter.get_value() - self.meter.step)
|
181
|
+
else:
|
182
|
+
return False
|
183
|
+
return True
|
184
|
+
else:
|
185
|
+
if key == pygame.K_UP:
|
186
|
+
self.set_value(self.meter.get_value() + self.meter.step)
|
187
|
+
elif key == pygame.K_DOWN:
|
188
|
+
self.set_value(self.meter.get_value() - self.meter.step)
|
189
|
+
else:
|
190
|
+
return False
|
191
|
+
|
192
|
+
return True
|
137
193
|
|
138
194
|
def do_on_click_down(self, button) -> None:
|
139
|
-
if not self.is_enabled
|
195
|
+
if not self.is_enabled:
|
140
196
|
return
|
141
197
|
if button == 1:
|
142
198
|
self.get_focus()
|
@@ -145,98 +201,197 @@ class Slider(Button):
|
|
145
201
|
"""
|
146
202
|
Converts a value to a position on the meter, considering the step size.
|
147
203
|
"""
|
148
|
-
rect = self.meter.
|
204
|
+
rect = self.meter.get_inner_rect()
|
149
205
|
value_range = self.meter.get_range()
|
150
206
|
value = round(value / self.meter.step) * self.meter.step
|
151
207
|
position_ratio = (value - self.meter.min_value) / value_range
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
208
|
+
if self.axis == bf.axis.HORIZONTAL:
|
209
|
+
return (
|
210
|
+
rect.left
|
211
|
+
+ (self.handle.rect.w / 2)
|
212
|
+
+ position_ratio * (rect.width - self.handle.rect.w)
|
213
|
+
)
|
214
|
+
else:
|
215
|
+
return (
|
216
|
+
rect.bottom
|
217
|
+
- (self.handle.rect.h / 2)
|
218
|
+
- position_ratio * (rect.height - self.handle.rect.h)
|
219
|
+
)
|
220
|
+
|
158
221
|
def position_to_value(self, position: float) -> float:
|
159
222
|
"""
|
160
223
|
Converts a position on the meter to a value, considering the step size.
|
161
224
|
"""
|
162
|
-
|
163
|
-
|
164
|
-
|
225
|
+
rect = self.meter.get_inner_rect()
|
226
|
+
if self.axis == bf.axis.HORIZONTAL:
|
227
|
+
if self.rect.w == self.handle.rect.w:
|
228
|
+
position_ratio = 0
|
229
|
+
else:
|
230
|
+
handle_half = self.handle.rect.w // 2
|
231
|
+
position = max(rect.left + handle_half, min(position, rect.right - handle_half))
|
232
|
+
position_ratio = (position - rect.left - handle_half) / (
|
233
|
+
rect.width - self.handle.rect.w
|
234
|
+
)
|
235
|
+
else:
|
236
|
+
if self.rect.h == self.handle.rect.h:
|
237
|
+
position_ratio = 0
|
238
|
+
else:
|
239
|
+
handle_half = self.handle.rect.h // 2
|
240
|
+
position = max(rect.top + handle_half, min(position, rect.bottom - handle_half))
|
241
|
+
# Flip ratio vertically: bottom is min, top is max
|
242
|
+
position_ratio = (rect.bottom - position - handle_half) / (
|
243
|
+
rect.height - self.handle.rect.h
|
244
|
+
)
|
165
245
|
|
166
|
-
position_ratio = (position - rect.left - handle_half) / (
|
167
|
-
rect.width - self.handle.rect.w
|
168
|
-
)
|
169
246
|
value_range = self.meter.get_range()
|
170
247
|
value = self.meter.min_value + position_ratio * value_range
|
171
248
|
return round(value / self.meter.step) * self.meter.step
|
172
249
|
|
173
250
|
def get_min_required_size(self) -> tuple[float, float]:
|
174
|
-
|
175
|
-
|
176
|
-
self.text_rect.size = self._get_text_rect_required_size()
|
177
|
-
w, h = self.text_rect.size
|
178
|
-
h+=self.unpressed_relief
|
179
|
-
return self.inflate_rect_by_padding((0, 0, w + gap + self.meter.get_min_required_size()[1], h)).size
|
180
|
-
|
181
|
-
def _build_layout(self) -> None:
|
251
|
+
"""
|
252
|
+
Calculates the minimum required size for the slider, considering the text, meter, and axis.
|
182
253
|
|
254
|
+
Returns:
|
255
|
+
tuple[float, float]: The width and height of the minimum required size.
|
256
|
+
"""
|
183
257
|
gap = self.gap if self.text else 0
|
184
|
-
|
185
|
-
|
186
|
-
#right part size
|
187
|
-
meter_width = self.text_rect.h * 10
|
188
|
-
if not self.autoresize_w:
|
189
|
-
meter_width = self.get_padded_width() - self.text_rect.w - gap
|
190
|
-
right_part_height = min(self.text_rect.h, self.font_object.point_size)
|
191
|
-
self.meter.set_size_if_autoresize((meter_width,right_part_height))
|
192
|
-
self.handle.set_size_if_autoresize((None,right_part_height))
|
258
|
+
text_width, text_height = self._get_text_rect_required_size() if self.text else (0, 0)
|
259
|
+
meter_width, meter_height = self.meter.resolve_size(self.meter.get_min_required_size())
|
193
260
|
|
261
|
+
if self.axis == bf.axis.HORIZONTAL:
|
262
|
+
width = text_width + gap + meter_width
|
263
|
+
height = max(text_height, meter_height)
|
264
|
+
else:
|
265
|
+
width = max(text_width, meter_width)
|
266
|
+
height = text_height + gap + meter_height
|
194
267
|
|
195
|
-
|
196
|
-
|
197
|
-
0, 0, self.text_rect.w + gap + meter_width, self.text_rect.h
|
198
|
-
)
|
268
|
+
height += self.unpressed_relief
|
269
|
+
return self.expand_rect_with_padding((0, 0, width, height)).size
|
199
270
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
self.set_size(target_rect.size)
|
209
|
-
self.build()
|
210
|
-
return
|
211
|
-
|
212
|
-
# ------------------------------------ size is ok
|
213
|
-
|
271
|
+
def _build_composed_layout(self) -> None:
|
272
|
+
"""
|
273
|
+
Builds the composed layout for the slider, including the meter and handle.
|
274
|
+
This method adjusts the sizes and positions of the meter and handle based on the slider's axis,
|
275
|
+
autoresize conditions, and spacing settings. It ensures the slider's components are properly aligned
|
276
|
+
and sized within the slider's padded rectangle.
|
277
|
+
"""
|
278
|
+
self.text_rect.size = self._get_text_rect_required_size()
|
214
279
|
|
215
|
-
|
216
|
-
|
217
|
-
padded_relative = padded_rect.move(-self.rect.x, -self.rect.y)
|
280
|
+
size_changed = False
|
281
|
+
gap = self.gap if self.text else 0
|
218
282
|
|
219
|
-
self.
|
220
|
-
self.text_rect.midleft = joined_rect.midleft
|
283
|
+
full_rect = self.text_rect.copy()
|
221
284
|
|
222
|
-
if self.text:
|
223
|
-
match self.spacing:
|
224
|
-
case bf.spacing.MAX:
|
225
|
-
gap = padded_relative.right - self.text_rect.right - self.meter.rect.w
|
226
|
-
case bf.spacing.MIN:
|
227
|
-
gap = 0
|
228
285
|
|
229
|
-
#
|
286
|
+
# Resolve the meter's size based on the axis and autoresize conditions
|
287
|
+
if self.axis == bf.axis.HORIZONTAL:
|
288
|
+
meter_width,meter_height = self.meter.get_min_required_size()
|
289
|
+
if not self.autoresize_w:
|
290
|
+
meter_width = self.get_inner_width() - gap - self.text_rect.w
|
291
|
+
meter_width,meter_height = self.meter.resolve_size((meter_width,meter_height))
|
230
292
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
).topright
|
235
|
-
self.meter.rect.topleft = pos
|
236
|
-
# place handle
|
293
|
+
full_rect.w = max(self.get_inner_width(), meter_width + (gap + self.text_rect.w if self.text else 0))
|
294
|
+
self.meter.set_size((meter_width, meter_height))
|
295
|
+
full_rect.h = max(meter_height, self.text_rect.h if self.text else meter_height)
|
237
296
|
|
238
|
-
|
239
|
-
|
240
|
-
|
297
|
+
else: # VERTICAL
|
298
|
+
meter_width, meter_height = self.meter.get_min_required_size()
|
299
|
+
if not self.autoresize_h:
|
300
|
+
meter_height = self.get_inner_height() - gap - self.text_rect.h
|
301
|
+
meter_width, meter_height = self.meter.resolve_size((meter_width, meter_height))
|
302
|
+
|
303
|
+
full_rect.h = meter_height + (gap + self.text_rect.h if self.text else 0)
|
304
|
+
self.meter.set_size((meter_width, meter_height))
|
305
|
+
full_rect.w = max(meter_width, self.text_rect.w if self.text else meter_width)
|
306
|
+
|
307
|
+
|
308
|
+
# Inflate the rect by padding and resolve the target size
|
309
|
+
full_rect.h += self.unpressed_relief
|
310
|
+
inflated = self.expand_rect_with_padding((0, 0, *full_rect.size)).size
|
311
|
+
|
312
|
+
target_size = self.resolve_size(inflated)
|
313
|
+
|
314
|
+
|
315
|
+
# Update the slider's size if it doesn't match the target size
|
316
|
+
if self.rect.size != target_size:
|
317
|
+
self.set_size(target_size)
|
318
|
+
size_changed = True
|
319
|
+
|
320
|
+
# Adjust the handle size based on the meter's size
|
321
|
+
if self.axis == bf.axis.HORIZONTAL:
|
322
|
+
handle_size = self.meter.get_inner_height()
|
323
|
+
self.handle.set_size(self.handle.resolve_size((handle_size, handle_size)))
|
324
|
+
else:
|
325
|
+
handle_size = self.meter.get_inner_width()
|
326
|
+
self.handle.set_size(self.handle.resolve_size((handle_size, handle_size)))
|
327
|
+
|
328
|
+
self._align_composed()
|
329
|
+
return size_changed
|
330
|
+
|
331
|
+
def _align_composed(self):
|
332
|
+
|
333
|
+
if not self.text:
|
334
|
+
self.text_rect.size = (0,0)
|
335
|
+
full_rect = self.get_local_inner_rect()
|
336
|
+
left_rect = self.text_rect
|
337
|
+
right_rect = self.meter.rect.copy()
|
338
|
+
|
339
|
+
|
340
|
+
|
341
|
+
if self.axis == bf.axis.HORIZONTAL:
|
342
|
+
gap = {
|
343
|
+
bf.spacing.MIN: 0,
|
344
|
+
bf.spacing.HALF: (full_rect.width - left_rect.width - right_rect.width) // 2,
|
345
|
+
bf.spacing.MAX: full_rect.width - left_rect.width - right_rect.width,
|
346
|
+
bf.spacing.MANUAL: self.gap
|
347
|
+
}.get(self.spacing, 0)
|
348
|
+
|
349
|
+
gap = max(0, gap)
|
350
|
+
combined_width = left_rect.width + right_rect.width + gap
|
351
|
+
|
352
|
+
group_x = {
|
353
|
+
bf.alignment.LEFT: full_rect.left,
|
354
|
+
bf.alignment.MIDLEFT: full_rect.left,
|
355
|
+
bf.alignment.RIGHT: full_rect.right - combined_width,
|
356
|
+
bf.alignment.MIDRIGHT: full_rect.right - combined_width,
|
357
|
+
bf.alignment.CENTER: full_rect.centerx - combined_width // 2
|
358
|
+
}.get(self.alignment, full_rect.left)
|
359
|
+
|
360
|
+
left_rect.x, right_rect.x = group_x, group_x + left_rect.width + gap
|
361
|
+
left_rect.centery = right_rect.centery = full_rect.centery
|
362
|
+
|
363
|
+
else: # VERTICAL
|
364
|
+
gap = {
|
365
|
+
bf.spacing.MIN: 0,
|
366
|
+
bf.spacing.HALF: (full_rect.height - left_rect.height - right_rect.height) // 2,
|
367
|
+
bf.spacing.MAX: full_rect.height - left_rect.height - right_rect.height,
|
368
|
+
bf.spacing.MANUAL: self.gap
|
369
|
+
}.get(self.spacing, 0)
|
370
|
+
|
371
|
+
gap = max(0, gap)
|
372
|
+
combined_height = left_rect.height + right_rect.height + gap
|
373
|
+
|
374
|
+
group_y = {
|
375
|
+
bf.alignment.TOP: full_rect.top,
|
376
|
+
bf.alignment.MIDTOP: full_rect.top,
|
377
|
+
bf.alignment.BOTTOM: full_rect.bottom - combined_height,
|
378
|
+
bf.alignment.MIDBOTTOM: full_rect.bottom - combined_height,
|
379
|
+
bf.alignment.CENTER: full_rect.centery - combined_height // 2
|
380
|
+
}.get(self.alignment, full_rect.top)
|
381
|
+
|
382
|
+
left_rect.y, right_rect.y = group_y, group_y + left_rect.height + gap
|
383
|
+
left_rect.centerx = right_rect.centerx = full_rect.centerx
|
384
|
+
|
385
|
+
# Push text to local, push shape to world
|
386
|
+
self.text_rect = left_rect
|
387
|
+
right_rect.move_ip(*self.rect.topleft)
|
388
|
+
self.meter.set_position(*right_rect.topleft)
|
389
|
+
|
390
|
+
# Position the handle based on the current value
|
391
|
+
if self.axis == bf.axis.HORIZONTAL:
|
392
|
+
self.handle.set_center(self.value_to_position(self.meter.value), self.meter.rect.centery)
|
393
|
+
else:
|
394
|
+
self.handle.set_center(self.meter.rect.centerx, self.value_to_position(self.meter.value))
|
241
395
|
|
242
|
-
|
396
|
+
def _build_layout(self) -> None:
|
397
|
+
return self._build_composed_layout()
|
@@ -0,0 +1,49 @@
|
|
1
|
+
from .widget import Widget
|
2
|
+
from typing import Callable, Any, Self
|
3
|
+
|
4
|
+
class SyncedVar:
|
5
|
+
def __init__(self, value=None):
|
6
|
+
self._value: Any = value
|
7
|
+
self.modify_callback: Callable[[Any], Any] | None = None
|
8
|
+
self._bound_widgets: set[tuple[Widget, Callable[[Any], Any]]] = set()
|
9
|
+
|
10
|
+
def set_modify_callback(self, callback : Callable[[Any], Any]) -> Self:
|
11
|
+
self.modify_callback = callback
|
12
|
+
return self
|
13
|
+
|
14
|
+
def bind_widget(self, widget: Widget, update_callback: Callable[[Any], Any]) -> Self:
|
15
|
+
"""
|
16
|
+
Binds a widget to the SyncedVar. The widget must provide an update_callback
|
17
|
+
function that will be called whenever the value changes.
|
18
|
+
"""
|
19
|
+
self._bound_widgets.add((widget, update_callback))
|
20
|
+
self._update_widgets()
|
21
|
+
return self
|
22
|
+
|
23
|
+
def unbind_widget(self, widget: Widget)->Self:
|
24
|
+
"""
|
25
|
+
Unbinds a widget from the SyncedVar.
|
26
|
+
"""
|
27
|
+
self._bound_widgets = {
|
28
|
+
(w, cb) for w, cb in self._bound_widgets if w != widget
|
29
|
+
}
|
30
|
+
return self
|
31
|
+
|
32
|
+
@property
|
33
|
+
def value(self):
|
34
|
+
return self._value
|
35
|
+
|
36
|
+
@value.setter
|
37
|
+
def value(self, new_value):
|
38
|
+
if self._value != new_value:
|
39
|
+
self._value = new_value
|
40
|
+
self._update_widgets()
|
41
|
+
if self.modify_callback is not None:
|
42
|
+
self.modify_callback(new_value)
|
43
|
+
|
44
|
+
def _update_widgets(self):
|
45
|
+
"""
|
46
|
+
Calls the update callback for all bound widgets.
|
47
|
+
"""
|
48
|
+
for _, update_callback in self._bound_widgets:
|
49
|
+
update_callback(self._value)
|
batFramework/gui/textInput.py
CHANGED
@@ -58,7 +58,7 @@ class TextInput(Label, InteractiveWidget):
|
|
58
58
|
def __init__(self) -> None:
|
59
59
|
self.cursor_position = (0, 0)
|
60
60
|
self.old_key_repeat = (0, 0)
|
61
|
-
self.cursor_timer = bf.Timer(0.2, self._cursor_toggle, loop
|
61
|
+
self.cursor_timer = bf.Timer(0.2, self._cursor_toggle, loop=-1).start()
|
62
62
|
self.cursor_timer.pause()
|
63
63
|
self.show_cursor = False
|
64
64
|
self.on_modify: Callable[[str], str] = None
|
@@ -72,7 +72,7 @@ class TextInput(Label, InteractiveWidget):
|
|
72
72
|
return self
|
73
73
|
|
74
74
|
def __str__(self) -> str:
|
75
|
-
return f"TextInput
|
75
|
+
return f"TextInput"
|
76
76
|
|
77
77
|
def _cursor_toggle(self, value: bool | None = None):
|
78
78
|
if value is None:
|
@@ -82,8 +82,9 @@ class TextInput(Label, InteractiveWidget):
|
|
82
82
|
|
83
83
|
def do_on_click_down(self, button):
|
84
84
|
if button != 1:
|
85
|
-
return
|
85
|
+
return False
|
86
86
|
self.get_focus()
|
87
|
+
return True
|
87
88
|
|
88
89
|
def do_on_enter(self):
|
89
90
|
pygame.mouse.set_cursor(pygame.SYSTEM_CURSOR_IBEAM)
|
@@ -111,11 +112,18 @@ class TextInput(Label, InteractiveWidget):
|
|
111
112
|
return lines[line]
|
112
113
|
|
113
114
|
def get_debug_outlines(self):
|
114
|
-
if self.visible:
|
115
|
-
offset = self._get_outline_offset() if self.show_text_outline else (0,0)
|
116
|
-
yield (self.text_rect.move(self.rect.x - offset[0] - self.scroll.x,self.rect.y - offset[1] - self.scroll.y), "purple")
|
117
115
|
yield from super().get_debug_outlines()
|
118
|
-
|
116
|
+
if self.visible:
|
117
|
+
# offset = self._get_outline_offset() if self.show_text_outline else (0,0)
|
118
|
+
# yield (self.text_rect.move(self.rect.x - offset[0] - self.scroll.x,self.rect.y - offset[1] - self.scroll.y), "purple")
|
119
|
+
yield (self.get_cursor_rect().move(-self.scroll+self.rect.topleft),"green")
|
120
|
+
|
121
|
+
|
122
|
+
def get_min_required_size(self) -> tuple[float, float]:
|
123
|
+
size = self._get_text_rect_required_size()
|
124
|
+
return self.expand_rect_with_padding(
|
125
|
+
(0, 0,size[0]+self.get_cursor_rect().w,size[1])
|
126
|
+
).size
|
119
127
|
|
120
128
|
|
121
129
|
def set_cursor_position(self, position: tuple[int, int]) -> Self:
|
@@ -127,9 +135,9 @@ class TextInput(Label, InteractiveWidget):
|
|
127
135
|
x = max(0, min(x, line_length))
|
128
136
|
self.show_cursor = True
|
129
137
|
self.cursor_position = (x,y)
|
130
|
-
self.
|
138
|
+
self.apply_post_updates(skip_draw=True)
|
131
139
|
offset = self._get_outline_offset() if self.show_text_outline else (0,0)
|
132
|
-
padded = self.
|
140
|
+
padded = self.get_inner_rect().move(-self.rect.x + offset[0], -self.rect.y + offset[1])
|
133
141
|
self.align_text(self.text_rect,padded,self.alignment)
|
134
142
|
return self
|
135
143
|
|
@@ -142,11 +150,13 @@ class TextInput(Label, InteractiveWidget):
|
|
142
150
|
|
143
151
|
height = self.font_object.get_linesize()
|
144
152
|
|
145
|
-
cursor_y = self.
|
153
|
+
cursor_y = self.get_inner_rect().__getattribute__(self.alignment.value)[1] - self.rect.top
|
146
154
|
cursor_y += line_y * height
|
147
155
|
cursor_x = self.text_rect.x
|
148
156
|
cursor_x += self.font_object.size(lines[line_y][:line_x])[0] if line_x > 0 else 0
|
149
|
-
cursor_rect = pygame.Rect(cursor_x, cursor_y, 1, height)
|
157
|
+
cursor_rect = pygame.Rect(cursor_x, cursor_y, 1, height-1)
|
158
|
+
offset = self._get_outline_offset()
|
159
|
+
cursor_rect.move_ip(offset[0] if self.cursor_position[0] >0 else 0,offset[1] if self.cursor_position[1] > 0 else 0)
|
150
160
|
return cursor_rect
|
151
161
|
|
152
162
|
def cursor_to_absolute(self, position: tuple[int, int]) -> int:
|
@@ -183,7 +193,6 @@ class TextInput(Label, InteractiveWidget):
|
|
183
193
|
# Insert text at the current cursor position
|
184
194
|
self.set_text(f"{text[:current_pos]}{event.text}{text[current_pos:]}")
|
185
195
|
self.set_cursor_position(self.absolute_to_cursor(current_pos + len(event.text)))
|
186
|
-
|
187
196
|
elif event.type == pygame.KEYDOWN:
|
188
197
|
match event.key:
|
189
198
|
case pygame.K_ESCAPE:
|
@@ -191,34 +200,48 @@ class TextInput(Label, InteractiveWidget):
|
|
191
200
|
|
192
201
|
case pygame.K_BACKSPACE if current_pos > 0:
|
193
202
|
# Remove the character before the cursor
|
194
|
-
|
195
|
-
|
203
|
+
delta = current_pos-1
|
204
|
+
if pressed[pygame.K_LCTRL] or pressed[pygame.K_RCTRL]:
|
205
|
+
delta = find_prev_word(self.text,current_pos-1)
|
206
|
+
if delta <0: delta = 0
|
207
|
+
|
208
|
+
self.set_text(f"{text[:delta]}{text[current_pos:]}")
|
209
|
+
self.set_cursor_position(self.absolute_to_cursor(delta))
|
210
|
+
self._cursor_toggle(True)
|
196
211
|
|
197
212
|
case pygame.K_DELETE if current_pos < len(text):
|
198
213
|
# Remove the character at the cursor
|
199
214
|
self.set_text(f"{text[:current_pos]}{text[current_pos + 1:]}")
|
215
|
+
self._cursor_toggle(True)
|
200
216
|
|
201
217
|
case pygame.K_RIGHT:
|
202
218
|
if current_pos < len(text):
|
203
219
|
self.handle_cursor_movement(pressed, current_pos, direction="right")
|
204
|
-
|
220
|
+
self._cursor_toggle(True)
|
221
|
+
|
205
222
|
case pygame.K_LEFT:
|
206
223
|
if current_pos > 0:
|
207
224
|
self.handle_cursor_movement(pressed, current_pos, direction="left")
|
225
|
+
self._cursor_toggle(True)
|
208
226
|
|
209
227
|
case pygame.K_UP:
|
210
228
|
# Move cursor up one line
|
211
229
|
self.set_cursor_position((self.cursor_position[0], self.cursor_position[1] - 1))
|
230
|
+
self._cursor_toggle(True)
|
212
231
|
|
213
232
|
case pygame.K_DOWN:
|
214
233
|
# Move cursor down one line
|
215
234
|
self.set_cursor_position((self.cursor_position[0], self.cursor_position[1] + 1))
|
235
|
+
self._cursor_toggle(True)
|
216
236
|
|
217
237
|
case pygame.K_RETURN:
|
218
238
|
# Insert a newline at the current cursor position
|
219
239
|
self.set_text(f"{text[:current_pos]}\n{text[current_pos:]}")
|
220
240
|
self.set_cursor_position(self.absolute_to_cursor(current_pos + 1))
|
241
|
+
self._cursor_toggle(True)
|
221
242
|
case _ :
|
243
|
+
if event.unicode:
|
244
|
+
event.consumed = True
|
222
245
|
return
|
223
246
|
|
224
247
|
event.consumed = True
|
@@ -249,7 +272,8 @@ class TextInput(Label, InteractiveWidget):
|
|
249
272
|
cursor_rect = self.get_cursor_rect()
|
250
273
|
cursor_rect.move_ip(-self.scroll)
|
251
274
|
|
252
|
-
|
275
|
+
if self.show_text_outline:
|
276
|
+
pygame.draw.rect(self.surface, self.text_outline_color, cursor_rect.inflate(2,2))
|
253
277
|
pygame.draw.rect(self.surface, self.text_color, cursor_rect)
|
254
278
|
|
255
279
|
def paint(self) -> None:
|
@@ -269,13 +293,13 @@ class TextInput(Label, InteractiveWidget):
|
|
269
293
|
text_rect.__setattr__(alignment.value, pos)
|
270
294
|
|
271
295
|
|
272
|
-
if cursor_rect.right > area.right+self.scroll.x:
|
273
|
-
|
274
|
-
elif cursor_rect.x < self.scroll.x+area.left:
|
275
|
-
|
276
|
-
self.scroll.x = max(self.scroll.x,0)
|
296
|
+
# if cursor_rect.right > area.right+self.scroll.x:
|
297
|
+
# self.scroll.x=cursor_rect.right - area.right
|
298
|
+
# elif cursor_rect.x < self.scroll.x+area.left:
|
299
|
+
# self.scroll.x= cursor_rect.left - area.left
|
300
|
+
# self.scroll.x = max(self.scroll.x,0)
|
277
301
|
|
278
|
-
if cursor_rect.bottom >
|
302
|
+
if cursor_rect.bottom > self.scroll.y + area.bottom:
|
279
303
|
self.scroll.y = cursor_rect.bottom - area.bottom
|
280
304
|
elif cursor_rect.y < self.scroll.y + area.top:
|
281
305
|
self.scroll.y = cursor_rect.top - area.top
|