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.
Files changed (61) 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 +16 -13
  6. batFramework/baseScene.py +240 -0
  7. batFramework/camera.py +4 -0
  8. batFramework/constants.py +6 -1
  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 +3 -1
  16. batFramework/gui/animatedLabel.py +10 -2
  17. batFramework/gui/button.py +4 -31
  18. batFramework/gui/clickableWidget.py +42 -30
  19. batFramework/gui/constraints/constraints.py +212 -136
  20. batFramework/gui/container.py +72 -48
  21. batFramework/gui/debugger.py +12 -17
  22. batFramework/gui/draggableWidget.py +8 -11
  23. batFramework/gui/image.py +3 -10
  24. batFramework/gui/indicator.py +73 -1
  25. batFramework/gui/interactiveWidget.py +117 -100
  26. batFramework/gui/label.py +73 -63
  27. batFramework/gui/layout.py +221 -452
  28. batFramework/gui/meter.py +21 -7
  29. batFramework/gui/radioButton.py +0 -1
  30. batFramework/gui/root.py +99 -29
  31. batFramework/gui/selector.py +257 -0
  32. batFramework/gui/shape.py +13 -5
  33. batFramework/gui/slider.py +260 -93
  34. batFramework/gui/textInput.py +45 -21
  35. batFramework/gui/toggle.py +70 -52
  36. batFramework/gui/tooltip.py +30 -0
  37. batFramework/gui/widget.py +203 -125
  38. batFramework/manager.py +7 -8
  39. batFramework/particle.py +4 -1
  40. batFramework/propertyEaser.py +79 -0
  41. batFramework/renderGroup.py +17 -50
  42. batFramework/resourceManager.py +43 -13
  43. batFramework/scene.py +15 -335
  44. batFramework/sceneLayer.py +138 -0
  45. batFramework/sceneManager.py +31 -36
  46. batFramework/scrollingSprite.py +8 -3
  47. batFramework/sprite.py +1 -1
  48. batFramework/templates/__init__.py +1 -2
  49. batFramework/templates/controller.py +97 -0
  50. batFramework/timeManager.py +76 -22
  51. batFramework/transition.py +37 -103
  52. batFramework/utils.py +121 -3
  53. {batframework-1.0.9a6.dist-info → batframework-1.0.9a8.dist-info}/METADATA +24 -3
  54. batframework-1.0.9a8.dist-info/RECORD +66 -0
  55. {batframework-1.0.9a6.dist-info → batframework-1.0.9a8.dist-info}/WHEEL +1 -1
  56. batFramework/character.py +0 -27
  57. batFramework/templates/character.py +0 -43
  58. batFramework/templates/states.py +0 -166
  59. batframework-1.0.9a6.dist-info/RECORD +0 -63
  60. /batframework-1.0.9a6.dist-info/LICENCE → /batframework-1.0.9a8.dist-info/LICENSE +0 -0
  61. {batframework-1.0.9a6.dist-info → batframework-1.0.9a8.dist-info}/top_level.txt +0 -0
@@ -16,12 +16,14 @@ class SliderHandle(Indicator, DraggableWidget):
16
16
  def __str__(self) -> str:
17
17
  return "SliderHandle"
18
18
 
19
- def on_click_down(self, button: int) -> None:
19
+ def on_click_down(self, button: int) -> bool:
20
20
  if not self.parent.is_enabled():
21
- return
21
+ return False
22
22
  super().on_click_down(button)
23
23
  if button == 1:
24
24
  self.parent.get_focus()
25
+ return True
26
+ return False
25
27
 
26
28
  def on_exit(self) -> None:
27
29
  self.is_hovered = False
@@ -34,13 +36,17 @@ class SliderHandle(Indicator, DraggableWidget):
34
36
  return
35
37
  super().do_on_drag(drag_start, drag_end)
36
38
  m: Meter = self.parent.meter
37
- r = m.get_padded_rect()
38
- position = self.rect.centerx
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)
@@ -50,20 +56,44 @@ class SliderMeter(Meter, InteractiveWidget):
50
56
  def __str__(self) -> str:
51
57
  return "SliderMeter"
52
58
 
53
- def on_click_down(self, button: int) -> None:
59
+ def __init__(self, min_value = 0, max_value = 1, step = 0.1):
60
+ super().__init__(min_value, max_value, step)
61
+ self.set_padding(0)
62
+
63
+ def get_min_required_size(self):
64
+ size = list(super().get_min_required_size())
65
+ if self.parent.axis == bf.axis.HORIZONTAL:
66
+ size[0] = size[1]*3
67
+ else:
68
+ size[1] = size[0]*3
69
+ return self.resolve_size(size)
70
+
71
+ def on_click_down(self, button: int) -> True:
54
72
  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))
