batframework 1.1.0__py3-none-any.whl → 2.0.0a1__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 (81) hide show
  1. batFramework/__init__.py +84 -52
  2. batFramework/action.py +280 -252
  3. batFramework/actionContainer.py +105 -38
  4. batFramework/animatedSprite.py +81 -117
  5. batFramework/animation.py +91 -0
  6. batFramework/audioManager.py +156 -85
  7. batFramework/baseScene.py +249 -0
  8. batFramework/camera.py +245 -123
  9. batFramework/constants.py +57 -75
  10. batFramework/cutscene.py +239 -119
  11. batFramework/cutsceneManager.py +34 -0
  12. batFramework/drawable.py +107 -0
  13. batFramework/dynamicEntity.py +30 -23
  14. batFramework/easingController.py +58 -0
  15. batFramework/entity.py +130 -123
  16. batFramework/enums.py +171 -0
  17. batFramework/fontManager.py +65 -0
  18. batFramework/gui/__init__.py +28 -14
  19. batFramework/gui/animatedLabel.py +90 -0
  20. batFramework/gui/button.py +18 -84
  21. batFramework/gui/clickableWidget.py +244 -0
  22. batFramework/gui/collapseContainer.py +98 -0
  23. batFramework/gui/constraints/__init__.py +1 -0
  24. batFramework/gui/constraints/constraints.py +1066 -0
  25. batFramework/gui/container.py +220 -49
  26. batFramework/gui/debugger.py +140 -47
  27. batFramework/gui/draggableWidget.py +63 -0
  28. batFramework/gui/image.py +61 -23
  29. batFramework/gui/indicator.py +116 -40
  30. batFramework/gui/interactiveWidget.py +243 -22
  31. batFramework/gui/label.py +147 -110
  32. batFramework/gui/layout.py +442 -81
  33. batFramework/gui/meter.py +155 -0
  34. batFramework/gui/radioButton.py +43 -0
  35. batFramework/gui/root.py +228 -60
  36. batFramework/gui/scrollingContainer.py +282 -0
  37. batFramework/gui/selector.py +232 -0
  38. batFramework/gui/shape.py +286 -86
  39. batFramework/gui/slider.py +353 -0
  40. batFramework/gui/style.py +10 -0
  41. batFramework/gui/styleManager.py +49 -0
  42. batFramework/gui/syncedVar.py +43 -0
  43. batFramework/gui/textInput.py +331 -0
  44. batFramework/gui/textWidget.py +308 -0
  45. batFramework/gui/toggle.py +140 -62
  46. batFramework/gui/tooltip.py +35 -0
  47. batFramework/gui/widget.py +546 -307
  48. batFramework/manager.py +131 -50
  49. batFramework/particle.py +118 -0
  50. batFramework/propertyEaser.py +79 -0
  51. batFramework/renderGroup.py +34 -0
  52. batFramework/resourceManager.py +130 -0
  53. batFramework/scene.py +31 -226
  54. batFramework/sceneLayer.py +134 -0
  55. batFramework/sceneManager.py +200 -165
  56. batFramework/scrollingSprite.py +115 -0
  57. batFramework/sprite.py +46 -0
  58. batFramework/stateMachine.py +49 -51
  59. batFramework/templates/__init__.py +2 -0
  60. batFramework/templates/character.py +15 -0
  61. batFramework/templates/controller.py +158 -0
  62. batFramework/templates/stateMachine.py +39 -0
  63. batFramework/tileset.py +46 -0
  64. batFramework/timeManager.py +213 -0
  65. batFramework/transition.py +162 -157
  66. batFramework/triggerZone.py +22 -22
  67. batFramework/utils.py +306 -184
  68. {batframework-1.1.0.dist-info → batframework-2.0.0a1.dist-info}/LICENSE +20 -20
  69. {batframework-1.1.0.dist-info → batframework-2.0.0a1.dist-info}/METADATA +7 -2
  70. batframework-2.0.0a1.dist-info/RECORD +72 -0
  71. batFramework/cutsceneBlocks.py +0 -176
  72. batFramework/debugger.py +0 -48
  73. batFramework/easing.py +0 -71
  74. batFramework/gui/constraints.py +0 -204
  75. batFramework/gui/frame.py +0 -19
  76. batFramework/particles.py +0 -77
  77. batFramework/time.py +0 -75
  78. batFramework/transitionManager.py +0 -0
  79. batframework-1.1.0.dist-info/RECORD +0 -43
  80. {batframework-1.1.0.dist-info → batframework-2.0.0a1.dist-info}/WHEEL +0 -0
  81. {batframework-1.1.0.dist-info → batframework-2.0.0a1.dist-info}/top_level.txt +0 -0
