batframework 1.0.9a10__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 -245
  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 -201
  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 -426
  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.9a10.dist-info → batframework-1.0.9a12.dist-info}/LICENSE +20 -20
  69. {batframework-1.0.9a10.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.9a10.dist-info/RECORD +0 -67
  72. {batframework-1.0.9a10.dist-info → batframework-1.0.9a12.dist-info}/WHEEL +0 -0
  73. {batframework-1.0.9a10.dist-info → batframework-1.0.9a12.dist-info}/top_level.txt +0 -0
@@ -1,306 +1,331 @@
1
- import batFramework as bf
2
- from typing import Self, Callable
3
- from .label import Label
4
- from .interactiveWidget import InteractiveWidget
5
- import pygame
6
-
7
- def find_next_word(s: str, start_index: int) -> int:
8
- length = len(s)
9
- # Ensure the starting index is within the bounds of the string
10
- if start_index < 0 or start_index >= length:
11
- raise ValueError("Starting index is out of bounds")
12
- index = start_index
13
- # If the start_index is at a space, skip leading spaces
14
- if s[index] in [' ','\n']:
15
- while index < length and s[index] in [' ','\n']:
16
- index += 1
17
- # If we've reached the end of the string
18
- if index >= length:
19
- return -1
20
- else:
21
- # If the start_index is within a word, move to the end of that word
22
- while index < length and s[index] not in [' ','\n']:
23
- index += 1
24
- if index == length:
25
- return index
26
- # Return the index of the start of the next word or -1 if no more words are found
27
- return index if index < length else -1
28
-
29
- def find_prev_word(s: str, start_index: int) -> int:
30
- if start_index <= 0 : return 0
31
- length = len(s)
32
-
33
- # Ensure the starting index is within the bounds of the string
34
- if start_index < 0 or start_index >= length:
35
- raise ValueError("Starting index is out of bounds")
36
-
37
- index = start_index
38
-
39
- # If the start_index is at a space, skip trailing spaces
40
- if s[index] in [' ', '\n']:
41
- while index > 0 and s[index-1] in [' ', '\n']:
42
- index -= 1
43
- # If we've reached the beginning of the string
44
- if index <= 0:
45
- return 0 if s[0] not in [' ', '\n'] else -1
46
- else:
47
- # If the start_index is within a word, move to the start of that word
48
- while index > 0 and s[index-1] not in [' ', '\n']:
49
- index -= 1
50
- if index == 0 and s[index] not in [' ', '\n']:
51
- return 0
52
-
53
- # Return the index of the start of the previous word or -1 if no more words are found
54
- return index if index > 0 or (index == 0 and s[0] not in [' ', '\n']) else -1
55
-
56
-
57
- class TextInput(Label, InteractiveWidget):
58
- def __init__(self) -> None:
59
- self.cursor_position = (0, 0)
60
- self.old_key_repeat = (0, 0)
61
- self.cursor_timer = bf.Timer(0.2, self._cursor_toggle, loop=-1).start()
62
- self.cursor_timer.pause()
63
- self.show_cursor = False
64
- self.on_modify: Callable[[str], str] = None
65
- self.set_focusable(True)
66
- self.set_outline_color("black")
67
- super().__init__("")
68
- self.alignment = bf.alignment.TOPLEFT
69
-
70
- def set_modify_callback(self, callback: Callable[[str], str]) -> Self:
71
- self.on_modify = callback
72
- return self
73
-
74
- def __str__(self) -> str:
75
- return f"TextInput"
76
-
77
- def _cursor_toggle(self, value: bool | None = None):
78
- if value is None:
79
- value = not self.show_cursor
80
- self.show_cursor = value
81
- self.dirty_surface = True
82
-
83
- def do_on_click_down(self, button):
84
- if button != 1:
85
- return False
86
- self.get_focus()
87
- return True
88
-
89
- def do_on_enter(self):
90
- pygame.mouse.set_cursor(pygame.SYSTEM_CURSOR_IBEAM)
91
-
92
- def do_on_exit(self):
93
- pygame.mouse.set_cursor(bf.const.DEFAULT_CURSOR)
94
-
95
- def do_on_get_focus(self):
96
- self.cursor_timer.resume()
97
- self._cursor_toggle(True)
98
- self.old_key_repeat = pygame.key.get_repeat()
99
- pygame.key.set_repeat(200, 50)
100
-
101
- def do_on_lose_focus(self):
102
- self.cursor_timer.pause()
103
- self._cursor_toggle(False)
104
- pygame.key.set_repeat(*self.old_key_repeat)
105
-
106
- def get_line(self, line: int) -> str | None:
107
- if line < 0:
108
- return None
109
- lines = self.text.split('\n')
110
- if line >= len(lines):
111
- return None
112
- return lines[line]
113
-
114
- def get_debug_outlines(self):
115
- yield from super().get_debug_outlines()
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
127
-
128
-
129
- def set_cursor_position(self, position: tuple[int, int]) -> Self:
130
- x, y = position
131
-
132
- lines = self.text.split('\n')
133
- y = max(0, min(y, len(lines) - 1))
134
- line_length = len(lines[y])
135
- x = max(0, min(x, line_length))
136
- self.show_cursor = True
137
- self.cursor_position = (x,y)
138
- self.apply_post_updates(skip_draw=True)
139
- offset = self._get_outline_offset() if self.show_text_outline else (0,0)
140
- padded = self.get_inner_rect().move(-self.rect.x + offset[0], -self.rect.y + offset[1])
141
- self.align_text(self.text_rect,padded,self.alignment)
142
- return self
143
-
144
- def get_cursor_rect(self)->pygame.FRect:
145
- if not self.font_object:
146
- return pygame.FRect(0,0,0,0)
147
-
148
- lines = self.text.split('\n')
149
- line_x, line_y = self.cursor_position
150
-
151
- height = self.font_object.get_linesize()
152
-
153
- cursor_y = self.get_inner_rect().__getattribute__(self.alignment.value)[1] - self.rect.top
154
- cursor_y += line_y * height
155
- cursor_x = self.text_rect.x
156
- cursor_x += self.font_object.size(lines[line_y][:line_x])[0] if line_x > 0 else 0
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)
160
- return cursor_rect
161
-
162
- def cursor_to_absolute(self, position: tuple[int, int]) -> int:
163
- x, y = position
164
-
165
- y = max(0, min(y, len(self.text.split('\n')) - 1))
166
- lines = self.text.split('\n')
167
- x = max(0, min(x, len(lines[y])))
168
-
169
- absolute_position = sum(len(line) + 1 for line in lines[:y]) + x
170
- return absolute_position
171
-
172
- def absolute_to_cursor(self, absolute: int) -> tuple[int, int]:
173
- text = self.text
174
- lines = text.split('\n')
175
- current_pos = 0
176
-
177
- for line_no, line in enumerate(lines):
178
- if absolute <= current_pos + len(line):
179
- return (absolute - current_pos, line_no)
180
- current_pos += len(line) + 1
181
-
182
- return (len(lines[-1]), len(lines) - 1)
183
-
184
- def do_handle_event(self, event):
185
- if not self.is_focused or event.type not in [pygame.TEXTINPUT, pygame.KEYDOWN]:
186
- return
187
-
188
- text = self.get_text()
189
- current_pos = self.cursor_to_absolute(self.cursor_position)
190
- pressed = pygame.key.get_pressed()
191
-
192
- if event.type == pygame.TEXTINPUT:
193
- # Insert text at the current cursor position
194
- self.set_text(f"{text[:current_pos]}{event.text}{text[current_pos:]}")
195
- self.set_cursor_position(self.absolute_to_cursor(current_pos + len(event.text)))
196
- elif event.type == pygame.KEYDOWN:
197
- match event.key:
198
- case pygame.K_ESCAPE:
199
- self.lose_focus()
200
-
201
- case pygame.K_BACKSPACE if current_pos > 0:
202
- # Remove the character before the cursor
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)
211
-
212
- case pygame.K_DELETE if current_pos < len(text):
213
- # Remove the character at the cursor
214
- self.set_text(f"{text[:current_pos]}{text[current_pos + 1:]}")
215
- self._cursor_toggle(True)
216
-
217
- case pygame.K_RIGHT:
218
- if current_pos < len(text):
219
- self.handle_cursor_movement(pressed, current_pos, direction="right")
220
- self._cursor_toggle(True)
221
-
222
- case pygame.K_LEFT:
223
- if current_pos > 0:
224
- self.handle_cursor_movement(pressed, current_pos, direction="left")
225
- self._cursor_toggle(True)
226
-
227
- case pygame.K_UP:
228
- # Move cursor up one line
229
- self.set_cursor_position((self.cursor_position[0], self.cursor_position[1] - 1))
230
- self._cursor_toggle(True)
231
-
232
- case pygame.K_DOWN:
233
- # Move cursor down one line
234
- self.set_cursor_position((self.cursor_position[0], self.cursor_position[1] + 1))
235
- self._cursor_toggle(True)
236
-
237
- case pygame.K_RETURN:
238
- # Insert a newline at the current cursor position
239
- self.set_text(f"{text[:current_pos]}\n{text[current_pos:]}")
240
- self.set_cursor_position(self.absolute_to_cursor(current_pos + 1))
241
- self._cursor_toggle(True)
242
- case _ :
243
- if event.unicode:
244
- event.consumed = True
245
- return
246
-
247
- event.consumed = True
248
-
249
- def handle_cursor_movement(self, pressed, current_pos, direction):
250
- if direction == "right":
251
- if pressed[pygame.K_LCTRL] or pressed[pygame.K_RCTRL]:
252
- next_word_pos = find_next_word(self.text, current_pos)
253
- self.set_cursor_position(self.absolute_to_cursor(next_word_pos if next_word_pos != -1 else current_pos + 1))
254
- else:
255
- self.set_cursor_position(self.absolute_to_cursor(current_pos + 1))
256
- elif direction == "left":
257
- if pressed[pygame.K_LCTRL] or pressed[pygame.K_RCTRL]:
258
- prev_word_pos = find_prev_word(self.text, current_pos - 1)
259
- self.set_cursor_position(self.absolute_to_cursor(prev_word_pos if prev_word_pos != -1 else current_pos - 1))
260
- else:
261
- self.set_cursor_position(self.absolute_to_cursor(current_pos - 1))
262
-
263
-
264
- def set_text(self, text: str) -> Self:
265
- if self.on_modify:
266
- text = self.on_modify(text)
267
- return super().set_text(text)
268
-
269
- def _paint_cursor(self) -> None:
270
- if not self.font_object or not self.show_cursor:
271
- return
272
- cursor_rect = self.get_cursor_rect()
273
- cursor_rect.move_ip(-self.scroll)
274
-
275
- if self.show_text_outline:
276
- pygame.draw.rect(self.surface, self.text_outline_color, cursor_rect.inflate(2,2))
277
- pygame.draw.rect(self.surface, self.text_color, cursor_rect)
278
-
279
- def paint(self) -> None:
280
- super().paint()
281
- self._paint_cursor()
282
-
283
- def align_text(
284
- self, text_rect: pygame.FRect, area: pygame.FRect, alignment: bf.alignment
285
- ):
286
- cursor_rect = self.get_cursor_rect()
287
-
288
- if alignment == bf.alignment.LEFT:
289
- alignment = bf.alignment.MIDLEFT
290
- elif alignment == bf.alignment.MIDRIGHT:
291
- alignment = bf.alignment.MIDRIGHT
292
- pos = area.__getattribute__(alignment.value)
293
- text_rect.__setattr__(alignment.value, pos)
294
-
295
-
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)
301
-
302
- if cursor_rect.bottom > self.scroll.y + area.bottom:
303
- self.scroll.y = cursor_rect.bottom - area.bottom
304
- elif cursor_rect.y < self.scroll.y + area.top:
305
- self.scroll.y = cursor_rect.top - area.top
306
- self.scroll.y = max(self.scroll.y, 0)
1
+ import batFramework as bf
2
+ from typing import Self, Callable
3
+ from .label import Label
4
+ from .interactiveWidget import InteractiveWidget
5
+ import pygame
6
+
7
+ def find_next_word(s: str, start_index: int) -> int:
8
+ length = len(s)
9
+ # Ensure the starting index is within the bounds of the string
10
+ if start_index < 0 or start_index >= length:
11
+ raise ValueError("Starting index is out of bounds")
12
+ index = start_index
13
+ # If the start_index is at a space, skip leading spaces
14
+ if s[index] in [' ','\n']:
15
+ while index < length and s[index] in [' ','\n']:
16
+ index += 1
17
+ # If we've reached the end of the string
18
+ if index >= length:
19
+ return -1
20
+ else:
21
+ # If the start_index is within a word, move to the end of that word
22
+ while index < length and s[index] not in [' ','\n']:
23
+ index += 1
24
+ if index == length:
25
+ return index
26
+ # Return the index of the start of the next word or -1 if no more words are found
27
+ return index if index < length else -1
28
+
29
+ def find_prev_word(s: str, start_index: int) -> int:
30
+ if start_index <= 0 : return 0
31
+ length = len(s)
32
+
33
+ # Ensure the starting index is within the bounds of the string
34
+ if start_index < 0 or start_index >= length:
35
+ raise ValueError("Starting index is out of bounds")
36
+
37
+ index = start_index
38
+
39
+ # If the start_index is at a space, skip trailing spaces
40
+ if s[index] in [' ', '\n']:
41
+ while index > 0 and s[index-1] in [' ', '\n']:
42
+ index -= 1
43
+ # If we've reached the beginning of the string
44
+ if index <= 0:
45
+ return 0 if s[0] not in [' ', '\n'] else -1
46
+ else:
47
+ # If the start_index is within a word, move to the start of that word
48
+ while index > 0 and s[index-1] not in [' ', '\n']:
49
+ index -= 1
50
+ if index == 0 and s[index] not in [' ', '\n']:
51
+ return 0
52
+
53
+ # Return the index of the start of the previous word or -1 if no more words are found
54
+ return index if index > 0 or (index == 0 and s[0] not in [' ', '\n']) else -1
55
+
56
+
57
+ class TextInput(Label, InteractiveWidget):
58
+ def __init__(self) -> None:
59
+ self.cursor_position = (0, 0)
60
+ self.old_key_repeat = (0, 0)
61
+ self.placeholder_text = ""
62
+ self.cursor_timer = bf.Timer(0.2, self._cursor_toggle, loop=-1).start()
63
+ self.cursor_timer.pause()
64
+ self.show_cursor = False
65
+ self._cursor_blink_show : bool = True
66
+ self.on_modify: Callable[[str], str]| None = None
67
+ self.set_click_pass_through(False)
68
+ super().__init__("")
69
+ self.set_outline_color("black")
70
+ self.alignment = bf.alignment.TOPLEFT
71
+
72
+ def set_placeholder_text(self, text: str) -> Self:
73
+ self.placeholder_text = text
74
+ self.dirty_surface = True
75
+ return self
76
+
77
+
78
+ def set_modify_callback(self, callback: Callable[[str], str]) -> Self:
79
+ self.on_modify = callback
80
+ return self
81
+
82
+ def __str__(self) -> str:
83
+ return f"TextInput"
84
+
85
+ def _cursor_toggle(self, value: bool | None = None):
86
+ if value is None:
87
+ value = not self._cursor_blink_show
88
+ self._cursor_blink_show = value
89
+ self.dirty_surface = True
90
+
91
+ def on_click_down(self, button,event):
92
+ if button == 1:
93
+ self.get_focus()
94
+ event.consumed = True
95
+ super().on_click_down(button,event)
96
+
97
+ def do_on_enter(self):
98
+ pygame.mouse.set_cursor(pygame.SYSTEM_CURSOR_IBEAM)
99
+
100
+ def do_on_exit(self):
101
+ pygame.mouse.set_cursor(bf.const.DEFAULT_CURSOR)
102
+
103
+ def do_on_get_focus(self):
104
+ self.cursor_timer.resume()
105
+ # self._cursor_toggle(True)
106
+ self.show_cursor = True
107
+ self.old_key_repeat = pygame.key.get_repeat()
108
+ pygame.key.set_repeat(200, 50)
109
+
110
+ def do_on_lose_focus(self):
111
+ self.cursor_timer.pause()
112
+ self.show_cursor = False
113
+ pygame.key.set_repeat(*self.old_key_repeat)
114
+
115
+ def get_line(self, line: int) -> str | None:
116
+ if line < 0:
117
+ return None
118
+ lines = self.text_widget.text.split('\n')
119
+ if line >= len(lines):
120
+ return None
121
+ return lines[line]
122
+
123
+ def get_debug_outlines(self):
124
+ yield from super().get_debug_outlines()
125
+ if self.visible:
126
+ # offset = self._get_outline_offset() if self.show_text_outline else (0,0)
127
+ # yield (self.text_widget.rect.move(self.rect.x - offset[0] - self.scroll.x,self.rect.y - offset[1] - self.scroll.y), "purple")
128
+ yield (self.get_cursor_rect().move(*self.text_widget.rect.topleft),"green")
129
+
130
+
131
+ def get_min_required_size(self) -> tuple[float, float]:
132
+ size = self.text_widget.get_min_required_size()
133
+ return self.expand_rect_with_padding(
134
+ (0, 0,size[0]+self.get_cursor_rect().w,size[1])
135
+ ).size
136
+
137
+
138
+ def set_cursor_position(self, position: tuple[int, int]) -> Self:
139
+ x, y = position
140
+
141
+ lines = self.text_widget.text.split('\n')
142
+ y = max(0, min(y, len(lines) - 1))
143
+ line_length = len(lines[y])
144
+ x = max(0, min(x, line_length))
145
+ self.cursor_position = (x,y)
146
+ return self
147
+
148
+ def get_cursor_rect(self) -> pygame.FRect:
149
+ if not self.text_widget.font_object:
150
+ return pygame.FRect(0, 0, 0, 0)
151
+ font = self.text_widget.font_object
152
+
153
+ lines = self.text_widget.text.split('\n')
154
+ line_x, line_y = self.cursor_position
155
+ line = lines[line_y]
156
+
157
+ # # Clamp line_y and line_x to valid ranges
158
+ # line_y = max(0, min(line_y, len(lines) - 1))
159
+ # line_x = max(0, min(line_x, len(line)))
160
+
161
+ line_height = font.get_linesize()
162
+
163
+ # Calculate the pixel x position of the cursor in the current line
164
+ x = font.size(line[:line_x])[0]
165
+ y = line_height * line_y
166
+
167
+ if self.text_widget.show_text_outline:
168
+ offset = self.text_widget._get_outline_offset()
169
+ x+=offset[0]
170
+ y+=offset[1]
171
+
172
+ res = pygame.FRect(x,y,1,line_height)
173
+ return res
174
+
175
+ def ensure_cursor_visible(self):
176
+ pass
177
+
178
+ def cursor_to_absolute(self, position: tuple[int, int]) -> int:
179
+ x, y = position
180
+
181
+ y = max(0, min(y, len(self.text_widget.text.split('\n')) - 1))
182
+ lines = self.text_widget.text.split('\n')
183
+ x = max(0, min(x, len(lines[y])))
184
+
185
+ absolute_position = sum(len(line) + 1 for line in lines[:y]) + x
186
+ return absolute_position
187
+
188
+ def absolute_to_cursor(self, absolute: int) -> tuple[int, int]:
189
+ text = self.text_widget.text
190
+ lines = text.split('\n')
191
+ current_pos = 0
192
+
193
+ for line_no, line in enumerate(lines):
194
+ if absolute <= current_pos + len(line):
195
+ return (absolute - current_pos, line_no)
196
+ current_pos += len(line) + 1
197
+
198
+ return (len(lines[-1]), len(lines) - 1)
199
+
200
+ def handle_event(self, event):
201
+ # TODO fix tab_focus not working when textInput in focus
202
+ super().handle_event(event)
203
+ if event.consumed or(not self.is_focused or event.type not in [pygame.TEXTINPUT, pygame.KEYDOWN]):
204
+ return
205
+
206
+ text = self.get_text()
207
+ current_pos = self.cursor_to_absolute(self.cursor_position)
208
+ pressed = pygame.key.get_pressed()
209
+
210
+ if event.type == pygame.TEXTINPUT:
211
+ # Insert text at the current cursor position
212
+ self.set_text(f"{text[:current_pos]}{event.text}{text[current_pos:]}")
213
+ self.set_cursor_position(self.absolute_to_cursor(current_pos + len(event.text)))
214
+ elif event.type == pygame.KEYDOWN:
215
+ match event.key:
216
+ case pygame.K_ESCAPE:
217
+ self.lose_focus()
218
+
219
+ case pygame.K_BACKSPACE if current_pos > 0:
220
+ # Remove the character before the cursor
221
+ delta = current_pos-1
222
+ if pressed[pygame.K_LCTRL] or pressed[pygame.K_RCTRL]:
223
+ delta = find_prev_word(self.text_widget.text,current_pos-1)
224
+ if delta <0: delta = 0
225
+
226
+ self.set_text(f"{text[:delta]}{text[current_pos:]}")
227
+ self.set_cursor_position(self.absolute_to_cursor(delta))
228
+ self._cursor_toggle(True)
229
+
230
+ case pygame.K_DELETE if current_pos < len(text):
231
+ # Remove the character at the cursor
232
+ self.set_text(f"{text[:current_pos]}{text[current_pos + 1:]}")
233
+ self._cursor_toggle(True)
234
+
235
+ case pygame.K_RIGHT:
236
+ if current_pos < len(text):
237
+ self.handle_cursor_movement(pressed, current_pos, direction="right")
238
+ self._cursor_toggle(True)
239
+
240
+ case pygame.K_LEFT:
241
+ if current_pos > 0:
242
+ self.handle_cursor_movement(pressed, current_pos, direction="left")
243
+ self._cursor_toggle(True)
244
+
245
+ case pygame.K_UP:
246
+ # Move cursor up one line
247
+ self.set_cursor_position((self.cursor_position[0], self.cursor_position[1] - 1))
248
+ self._cursor_toggle(True)
249
+
250
+ case pygame.K_DOWN:
251
+ # Move cursor down one line
252
+ self.set_cursor_position((self.cursor_position[0], self.cursor_position[1] + 1))
253
+ self._cursor_toggle(True)
254
+
255
+ case pygame.K_RETURN:
256
+ # Insert a newline at the current cursor position
257
+ self.set_text(f"{text[:current_pos]}\n{text[current_pos:]}")
258
+ self.set_cursor_position(self.absolute_to_cursor(current_pos + 1))
259
+ self._cursor_toggle(True)
260
+ case _ :
261
+ if event.unicode:
262
+ event.consumed = True
263
+ return
264
+
265
+ event.consumed = True
266
+
267
+ def handle_cursor_movement(self, pressed, current_pos, direction):
268
+ if direction == "right":
269
+ if pressed[pygame.K_LCTRL] or pressed[pygame.K_RCTRL]:
270
+ next_word_pos = find_next_word(self.text_widget.text, current_pos)
271
+ self.set_cursor_position(self.absolute_to_cursor(next_word_pos if next_word_pos != -1 else current_pos + 1))
272
+ else:
273
+ self.set_cursor_position(self.absolute_to_cursor(current_pos + 1))
274
+ elif direction == "left":
275
+ if pressed[pygame.K_LCTRL] or pressed[pygame.K_RCTRL]:
276
+ prev_word_pos = find_prev_word(self.text_widget.text, current_pos - 1)
277
+ self.set_cursor_position(self.absolute_to_cursor(prev_word_pos if prev_word_pos != -1 else current_pos - 1))
278
+ else:
279
+ self.set_cursor_position(self.absolute_to_cursor(current_pos - 1))
280
+
281
+
282
+ def set_text(self, text: str) -> Self:
283
+ if self.on_modify:
284
+ text = self.on_modify(text)
285
+ if text == "" and self.placeholder_text:
286
+ text = self.placeholder_text
287
+ if text != "" and text == self.placeholder_text:
288
+ self.text_widget.set_text("")
289
+ self.text_widget.set_text(text)
290
+
291
+ return self
292
+ def _draw_cursor(self,camera:bf.Camera) -> None:
293
+ if not self.show_cursor or not self._cursor_blink_show:
294
+ return
295
+
296
+ cursor_rect = self.get_cursor_rect()
297
+ cursor_rect.move_ip(*self.text_widget.rect.topleft)
298
+ cursor_rect = camera.world_to_screen(cursor_rect)
299
+ if self.text_widget.show_text_outline:
300
+ pygame.draw.rect(camera.surface, self.text_widget.text_outline_color, cursor_rect.inflate(2,2))
301
+ pygame.draw.rect(camera.surface, self.text_widget.text_color, cursor_rect)
302
+
303
+ def draw(self,camera:bf.Camera) -> None:
304
+ super().draw(camera)
305
+ self._draw_cursor(camera)
306
+
307
+ def align_text(
308
+ self, text_rect: pygame.FRect, area: pygame.FRect, alignment: bf.alignment
309
+ ):
310
+ cursor_rect = self.get_cursor_rect()
311
+
312
+ if alignment == bf.alignment.LEFT:
313
+ alignment = bf.alignment.MIDLEFT
314
+ elif alignment == bf.alignment.MIDRIGHT:
315
+ alignment = bf.alignment.MIDRIGHT
316
+ pos = area.__getattribute__(alignment.value)
317
+ text_rect.__setattr__(alignment.value, pos)
318
+ scroll = self.text_widget.scroll
319
+
320
+ if cursor_rect.right > area.right+scroll.x:
321
+ scroll.x=cursor_rect.right - area.right
322
+ elif cursor_rect.x < scroll.x+area.left:
323
+ scroll.x= cursor_rect.left - area.left
324
+ # self.scroll.x = 0
325
+ scroll.x = max(scroll.x,0)
326
+
327
+ if cursor_rect.bottom > scroll.y + area.bottom:
328
+ scroll.y = cursor_rect.bottom - area.bottom
329
+ elif cursor_rect.y < scroll.y + area.top:
330
+ scroll.y = cursor_rect.top - area.top
331
+ scroll.y = max(scroll.y, 0)