73
+ return False
74
+ if button != 1:
75
+ return False
76
+
77
+ self.parent.get_focus()
78
+ r = self.get_root()
79
+ if r:
80
+ pos = r.drawing_camera.screen_to_world(pygame.mouse.get_pos())
81
+ if self.parent.axis == bf.axis.HORIZONTAL:
82
+ pos = pos[0]
83
+ else:
84
+ pos = pos[1]
85
+ new_value = self.parent.position_to_value(pos)
86
+ self.parent.set_value(new_value)
62
87
  self.do_on_click_down(button)
88
+ return True
89
+
90
+ def resolve_constraints(self, size_only = False, position_only = False):
91
+ return super().resolve_constraints(size_only, position_only)
63
92
 
64
93
  class Slider(Button):
65
94
  def __init__(self, text: str, default_value: float = 1.0) -> None:
66
95
  super().__init__(text, None)
96
+ self.axis : bf.axis = bf.axis.HORIZONTAL
67
97
  self.gap: float | int = 0
68
98
  self.spacing: bf.spacing = bf.spacing.MANUAL
69
99
  self.modified_callback : Callable[[float],Any] = None
@@ -73,6 +103,19 @@ class Slider(Button):
73
103
  self.meter.set_debug_color(bf.color.RED)
74
104
  self.set_value(default_value, True)
75
105
 
106
+ def set_tooltip_text(self, text):
107
+ return super().set_tooltip_text(text)
108
+
109
+ def set_fill_color(self,color)->Self:
110
+ self.meter.content.set_color(color)
111
+ return self
112
+
113
+ def set_axis(self,axis:bf.axis)->Self:
114
+ self.axis = axis
115
+ self.meter.set_axis(axis)
116
+ self.dirty_shape = True
117
+ return self
118
+
76
119
  def set_visible(self, value: bool) -> Self:
77
120
  self.handle.set_visible(value)
78
121
  self.meter.set_visible(value)
@@ -119,21 +162,38 @@ class Slider(Button):
119
162
  def set_value(self, value, no_callback: bool = False) -> Self:
120
163
  if self.meter.value != value:
121
164
  self.meter.set_value(value)
122
- self.dirty_shape = True
123
165
  if self.modified_callback and (not no_callback):
124
166
  self.modified_callback(self.meter.value)
167
+ self.handle.set_tooltip_text(str(self.get_value()))
168
+ self.meter.set_tooltip_text(str(self.get_value()))
169
+
125
170
  return self
126
171
 
127
172
  def get_value(self) -> float:
128
173
  return self.meter.get_value()
129
174
 
130
- def do_on_key_down(self, key):
175
+ def on_key_down(self, key):
176
+ if super().on_key_down(key):
177
+ return True
131
178
  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)
179
+ return False
180
+ if self.axis == bf.axis.HORIZONTAL:
181
+ if key == pygame.K_RIGHT:
182
+ self.set_value(self.meter.get_value() + self.meter.step)
183
+ elif key == pygame.K_LEFT:
184
+ self.set_value(self.meter.get_value() - self.meter.step)
185
+ else:
186
+ return False
187
+ return True
188
+ else:
189
+ if key == pygame.K_UP:
190
+ self.set_value(self.meter.get_value() + self.meter.step)
191
+ elif key == pygame.K_DOWN:
192
+ self.set_value(self.meter.get_value() - self.meter.step)
193
+ else:
194
+ return False
195
+
196
+ return True
137
197
 
138
198
  def do_on_click_down(self, button) -> None:
139
199
  if not self.is_enabled():
@@ -145,98 +205,205 @@ class Slider(Button):
145
205
  """
146
206
  Converts a value to a position on the meter, considering the step size.
147
207
  """
148
- rect = self.meter.get_padded_rect()
208
+ rect = self.meter.get_inner_rect()
149
209
  value_range = self.meter.get_range()
150
210
  value = round(value / self.meter.step) * self.meter.step
151
211
  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
-
212
+ if self.axis == bf.axis.HORIZONTAL:
213
+ return (
214
+ rect.left
215
+ + (self.handle.rect.w / 2)
216
+ + position_ratio * (rect.width - self.handle.rect.w)
217
+ )
218
+ else:
219
+ return (
220
+ rect.bottom
221
+ - (self.handle.rect.h / 2)
222
+ - position_ratio * (rect.height - self.handle.rect.h)
223
+ )
224
+
158
225
  def position_to_value(self, position: float) -> float:
159
226
  """
160
227
  Converts a position on the meter to a value, considering the step size.
161
228
  """
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))
229
+ rect = self.meter.get_inner_rect()
230
+ if self.axis == bf.axis.HORIZONTAL:
231
+ if self.rect.w == self.handle.rect.w:
232
+ position_ratio = 0
233
+ else:
234
+ handle_half = self.handle.rect.w // 2
235
+ position = max(rect.left + handle_half, min(position, rect.right - handle_half))
236
+ position_ratio = (position - rect.left - handle_half) / (
237
+ rect.width - self.handle.rect.w
238
+ )
239
+ else:
240
+ if self.rect.h == self.handle.rect.h:
241
+ position_ratio = 0
242
+ else:
243
+ handle_half = self.handle.rect.h // 2
244
+ position = max(rect.top + handle_half, min(position, rect.bottom - handle_half))
245
+ # Flip ratio vertically: bottom is min, top is max
246
+ position_ratio = (rect.bottom - position - handle_half) / (
247
+ rect.height - self.handle.rect.h
248
+ )
165
249
 
166
- position_ratio = (position - rect.left - handle_half) / (
167
- rect.width - self.handle.rect.w
168
- )
169
250
  value_range = self.meter.get_range()
170
251
  value = self.meter.min_value + position_ratio * value_range
171
252
  return round(value / self.meter.step) * self.meter.step
172
253
 
173
- 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
254
 
181
- def _build_layout(self) -> None:
255
+ def get_min_required_size(self) -> tuple[float, float]:
256
+ """
257
+ Calculates the minimum required size for the slider, considering the text, meter, and axis.
182
258
 
259
+ Returns:
260
+ tuple[float, float]: The width and height of the minimum required size.
261
+ """
183
262
  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))
193
-
194
-
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
- )
199
-
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
-
263
+ text_width, text_height = self._get_text_rect_required_size() if self.text else (0, 0)
264
+ meter_width, meter_height = self.meter.resolve_size(self.meter.get_min_required_size())
214
265
 
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)
266
+ if self.axis == bf.axis.HORIZONTAL:
267
+ width = text_width + gap + meter_width
268
+ height = max(text_height, meter_height)
269
+ else:
270
+ width = max(text_width, meter_width)
271
+ height = text_height + gap + meter_height
218
272
 
