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.
Files changed (62) hide show
  1. batFramework/__init__.py +20 -11
  2. batFramework/action.py +1 -1
  3. batFramework/animatedSprite.py +47 -116
  4. batFramework/animation.py +30 -5
  5. batFramework/audioManager.py +8 -5
  6. batFramework/baseScene.py +240 -0
  7. batFramework/camera.py +4 -0
  8. batFramework/constants.py +6 -2
  9. batFramework/cutscene.py +221 -21
  10. batFramework/cutsceneManager.py +5 -2
  11. batFramework/drawable.py +7 -5
  12. batFramework/easingController.py +10 -11
  13. batFramework/entity.py +21 -2
  14. batFramework/enums.py +48 -33
  15. batFramework/gui/__init__.py +6 -3
  16. batFramework/gui/animatedLabel.py +10 -2
  17. batFramework/gui/button.py +4 -31
  18. batFramework/gui/clickableWidget.py +63 -50
  19. batFramework/gui/constraints/constraints.py +212 -136
  20. batFramework/gui/container.py +77 -58
  21. batFramework/gui/debugger.py +12 -17
  22. batFramework/gui/draggableWidget.py +21 -17
  23. batFramework/gui/image.py +3 -10
  24. batFramework/gui/indicator.py +56 -1
  25. batFramework/gui/interactiveWidget.py +127 -108
  26. batFramework/gui/label.py +73 -64
  27. batFramework/gui/layout.py +286 -445
  28. batFramework/gui/meter.py +42 -20
  29. batFramework/gui/radioButton.py +20 -69
  30. batFramework/gui/root.py +99 -29
  31. batFramework/gui/selector.py +250 -0
  32. batFramework/gui/shape.py +13 -5
  33. batFramework/gui/slider.py +262 -107
  34. batFramework/gui/syncedVar.py +49 -0
  35. batFramework/gui/textInput.py +46 -22
  36. batFramework/gui/toggle.py +70 -52
  37. batFramework/gui/tooltip.py +30 -0
  38. batFramework/gui/widget.py +222 -135
  39. batFramework/manager.py +7 -8
  40. batFramework/particle.py +4 -1
  41. batFramework/propertyEaser.py +79 -0
  42. batFramework/renderGroup.py +17 -50
  43. batFramework/resourceManager.py +43 -13
  44. batFramework/scene.py +15 -335
  45. batFramework/sceneLayer.py +138 -0
  46. batFramework/sceneManager.py +31 -36
  47. batFramework/scrollingSprite.py +8 -3
  48. batFramework/sprite.py +1 -1
  49. batFramework/templates/__init__.py +1 -2
  50. batFramework/templates/controller.py +97 -0
  51. batFramework/timeManager.py +76 -22
  52. batFramework/transition.py +37 -103
  53. batFramework/utils.py +125 -66
  54. {batframework-1.0.9a7.dist-info → batframework-1.0.9a9.dist-info}/METADATA +24 -3
  55. batframework-1.0.9a9.dist-info/RECORD +67 -0
  56. {batframework-1.0.9a7.dist-info → batframework-1.0.9a9.dist-info}/WHEEL +1 -1
  57. batFramework/character.py +0 -27
  58. batFramework/templates/character.py +0 -43
  59. batFramework/templates/states.py +0 -166
  60. batframework-1.0.9a7.dist-info/RECORD +0 -63
  61. /batframework-1.0.9a7.dist-info/LICENCE → /batframework-1.0.9a9.dist-info/LICENSE +0 -0
  62. {batframework-1.0.9a7.dist-info → batframework-1.0.9a9.dist-info}/top_level.txt +0 -0
@@ -1,69 +1,94 @@
1
1
  import batFramework as bf
2
- from .meter import Meter
2
+ from .meter import BarMeter
3
3
  from .button import Button
4
4
  from .indicator import *
5
- from .meter import Meter
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) -> None:
20
- if not self.parent.is_enabled():
21
- return
22
- super().on_click_down(button)
23
- if button == 1:
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
- def on_exit(self) -> None:
27
- self.is_hovered = False
28
- self.do_on_exit()
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: Meter = self.parent.meter
37
- r = m.get_padded_rect()
38
- position = self.rect.centerx
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.rect.centerx = self.parent.value_to_position(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)
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 on_click_down(self, button: int) -> None:
54
- if not self.parent.is_enabled():
55
- return
56
- if button == 1:
57
- self.parent.get_focus()
58
- r = self.get_root()
59
- if r:
60
- pos = r.drawing_camera.screen_to_world(pygame.mouse.get_pos())[0]
61
- self.parent.set_value(self.parent.position_to_value(pos))
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 do_on_key_down(self, key):
131
- if not self.is_enabled():
132
- return
133
- if key == pygame.K_RIGHT:
134
- self.set_value(self.meter.get_value() + self.meter.step)
135
- elif key == pygame.K_LEFT:
136
- self.set_value(self.meter.get_value() - self.meter.step)
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.get_padded_rect()
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
- return (
153
- rect.left
154
- + (self.handle.rect.w / 2)
155
- + position_ratio * (rect.width - self.handle.rect.w)
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
- handle_half = self.handle.rect.w / 2
163
- rect = self.meter.get_padded_rect()
164
- position = max(rect.left + handle_half, min(position, rect.right - handle_half))
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
- gap = self.gap if self.text else 0
175
- if not self.text_rect:
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
- self.text_rect.size = self._get_text_rect_required_size()
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
- #join left and right
196
- joined_rect = pygame.FRect(
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
- if self.autoresize_h or self.autoresize_w:
201
- target_rect = self.inflate_rect_by_padding(joined_rect)
202
- target_rect.h += self.unpressed_relief
203
- if not self.autoresize_w:
204
- target_rect.w = self.rect.w
205
- if not self.autoresize_h:
206
- target_rect.h = self.rect.h
207
- if self.rect.size != target_rect.size:
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
- offset = self._get_outline_offset() if self.show_text_outline else (0,0)
216
- padded_rect = self.get_padded_rect()
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.align_text(joined_rect, padded_relative.move( offset), self.alignment)
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
- # place meter
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
- pos = self.text_rect.move(
232
- self.rect.x + gap -offset[0],
233
- self.rect.y + (self.text_rect.h / 2) - (right_part_height / 2) -offset[1],
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
- x = self.value_to_position(self.meter.value)
239
- r = self.meter.get_padded_rect()
240
- self.handle.set_center(x, r.centery)
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
- # self.handle.set_center(x,self.rect.top)
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)
@@ -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=True).start()
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({repr(self.text)})"
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
- yield (self.get_cursor_rect().move(-self.scroll+self.rect.topleft),"green")
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.dirty_surface = True
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.get_padded_rect().move(-self.rect.x + offset[0], -self.rect.y + offset[1])
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.get_padded_rect().__getattribute__(self.alignment.value)[1] - self.rect.top
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
- self.set_text(f"{text[:current_pos - 1]}{text[current_pos:]}")
195
- self.set_cursor_position(self.absolute_to_cursor(current_pos - 1))
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
- pygame.draw.rect(self.surface, bf.color.CLOUD, cursor_rect.inflate(2,2))
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
- self.scroll.x=cursor_rect.right - area.right - cursor_rect.w*2
274
- elif cursor_rect.x < self.scroll.x+area.left:
275
- self.scroll.x= cursor_rect.left - area.left
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 > area.bottom + self.scroll.y:
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