@@ -0,0 +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.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)
@@ -0,0 +1,308 @@
1
+ from math import ceil
2
+ import pygame
3
+ from .widget import Widget
4
+ import batFramework as bf
5
+ from typing import Literal, Self,Union
6
+
7
+ class TextWidget(Widget):
8
+ def __init__(self, text:str):
9
+ super().__init__()
10
+ self.text = text
11
+
12
+ # Allows scrolling the text
13
+ self.allow_scroll : bool = True
14
+
15
+ # Scroll variable
16
+ # TODO make scroll work
17
+ self.scroll :pygame.Vector2 = pygame.Vector2(0,0)
18
+
19
+ # Enable/Disable antialiasing
20
+ self.antialias: bool = bf.FontManager().DEFAULT_ANTIALIAS
21
+
22
+ self.text_size = bf.FontManager().DEFAULT_FONT_SIZE
23
+
24
+ self.auto_wraplength: bool = False
25
+
26
+ self.text_color: tuple[int, int, int] | str = "black"
27
+
28
+ self.text_bg_color : tuple[int,int,int]| str | None = None
29
+
30
+
31
+ self.text_outline_color: tuple[int, int, int] | str = "gray50"
32
+
33
+ self._text_outline_mask = pygame.Mask((3, 3), fill=True)
34
+
35
+ self.line_alignment = pygame.FONT_LEFT
36
+ # font name (given when loaded by utils) to use for the text
37
+
38
+ self.font_name = None
39
+ # reference to the font object
40
+ self.font_object = None
41
+ # Rect containing the text of the label
42
+ self.show_text_outline: bool = False
43
+
44
+ self.is_italic: bool = False
45
+
46
+ self.is_bold: bool = False
47
+
48
+ self.is_underlined: bool = False
49
+
50
+ super().__init__()
51
+ self.set_debug_color("purple")
52
+ self.set_autoresize(True)
53
+ self.set_font(force=True)
54
+ self.set_convert_alpha(True)
55
+
56
+
57
+ def set_padding(self, value): # can't set padding
58
+ return self
59
+
60
+ def __str__(self) -> str:
61
+ return f"TextWidget({repr(self.text)})"
62
+
63
+ def set_allow_scroll(self, value:bool)->Self:
64
+ if self.allow_scroll == value: return self
65
+ self.allow_scroll = value
66
+ self.dirty_surface = True
67
+ return self
68
+
69
+ def set_scroll(self,x=None,y=None)->Self:
70
+ x = x if x is not None else self.scroll.x
71
+ y = y if y is not None else self.scroll.y
72
+
73
+ self.scroll.update(x,y)
74
+ self.dirty_surface = True
75
+ return self
76
+
77
+ def scroll_by(self,x=0,y=0)->Self:
78
+ self.scroll += x,y
79
+ self.dirty_surface = True
80
+ return self
81
+
82
+ def set_text_color(self, color) -> Self:
83
+ self.text_color = color
84
+ self.dirty_surface = True
85
+ return self
86
+
87
+ def set_text_bg_color(self, color) -> Self:
88
+ self.text_bg_color = color
89
+ self.set_convert_alpha(color is None)
90
+ self.dirty_surface = True
91
+ return self
92
+
93
+
94
+ def top_at(self, x, y):
95
+ return None
96
+
97
+ def set_line_alignment(self, alignment: Union[Literal["left"], Literal["right"], Literal["center"]]) -> Self:
98
+ self.line_alignment = alignment
99
+ self.dirty_surface = True
100
+ return self
101
+
102
+ def set_italic(self, value: bool) -> Self:
103
+ if value == self.is_italic:
104
+ return self
105
+ self.is_italic = value
106
+ if self.autoresize_h or self.autoresize_w:
107
+ self.dirty_shape = True
108
+ else:
109
+ self.dirty_surface = True
110
+ return self
111
+
112
+ def set_bold(self, value: bool) -> Self:
113
+ if value == self.is_bold:
114
+ return self
115
+ self.is_bold = value
116
+ if self.autoresize_h or self.autoresize_w:
117
+ self.dirty_shape = True
118
+ else:
119
+ self.dirty_surface = True
120
+ return self
121
+
122
+ def set_underlined(self, value: bool) -> Self:
123
+ if value == self.is_underlined:
124
+ return self
125
+ self.is_underlined = value
126
+ self.dirty_surface = True
127
+ return self
128
+
129
+ def set_text_outline_mask_size(self,size:tuple[int,int])->Self:
130
+ old_size = self._text_outline_mask.get_size()
131
+ min_w, min_h = min(old_size[0], size[0]), min(old_size[1], size[1])
132
+ m = [
133
+ [self._text_outline_mask.get_at((x, y)) for x in range(min_w)]
134
+ for y in range(min_h)
135
+ ]
136
+ self._text_outline_mask = pygame.Mask(size, fill=True)
137
+ self.set_text_outline_matrix(m)
138
+ return self
139
+
140
+ def set_text_outline_matrix(self, matrix: list[list[0 | 1]]) -> Self:
141
+ if matrix is None:
142
+ matrix = [[0 for _ in range(3)] for _ in range(3)]
143
+ for y in range(3):
144
+ for x in range(3):
145
+ self._text_outline_mask.set_at((x, y), matrix[2 - y][2 - x])
146
+ self.dirty_shape = True
147
+ return self
148
+
149
+ def set_text_outline_color(self, color) -> Self:
150
+ self.text_outline_color = color
151
+ self.dirty_surface = True
152
+ return self
153
+
154
+ def set_show_text_outline(self,value:bool) -> Self:
155
+ self.show_text_outline = value
156
+ self.dirty_shape = True
157
+ return self
158
+
159
+ def set_auto_wraplength(self, val: bool) -> Self:
160
+ self.auto_wraplength = val
161
+ if self.autoresize_h or self.autoresize_w:
162
+ self.dirty_shape = True
163
+ else:
164
+ self.dirty_surface = True
165
+ return self
166
+
167
+ def get_debug_outlines(self):
168
+ if self.visible:
169
+ yield from super().get_debug_outlines()
170
+
171
+ def set_font(self, font_name: str = None, force: bool = False) -> Self:
172
+ if font_name == self.font_name and not force:
173
+ return self
174
+ self.font_name = font_name
175
+ self.font_object = bf.FontManager().get_font(self.font_name, self.text_size)
176
+ if self.autoresize_h or self.autoresize_w:
177
+ self.dirty_shape = True
178
+ else:
179
+ self.dirty_surface = True
180
+ return self
181
+
182
+ def set_text_size(self, text_size: int) -> Self:
183
+ text_size = (text_size // 2) * 2
184
+ if text_size == self.text_size:
185
+ return self
186
+ self.text_size = text_size
187
+ self.font_object = bf.FontManager().get_font(self.font_name, self.text_size)
188
+ self.dirty_shape = True
189
+ return self
190
+
191
+ def get_text_size(self) -> int:
192
+ return self.text_size
193
+
194
+
195
+ def is_antialias(self) -> bool:
196
+ return self.antialias
197
+
198
+ def set_antialias(self, value: bool) -> Self:
199
+ self.antialias = value
200
+ self.dirty_surface = True
201
+ return self
202
+
203
+ def set_text(self, text: str) -> Self:
204
+ if text == self.text:
205
+ return self
206
+ self.text = text
207
+ self.dirty_shape = True
208
+ return self
209
+
210
+ def get_min_required_size(self) -> tuple[float, float]:
211
+ if not self.font_object : return 0,0
212
+
213
+ tmp_text = self.text
214
+ if self.text.endswith('\n'):
215
+ tmp_text+=" " # hack to have correct size if ends with newline
216
+ params = {
217
+ "font_name": self.font_object.name,
218
+ "text": tmp_text,
219
+ "antialias": self.antialias,
220
+ "color": self.text_color,
221
+ "bgcolor": self.text_bg_color,
222
+ "wraplength": int(self.get_inner_width()) if self.auto_wraplength and not self.autoresize_w else 0,
223
+ }
224
+
225
+ size = list(self._render_font(params).get_size())
226
+ size[1]= max(size[1],self.font_object.get_ascent() - self.font_object.get_descent())
227
+ if not self.show_text_outline:
228
+ return size
229
+ s = self._get_outline_offset()
230
+ return size[0] + s[0]*2, size[1] + s[1]*2
231
+
232
+
233
+ def get_text(self) -> str:
234
+ return self.text
235
+
236
+ def _render_font(self, params: dict) -> pygame.Surface:
237
+ params.pop("font_name")
238
+ # save old settings
239
+ old_italic = self.font_object.get_italic()
240
+ old_bold = self.font_object.get_bold()
241
+ old_underline = self.font_object.get_underline()
242
+ old_align = self.font_object.align
243
+ # setup font
244
+ self.font_object.set_italic(self.is_italic)
245
+ self.font_object.set_bold(self.is_bold)
246
+ self.font_object.set_underline(self.is_underlined)
247
+ self.font_object.align = self.line_alignment
248
+ surf = self.font_object.render(**params)
249
+ # reset font
250
+ self.font_object.set_italic(old_italic)
251
+ self.font_object.set_bold(old_bold)
252
+ self.font_object.set_underline(old_underline)
253
+ self.font_object.align = old_align
254
+ return surf
255
+
256
+ def _get_outline_offset(self)->tuple[int,int]:
257
+ mask_size = self._text_outline_mask.get_size()
258
+ return mask_size[0]//2,mask_size[1]//2
259
+
260
+
261
+ def build(self) -> bool:
262
+ """
263
+ return True if size changed
264
+ """
265
+ target_size = self.resolve_size(self.get_min_required_size())
266
+ if self.rect.size != target_size:
267
+ self.set_size(target_size)
268
+ return True
269
+ return False
270
+
271
+
272
+
273
+ def paint(self) -> None:
274
+ self._resize_surface()
275
+ if self.font_object is None:
276
+ print(f"No font for widget with text : '{self}' :(")
277
+ return
278
+
279
+
280
+ wrap = int(self.get_inner_width()) if self.auto_wraplength and not self.autoresize_w else 0
281
+ params = {
282
+ "font_name": self.font_object.name,
283
+ "text": self.text,
284
+ "antialias": self.antialias,
285
+ "color": self.text_color,
286
+ "bgcolor": self.text_bg_color if not self.show_text_outline else None,
287
+ "wraplength": wrap,
288
+ }
289
+
290
+ if self.text_bg_color is None :
291
+ self.surface = self.surface.convert_alpha()
292
+
293
+ bg_fill_color = (0, 0, 0, 0) if self.text_bg_color is None else self.text_bg_color
294
+ self.surface.fill(bg_fill_color)
295
+
296
+ text_surf = self._render_font(params)
297
+
298
+ if self.show_text_outline:
299
+ mask = pygame.mask.from_surface(text_surf).convolve(self._text_outline_mask)
300
+ outline_surf = mask.to_surface(
301
+ setcolor=self.text_outline_color,
302
+ unsetcolor=bg_fill_color
303
+ )
304
+
305
+ outline_surf.blit(text_surf,self._get_outline_offset())
306
+ text_surf = outline_surf
307
+
308
+ self.surface.blit(text_surf, -self.scroll)