219
- self.align_text(joined_rect, padded_relative.move( offset), self.alignment)
220
- self.text_rect.midleft = joined_rect.midleft
273
+ return self.expand_rect_with_padding((0, 0, width, height)).size
274
+
221
275
 
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
276
+ def _build_composed_layout(self, other: Shape) -> None:
277
+ """
278
+ Builds the layout for the slider, ensuring that child elements (handle and meter)
279
+ are resized and positioned correctly based on the slider's size and axis.
228
280
 
229
- # place meter
281
+ Args:
282
+ other (Shape): The shape object to align with the slider.
283
+ """
284
+ size_changed = False
285
+ gap = self.gap if self.text else 0
230
286
 
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
287
+ full_rect = self.text_rect.copy()
288
+ # Resolve the meter's size based on the axis and autoresize conditions
289
+ if self.axis == bf.axis.HORIZONTAL:
290
+ meter_width = self.meter.get_min_required_size()[0] if self.meter.autoresize_w else self.meter.rect.w
291
+ full_rect.w = max(self.get_inner_width(), meter_width + gap + self.text_rect.w)
292
+ self.meter.set_size((meter_width, None))
293
+ full_rect.h = max(self.meter.rect.h, self.text_rect.h if self.text else self.meter.rect.h)
294
+ else: # VERTICAL
295
+ meter_height = self.meter.get_min_required_size()[1] if self.meter.autoresize_h else self.meter.rect.h
296
+ full_rect.h = max(self.get_inner_height(), meter_height + (gap + self.text_rect.h if self.text else 0))
297
+ self.meter.set_size((None, meter_height))
298
+ full_rect.w = max(self.meter.rect.w, self.text_rect.w if self.text else self.meter.rect.w)
299
+
300
+ # Inflate the rect by padding and resolve the target size
301
+ inflated = self.expand_rect_with_padding((0, 0, *full_rect.size)).size
302
+ target_size = self.resolve_size(inflated)
303
+
304
+ # Update the slider's size if it doesn't match the target size
305
+ if self.rect.size != target_size:
306
+ self.set_size(target_size)
307
+ size_changed = True
308
+
309
+ # Adjust the handle size based on the meter's size
310
+ if self.axis == bf.axis.HORIZONTAL:
311
+ handle_size = self.meter.get_inner_height()
312
+ self.handle.set_size(self.handle.resolve_size((handle_size, handle_size)))
313
+ else:
314
+ handle_size = self.meter.get_inner_width()
315
+ self.handle.set_size(self.handle.resolve_size((handle_size, handle_size)))
316
+
317
+ # Align the composed elements (text and meter)
318
+ self._align_composed(other)
319
+
320
+ # Position the handle based on the current value
321
+ if self.axis == bf.axis.HORIZONTAL:
322
+ self.handle.set_center(self.value_to_position(self.meter.value), self.meter.rect.centery)
323
+ else:
324
+ self.handle.set_center(self.meter.rect.centerx, self.value_to_position(self.meter.value))
325
+ return size_changed
326
+ def _align_composed(self, other: Shape):
327
+ if not self.text:
328
+ self.text_rect.size = (0,0)
329
+
330
+
331
+ full_rect = self.get_local_padded_rect()
332
+ left_rect = self.text_rect
333
+ right_rect = other.rect.copy()
334
+
335
+
336
+
337
+ if self.axis == bf.axis.HORIZONTAL:
338
+ gap = {
339
+ bf.spacing.MIN: 0,
340
+ bf.spacing.HALF: (full_rect.width - left_rect.width - right_rect.width) // 2,
341
+ bf.spacing.MAX: full_rect.width - left_rect.width - right_rect.width,
342
+ bf.spacing.MANUAL: self.gap
343
+ }.get(self.spacing, 0)
344
+
345
+ gap = max(0, gap)
346
+ combined_width = left_rect.width + right_rect.width + gap
347
+
348
+ group_x = {
349
+ bf.alignment.LEFT: full_rect.left,
350
+ bf.alignment.MIDLEFT: full_rect.left,
351
+ bf.alignment.RIGHT: full_rect.right - combined_width,
352
+ bf.alignment.MIDRIGHT: full_rect.right - combined_width,
353
+ bf.alignment.CENTER: full_rect.centerx - combined_width // 2
354
+ }.get(self.alignment, full_rect.left)
355
+
356
+ left_rect.x, right_rect.x = group_x, group_x + left_rect.width + gap
357
+ left_rect.centery = right_rect.centery = full_rect.centery
358
+
359
+ else: # VERTICAL
360
+ gap = {
361
+ bf.spacing.MIN: 0,
362
+ bf.spacing.HALF: (full_rect.height - left_rect.height - right_rect.height) // 2,
363
+ bf.spacing.MAX: full_rect.height - left_rect.height - right_rect.height,
364
+ bf.spacing.MANUAL: self.gap
365
+ }.get(self.spacing, 0)
366
+
367
+ gap = max(0, gap)
368
+ combined_height = left_rect.height + right_rect.height + gap
369
+
370
+ group_y = {
371
+ bf.alignment.TOP: full_rect.top,
372
+ bf.alignment.MIDTOP: full_rect.top,
373
+ bf.alignment.BOTTOM: full_rect.bottom - combined_height,
374
+ bf.alignment.MIDBOTTOM: full_rect.bottom - combined_height,
375
+ bf.alignment.CENTER: full_rect.centery - combined_height // 2
376
+ }.get(self.alignment, full_rect.top)
377
+
378
+ left_rect.y, right_rect.y = group_y, group_y + left_rect.height + gap
379
+ left_rect.centerx = right_rect.centerx = full_rect.centerx
380
+
381
+ # Push text to local, push shape to world
382
+ self.text_rect = left_rect
383
+ right_rect.move_ip(*self.rect.topleft)
384
+ other.set_position(*right_rect.topleft)
237
385
 
238
- x = self.value_to_position(self.meter.value)
239
- r = self.meter.get_padded_rect()
240
- self.handle.set_center(x, r.centery)
386
+ def _build_layout(self) -> None:
387
+ self.text_rect.size = self._get_text_rect_required_size()
388
+ return self._build_composed_layout(self.meter)
389
+
241
390
 
242
- # self.handle.set_center(x,self.rect.top)
391
+ def apply_pre_updates(self):
392
+ # Step 1: Constraints and shape/size
393
+ super().apply_pre_updates()
394
+ # Build text rect size
395
+ self.text_rect.size = self._get_text_rect_required_size()
396
+ # Compose layout for meter (but not handle position yet)
397
+ self._build_composed_layout(self.meter)
398
+ # Meter and handle may need to update their own pre-updates
399
+ self.meter.apply_pre_updates()
400
+ self.handle.apply_pre_updates()
401
+
402
+
403
+ def apply_post_updates(self, skip_draw: bool = False):
404
+ # Step 2: Final alignment and painting
405
+ super().apply_post_updates(skip_draw=skip_draw)
406
+ # Align handle to value (now that sizes are final)
407
+ self._align_composed(self.meter)
408
+ self.handle.apply_post_updates(skip_draw=skip_draw)
409
+ self.meter.apply_post_updates(skip_draw=skip_draw)
@@ -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
@@ -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