batframework 1.0.9a11__py3-none-any.whl → 1.0.9a12__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 +2 -0
  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.9a12.dist-info}/LICENSE +20 -20
  69. {batframework-1.0.9a11.dist-info → batframework-1.0.9a12.dist-info}/METADATA +24 -17
  70. batframework-1.0.9a12.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.9a12.dist-info}/WHEEL +0 -0
  73. {batframework-1.0.9a11.dist-info → batframework-1.0.9a12.dist-info}/top_level.txt +0 -0
@@ -1,397 +1,353 @@
1
- import batFramework as bf
2
- from .meter import BarMeter
3
- from .button import Button
4
- from .indicator import *
5
- from .meter import BarMeter
6
- from .shape import Shape
7
- from .interactiveWidget import InteractiveWidget
8
-
9
- class SliderHandle(Indicator, DraggableWidget):
10
- def __init__(self):
11
- super().__init__()
12
- self.set_color(bf.color.CLOUD_SHADE)
13
- self.old_key_repeat: tuple = (0, 0)
14
- self.parent : bf.ClickableWidget = self.parent
15
- self.set_click_mask(1)
16
- def __str__(self) -> str:
17
- return "SliderHandle"
18
-
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
- self.parent.get_focus()
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
-
32
- def do_on_drag(
33
- self, drag_start: tuple[float, float], drag_end: tuple[float, float]
34
- ) -> None:
35
- if not self.parent.is_enabled:
36
- return
37
- super().do_on_drag(drag_start, drag_end)
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
42
- self.rect.clamp_ip(r)
43
- # Adjust handle position to value
44
- new_value = self.parent.position_to_value(position)
45
- self.parent.set_value(new_value)
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)
50
-
51
- def top_at(self, x, y):
52
- return Widget.top_at(self, x, y)
53
-
54
- class SliderMeter(BarMeter, InteractiveWidget):
55
- def __str__(self) -> str:
56
- return "SliderMeter"
57
-
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)
84
- self.do_on_click_down(button)
85
- return True
86
-
87
- class Slider(Button):
88
-
89
- def __init__(self, text: str, default_value: float = 1.0) -> None:
90
- super().__init__(text, None)
91
- self.axis : bf.axis = bf.axis.HORIZONTAL
92
- self.gap: float | int = 0
93
- self.spacing: bf.spacing = bf.spacing.MANUAL
94
- self.modified_callback : Callable[[float],Any] = None
95
- self.meter: SliderMeter = SliderMeter()
96
- self.handle = SliderHandle()
97
- self.add(self.meter, self.handle)
98
- self.meter.set_debug_color(bf.color.RED)
99
- self.set_value(default_value, True)
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
-
114
- def set_visible(self, value: bool) -> Self:
115
- self.handle.set_visible(value)
116
- self.meter.set_visible(value)
117
- return super().set_visible(value)
118
-
119
- def __str__(self) -> str:
120
- return "Slider"
121
-
122
- def set_gap(self, value: int | float) -> Self:
123
- value = max(0, value)
124
- self.gap = value
125
- return self
126
-
127
- def do_on_get_focus(self) -> None:
128
- super().do_on_get_focus()
129
- self.old_key_repeat = pygame.key.get_repeat()
130
- pygame.key.set_repeat(200, 50)
131
-
132
- def do_on_lose_focus(self) -> None:
133
- super().do_on_lose_focus()
134
- pygame.key.set_repeat(*self.old_key_repeat)
135
-
136
- def set_spacing(self, spacing: bf.spacing) -> Self:
137
- if spacing == self.spacing:
138
- return self
139
- self.spacing = spacing
140
- self.dirty_shape = True
141
- return self
142
-
143
- def set_modify_callback(self, callback : Callable[[float],Any]) -> Self:
144
- self.modified_callback = callback
145
- return self
146
-
147
- def set_range(self, range_min: float, range_max: float) -> Self:
148
- self.meter.set_range(range_min, range_max)
149
- self.dirty_shape = True
150
- return self
151
-
152
- def set_step(self, step: float) -> Self:
153
- self.meter.set_step(step)
154
- self.dirty_shape = True
155
- return self
156
-
157
- def set_value(self, value, no_callback: bool = False) -> Self:
158
- if self.meter.value != value:
159
- self.meter.set_value(value)
160
- self.dirty_shape = True
161
- if self.modified_callback and (not no_callback):
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
-
166
- return self
167
-
168
- def get_value(self) -> float:
169
- return self.meter.get_value()
170
-
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
193
-
194
- def do_on_click_down(self, button) -> None:
195
- if not self.is_enabled:
196
- return
197
- if button == 1:
198
- self.get_focus()
199
-
200
- def value_to_position(self, value: float) -> float:
201
- """
202
- Converts a value to a position on the meter, considering the step size.
203
- """
204
- rect = self.meter.get_inner_rect()
205
- value_range = self.meter.get_range()
206
- value = round(value / self.meter.step) * self.meter.step
207
- position_ratio = (value - self.meter.min_value) / value_range
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
-
221
- def position_to_value(self, position: float) -> float:
222
- """
223
- Converts a position on the meter to a value, considering the step size.
224
- """
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
- )
245
-
246
- value_range = self.meter.get_range()
247
- value = self.meter.min_value + position_ratio * value_range
248
- return round(value / self.meter.step) * self.meter.step
249
-
250
- def get_min_required_size(self) -> tuple[float, float]:
251
- """
252
- Calculates the minimum required size for the slider, considering the text, meter, and axis.
253
-
254
- Returns:
255
- tuple[float, float]: The width and height of the minimum required size.
256
- """
257
- gap = self.gap if self.text else 0
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())
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
267
-
268
- height += self.unpressed_relief
269
- return self.expand_rect_with_padding((0, 0, width, height)).size
270
-
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()
279
-
280
- size_changed = False
281
- gap = self.gap if self.text else 0
282
-
283
- full_rect = self.text_rect.copy()
284
-
285
-
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))
292
-
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)
296
-
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))
395
-
396
- def _build_layout(self) -> None:
397
- return self._build_composed_layout()
1
+ import batFramework as bf
2
+ import pygame
3
+ from .button import Button
4
+ from .meter import BarMeter
5
+ from .indicator import Indicator, DraggableWidget
6
+ from .shape import Shape
7
+ from .interactiveWidget import InteractiveWidget
8
+ from .syncedVar import SyncedVar
9
+ from decimal import Decimal
10
+ from typing import Callable, Any, Self
11
+
12
+ def round_to_step_precision(value, step):
13
+ decimals = -Decimal(str(step)).as_tuple().exponent
14
+ rounded = round(value, decimals)
15
+ # Preserve int type if step is an int
16
+ if isinstance(step, int) or (isinstance(step, float) and step.is_integer()):
17
+ return int(rounded)
18
+ return rounded
19
+
20
+
21
+ class SliderHandle(Indicator, DraggableWidget):
22
+ def __init__(self,synced_var:SyncedVar):
23
+ super().__init__()
24
+ self.set_color(bf.color.CLOUD_SHADE)
25
+ self.set_click_mask(1)
26
+ self.synced_var = synced_var
27
+ synced_var.bind(self,self._on_synced_var_update)
28
+
29
+ def __str__(self) -> str:
30
+ return "SliderHandle"
31
+
32
+ def top_at(self, x, y):
33
+ return Shape.top_at(self,x,y)
34
+
35
+ def on_click_down(self, button, event=None):
36
+ if button == 1:
37
+ self.parent.get_focus()
38
+ event.consumed = True
39
+ super().on_click_down(button, event)
40
+
41
+ def do_on_drag(self, drag_start, drag_end):
42
+ if not self.parent:
43
+ return
44
+ super().do_on_drag(drag_start, drag_end)
45
+ meter: SliderMeter = self.parent
46
+ new_value = meter.position_to_value(self.rect.center)
47
+ self._on_synced_var_update(new_value) # need to call this manually otherwise the order of update is fucked up and handle goes outside meter
48
+ self.synced_var.value = new_value
49
+
50
+ def _on_synced_var_update(self,value:float):
51
+ meter: SliderMeter = self.parent
52
+ self.set_center(*meter.value_to_position(value))
53
+ self.rect.clamp_ip(meter.get_inner_rect())
54
+
55
+ def set_size(self, size):
56
+ super().set_size(size)
57
+ if self.parent:
58
+ self.parent.dirty_shape = True
59
+
60
+ def on_exit(self):
61
+ before = self.is_clicked_down
62
+ super().on_exit()
63
+ self.is_clicked_down = before
64
+
65
+ def draw_focused(self, camera):
66
+ return
67
+
68
+ class SliderMeter(BarMeter, InteractiveWidget):
69
+ def __init__(self, min_value=0, max_value=1, step=0.1, synced_var: SyncedVar = None):
70
+ super().__init__(min_value, max_value, step, synced_var)
71
+ self.axis = bf.axis.HORIZONTAL
72
+ self.handle = SliderHandle(synced_var=synced_var)
73
+ self.add(self.handle)
74
+ self.set_padding(0)
75
+ self.set_debug_color(bf.color.RED)
76
+
77
+ def get_focus(self) -> bool:
78
+ res = super().get_focus()
79
+ if res:
80
+ return self.parent.get_focus()
81
+ return False
82
+ def __str__(self) -> str:
83
+ return "SliderMeter"
84
+
85
+ def set_tooltip_text(self, text):
86
+ self.handle.set_tooltip_text(text)
87
+ return super().set_tooltip_text(text)
88
+
89
+ def get_min_required_size(self):
90
+ size = [bf.FontManager().DEFAULT_FONT_SIZE] * 2
91
+ if self.axis == bf.axis.HORIZONTAL:
92
+ size[0] = size[1] * 3
93
+ else:
94
+ size[1] = size[0] * 3
95
+ return self.expand_rect_with_padding((0, 0, *size)).size
96
+
97
+ def value_to_position(self, value: float) -> tuple[float, float]:
98
+ rect = self.get_inner_rect()
99
+ value_range = self.get_range()
100
+ if self.snap :
101
+ value = round_to_step_precision(value, self.step)
102
+ ratio = (value - self.min_value) / value_range if value_range else 0
103
+
104
+ if self.direction in [bf.direction.LEFT, bf.direction.DOWN]:
105
+ ratio = 1 - ratio
106
+
107
+ if self.axis == bf.axis.HORIZONTAL:
108
+ x = rect.left + (self.handle.rect.w / 2) + ratio * (rect.width - self.handle.rect.w)
109
+ y = rect.centery
110
+ else:
111
+ x = rect.centerx
112
+ y = rect.bottom - (self.handle.rect.h / 2) - ratio * (rect.height - self.handle.rect.h)
113
+ return (x, y)
114
+
115
+ def position_to_value(self, position: tuple[float, float]) -> float:
116
+ rect = self.get_inner_rect()
117
+ if self.axis == bf.axis.HORIZONTAL:
118
+ pos = position[0]
119
+ handle_half = self.handle.rect.w / 2
120
+ pos = max(rect.left + handle_half, min(pos, rect.right - handle_half))
121
+ ratio = (pos - rect.left - handle_half) / (rect.width - self.handle.rect.w) if rect.width != self.handle.rect.w else 0
122
+ else:
123
+ pos = position[1]
124
+ handle_half = self.handle.rect.h / 2
125
+ pos = max(rect.top + handle_half, min(pos, rect.bottom - handle_half))
126
+ ratio = (rect.bottom - pos - handle_half) / (rect.height - self.handle.rect.h) if rect.height != self.handle.rect.h else 0
127
+
128
+ if self.direction in [bf.direction.LEFT, bf.direction.DOWN]:
129
+ ratio = 1 - ratio
130
+
131
+ value = self.min_value + ratio * self.get_range()
132
+ return round_to_step_precision(value, self.step) if self.snap else value
133
+
134
+
135
+ def handle_event(self, event: pygame.Event):
136
+
137
+
138
+ if (self.is_hovered or getattr(self.parent, 'is_hovered', False) or self.handle.is_hovered) :
139
+ if event.type in [pygame.MOUSEBUTTONUP,pygame.MOUSEBUTTONDOWN] and event.button in [4,5]:
140
+ event.consumed = True
141
+ elif event.type == pygame.MOUSEWHEEL:
142
+ keys = pygame.key.get_pressed()
143
+ shift_held = keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]
144
+ is_vertical = self.axis == bf.axis.VERTICAL
145
+ is_horizontal = not is_vertical
146
+ if (not shift_held and is_horizontal) or (shift_held and is_vertical):
147
+ return
148
+ if event.y:
149
+ delta = -self.step if event.y < 0 else self.step
150
+ if self.direction in [bf.direction.DOWN,bf.direction.LEFT]:
151
+ delta*=-1
152
+ self.parent.set_value(self.parent.get_value() + delta)
153
+ event.consumed = True
154
+ super().handle_event(event)
155
+
156
+ def on_click_down(self, button: int,event=None):
157
+ # old_consume = event.consumed
158
+ if not self.parent.is_enabled:
159
+ return
160
+ super().on_click_down(button,event)
161
+ # event.consumed = old_consume
162
+ if button == 1:
163
+ self.parent.get_focus()
164
+ world_pos = self.parent_layer.camera.get_mouse_pos()
165
+ if self.get_inner_rect().collidepoint(*world_pos):
166
+ new_value = self.position_to_value(world_pos)
167
+ self.set_value(new_value)
168
+ self.handle.on_click_down(button,event)
169
+
170
+ def on_click_up(self, button, event=None):
171
+ super().do_on_click_up(button,event)
172
+
173
+ def _build_content(self):
174
+ super()._build_content()
175
+ handle_size = self.get_inner_height() if self.axis == bf.axis.HORIZONTAL else self.get_inner_width()
176
+ self.handle.set_size(self.handle.resolve_size((handle_size, handle_size)))
177
+ self.handle._on_synced_var_update(self.synced_var.value)
178
+
179
+
180
+ class Slider(Button):
181
+ def __init__(self, text: str, default_value: float = 1.0, synced_var: SyncedVar = None) -> None:
182
+ super().__init__(text, None)
183
+ self.old_key_repeat = (0, 0)
184
+ self.synced_var = synced_var or SyncedVar(default_value)
185
+ self.axis: bf.axis = bf.axis.HORIZONTAL
186
+ self.gap: float | int = 0
187
+ self.spacing: bf.spacing = bf.spacing.MANUAL
188
+ self.modify_callback: Callable[[float], Any] = None
189
+ self.meter: SliderMeter = SliderMeter(synced_var=self.synced_var)
190
+ self.add(self.meter)
191
+ self.synced_var.bind(self, self._on_synced_var_update)
192
+ self.set_range(0, self.synced_var.value)
193
+ self.synced_var.update_bound_entities()
194
+
195
+ def set_snap(self,snap:bool)->Self:
196
+ self.meter.set_snap(snap)
197
+ return self
198
+
199
+ def do_on_get_focus(self) -> None:
200
+ super().do_on_get_focus()
201
+ self.old_key_repeat = pygame.key.get_repeat()
202
+ pygame.key.set_repeat(200, 50)
203
+
204
+ def do_on_lose_focus(self) -> None:
205
+ super().do_on_lose_focus()
206
+ pygame.key.set_repeat(*self.old_key_repeat)
207
+
208
+ def _on_synced_var_update(self, value):
209
+ if self.modify_callback:
210
+ self.modify_callback(value)
211
+ rounded = round_to_step_precision(self.get_value(), self.meter.step)
212
+ self.meter.set_tooltip_text(str(rounded))
213
+
214
+ def set_fill_color(self, color) -> Self:
215
+ self.meter.content.set_color(color)
216
+ return self
217
+
218
+ def set_axis(self, axis: bf.axis) -> Self:
219
+ self.axis = axis
220
+ self.meter.axis = axis
221
+ self.dirty_shape = True
222
+ return self
223
+
224
+ def set_direction(self, direction: bf.direction) -> Self:
225
+ self.meter.set_direction(direction)
226
+ self.set_axis(self.meter.axis)
227
+ return self
228
+
229
+ def set_visible(self, value: bool) -> Self:
230
+ self.meter.set_visible(value)
231
+ return super().set_visible(value)
232
+
233
+ def __str__(self) -> str:
234
+ return "Slider"
235
+
236
+ def set_gap(self, value: int | float) -> Self:
237
+ self.gap = max(0, value)
238
+ return self
239
+
240
+ def set_spacing(self, spacing: bf.spacing) -> Self:
241
+ if spacing != self.spacing:
242
+ self.spacing = spacing
243
+ self.dirty_shape = True
244
+ return self
245
+
246
+ def set_modify_callback(self, callback: Callable[[float], Any]) -> Self:
247
+ self.modify_callback = callback
248
+ return self
249
+
250
+ def set_range(self, range_min: float, range_max: float) -> Self:
251
+ self.meter.set_range(range_min, range_max)
252
+ # self.meter.set_value(self.synced_var.value)
253
+ self.dirty_shape = True
254
+ return self
255
+
256
+ def set_step(self, step: float) -> Self:
257
+ self.meter.set_step(step)
258
+ self.dirty_shape = True
259
+ return self
260
+
261
+ def set_value(self, value) -> Self:
262
+ value = max(self.meter.min_value, min(value, self.meter.max_value))
263
+ self.synced_var.value = value
264
+ return self
265
+
266
+ def get_value(self) -> float:
267
+ return self.synced_var.value
268
+
269
+ def on_key_down(self, key, event):
270
+ super().on_key_down(key, event)
271
+ if event.consumed or not self.is_enabled:
272
+ return
273
+
274
+ step = self.meter.step
275
+ value = self.get_value()
276
+ axis = self.axis
277
+ direction = self.meter.direction
278
+
279
+ if axis == bf.axis.HORIZONTAL:
280
+ if key == pygame.K_RIGHT:
281
+ delta = step if direction == bf.direction.RIGHT else -step
282
+ elif key == pygame.K_LEFT:
283
+ delta = -step if direction == bf.direction.RIGHT else step
284
+ else:
285
+ return
286
+ elif axis == bf.axis.VERTICAL:
287
+ if key == pygame.K_UP:
288
+ delta = step if direction == bf.direction.UP else -step
289
+ elif key == pygame.K_DOWN:
290
+ delta = -step if direction == bf.direction.UP else step
291
+ else:
292
+ return
293
+ else:
294
+ return
295
+
296
+ self.set_value(value + delta)
297
+ event.consumed = True
298
+
299
+
300
+ def get_min_required_size(self) -> tuple[float, float]:
301
+ left = self.text_widget.get_min_required_size()
302
+ right = self.meter.get_min_required_size()
303
+ gap = self.gap if self.text_widget.text else 0
304
+ full_rect = pygame.FRect(0, 0, left[0] + right[0] + gap, left[1])
305
+ full_rect.h += self.unpressed_relief
306
+ return self.expand_rect_with_padding((0, 0, *full_rect.size)).size
307
+
308
+ def _align_composed(self, left: Shape, right: Shape):
309
+ full_rect = self.get_inner_rect()
310
+ left_rect = left.rect
311
+ right_rect = right.rect
312
+ gap = {
313
+ bf.spacing.MIN: 0,
314
+ bf.spacing.HALF: (full_rect.width - left_rect.width - right_rect.width) // 2,
315
+ bf.spacing.MAX: full_rect.width - left_rect.width - right_rect.width,
316
+ bf.spacing.MANUAL: self.gap
317
+ }.get(self.spacing, 0)
318
+ gap = max(0, gap)
319
+ combined_width = left_rect.width + right_rect.width + gap
320
+ group_x = {
321
+ bf.alignment.LEFT: full_rect.left,
322
+ bf.alignment.MIDLEFT: full_rect.left,
323
+ bf.alignment.RIGHT: full_rect.right - combined_width,
324
+ bf.alignment.MIDRIGHT: full_rect.right - combined_width,
325
+ bf.alignment.CENTER: full_rect.centerx - combined_width // 2
326
+ }.get(self.alignment, full_rect.left)
327
+ left.set_position(x=group_x)
328
+ right.set_position(x=group_x + left_rect.width + gap)
329
+ # Set vertical positions
330
+ if self.alignment in {bf.alignment.TOP, bf.alignment.TOPLEFT, bf.alignment.TOPRIGHT}:
331
+ left.set_position(y=full_rect.top)
332
+ right.set_position(y=full_rect.top)
333
+ elif self.alignment in {bf.alignment.BOTTOM, bf.alignment.BOTTOMLEFT, bf.alignment.BOTTOMRIGHT}:
334
+ left.set_position(y=full_rect.bottom - left_rect.height)
335
+ right.set_position(y=full_rect.bottom - right_rect.height)
336
+ else:
337
+ left.set_center(y=full_rect.centery)
338
+ right.set_center(y=full_rect.centery)
339
+
340
+ def build(self) -> None:
341
+ res = super().build()
342
+ gap = self.gap if self.spacing == bf.spacing.MANUAL else 0
343
+ if self.meter.axis==bf.axis.HORIZONTAL:
344
+ meter_width = self.get_inner_width() - self.text_widget.rect.w - gap
345
+ meter_height = min(self.meter.get_min_required_size()[1], self.text_widget.rect.h)
346
+ else:
347
+ meter_height = self.get_inner_height()
348
+ meter_width = min(self.text_widget.rect.h,max(self.meter.get_min_required_size()[0],self.get_inner_width() - self.text_widget.rect.w - gap))
349
+
350
+ meter_size = self.meter.resolve_size((meter_width, meter_height))
351
+ self.meter.set_size(meter_size)
352
+ self._align_composed(self.text_widget, self.meter)
353
+ return res