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
@@ -18,10 +18,11 @@ def children_has_focus(widget)->bool:
18
18
 
19
19
 
20
20
  class InteractiveWidget(Widget):
21
+ __focus_effect_cache = {}
21
22
  def __init__(self, *args, **kwargs) -> None:
22
23
  self.is_focused: bool = False
23
24
  self.is_hovered: bool = False
24
- self.is_clicked_down: bool = False
25
+ self.is_clicked_down: list[bool] = [False]*5
25
26
  self.focused_index = 0
26
27
  self.focusable = True
27
28
  super().__init__(*args, **kwargs)
@@ -56,7 +57,7 @@ class InteractiveWidget(Widget):
56
57
 
57
58
  def on_get_focus(self) -> None:
58
59
  self.is_focused = True
59
- if isinstance(self.parent,bf.Container):
60
+ if isinstance(self.parent,bf.gui.Container):
60
61
  self.parent.layout.scroll_to_widget(self)
61
62
  self.do_on_get_focus()
62
63
 
@@ -64,98 +65,127 @@ class InteractiveWidget(Widget):
64
65
  self.is_focused = False
65
66
  self.do_on_lose_focus()
66
67
 
67
- def focus_next_tab(self, previous_widget):
68
-
69
- if previous_widget != self and self.visible:
70
- if (
71
- isinstance(self, InteractiveWidget)
72
- and not isinstance(self, bf.Container)
73
- and self.allow_focus_to_self()
74
- ):
75
- self.focus_next_sibling()
76
- return
77
- i_children = [
78
- c
79
- for c in self.children
80
- if isinstance(c, InteractiveWidget) and c.visible
81
- ]
82
- if i_children:
83
- index = i_children.index(previous_widget)
84
- if index < len(i_children) - 1:
85
-
86
- i_children[index + 1].get_focus()
87
- return
88
-
89
- if self.parent and isinstance(self.parent,InteractiveWidget):
90
- self.parent.focus_next_tab(self)
68
+
91
69
 
92
- def focus_prev_tab(self, previous_widget):
93
- if previous_widget != self and self.visible:
94
- if (
95
- isinstance(self, InteractiveWidget)
96
- and not isinstance(self, bf.Container)
97
- and self.allow_focus_to_self()
98
- ):
99
- self.get_focus()
100
- return
101
- i_children = [
102
- c
103
- for c in self.children
104
- if isinstance(c, InteractiveWidget) and c.visible
105
- ]
106
-
107
- if i_children:
108
- index = i_children.index(previous_widget)
109
- if index > 0:
110
- i_children[index - 1].get_focus()
111
- return
112
-
113
- if self.parent and isinstance(self.parent,InteractiveWidget):
114
- self.parent.focus_prev_tab(self)
70
+ def get_interactive_widgets(self):
71
+ """Retrieve all interactive widgets in the tree, in depth-first order."""
72
+ widgets = []
73
+ stack = [self]
74
+ while stack:
75
+ widget = stack.pop()
76
+ if isinstance(widget, InteractiveWidget) and widget.allow_focus_to_self():
77
+ widgets.append(widget)
78
+ stack.extend(reversed(widget.children)) # Add children in reverse for left-to-right traversal
79
+ return widgets
80
+
81
+ def find_next_widget(self, current):
82
+ """Find the next interactive widget, considering parent and sibling relationships."""
83
+ if current.is_root:
84
+ return None # Root has no parent
85
+
86
+ siblings = current.parent.children
87
+ start_index = siblings.index(current)
88
+ good_index = -1
89
+ for i in range(start_index + 1, len(siblings)):
90
+ if isinstance(siblings[i], InteractiveWidget) and siblings[i].allow_focus_to_self():
91
+ good_index = i
92
+ break
93
+ if good_index >= 0:
94
+ # Not the last child, return the next sibling
95
+ return siblings[good_index]
96
+ else:
97
+ # Current is the last child, move to parent's next sibling
98
+ return self.find_next_widget(current.parent)
99
+
100
+ def find_prev_widget(self, current : "Widget"):
101
+ """Find the previous interactive widget, considering parent and sibling relationships."""
102
+ if current.is_root:
103
+ return None # Root has no parent
104
+
105
+ # siblings = [c for c in current.parent.children if isinstance(c,InteractiveWidget) and c.allow_focus_to_self()]
106
+ siblings = current.parent.children
107
+ start_index = siblings.index(current)
108
+ good_index = -1
109
+ for i in range(start_index-1,-1,-1):
110
+ sibling = siblings[i]
111
+ if isinstance(sibling,InteractiveWidget):
112
+ if sibling.allow_focus_to_self():
113
+ good_index = i
114
+ break
115
+ if good_index >= 0:
116
+ # Not the first child, return the previous sibling
117
+ return siblings[good_index]
118
+ else:
119
+ # Current is the first child, move to parent's previous sibling
120
+ return self.find_prev_widget(current.parent)
115
121
 
116
- def on_key_down(self, key) -> bool:
117
- if key == pygame.K_TAB and self.parent:
118
- keys = pygame.key.get_pressed()
119
- if keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]:
122
+ def focus_next_tab(self, previous_widget):
123
+ """Focus the next interactive widget."""
124
+ if previous_widget:
125
+ next_widget = self.find_next_widget(previous_widget)
126
+ if next_widget:
127
+ next_widget.get_focus()
120
128
 
121
- self.focus_prev_tab(self)
122
- else:
123
- self.focus_next_tab(self)
124
- return True
125
- else:
129
+ def focus_prev_tab(self, previous_widget):
130
+ """Focus the previous interactive widget."""
131
+ if previous_widget:
132
+ prev_widget = self.find_prev_widget(previous_widget)
133
+ if prev_widget:
134
+ prev_widget.get_focus()
126
135
 
127
- return self.do_on_key_down(key)
128
136
 
129
- return False
137
+
138
+ def on_key_down(self, key) -> bool:
139
+ """
140
+ return True to stop event propagation
141
+ """
142
+ return self.do_on_key_down(key)
130
143
 
131
144
  def on_key_up(self, key) -> bool:
145
+ """
146
+ return True to stop event propagation
147
+ """
132
148
  return self.do_on_key_up(key)
133
149
 
134
- def do_on_key_down(self, key) -> bool:
135
- return False
136
-
137
- def do_on_key_up(self, key) -> bool:
138
- return False
139
-
140
150
  def do_on_get_focus(self) -> None:
141
151
  pass
142
152
 
143
153
  def do_on_lose_focus(self) -> None:
144
154
  pass
145
155
 
156
+ def do_on_key_down(self, key) -> bool:
157
+ """
158
+ return True to stop event propagation
159
+ """
160
+ return False
161
+
162
+ def do_on_key_up(self, key) -> bool:
163
+ """
164
+ return True to stop event propagation
165
+ """
166
+ return False
167
+
146
168
  def on_click_down(self, button: int) -> bool:
147
- self.is_clicked_down = True
169
+ """
170
+ return True to stop event propagation
171
+ """
172
+ if button < 1 or button > 5 : return False
173
+ self.is_clicked_down[button-1] = True
148
174
  return self.do_on_click_down(button)
149
175
 
150
176
  def on_click_up(self, button: int) -> bool:
151
- self.is_clicked_down = False
177
+ """
178
+ return True to stop event propagation
179
+ """
180
+ if button < 1 or button > 5 : return False
181
+ self.is_clicked_down[button-1] = False
152
182
  return self.do_on_click_up(button)
153
183
 
154
- def do_on_click_down(self, button: int) -> bool:
155
- return False
184
+ def do_on_click_down(self, button: int) -> None:
185
+ return
156
186
 
157
- def do_on_click_up(self, button: int) -> bool:
158
- return False
187
+ def do_on_click_up(self, button: int) -> None:
188
+ return
159
189
 
160
190
  def on_enter(self) -> None:
161
191
  self.is_hovered = True
@@ -163,7 +193,7 @@ class InteractiveWidget(Widget):
163
193
 
164
194
  def on_exit(self) -> None:
165
195
  self.is_hovered = False
166
- self.is_clicked_down = False
196
+ self.is_clicked_down = [False]*5
167
197
  self.do_on_exit()
168
198
 
169
199
  def do_on_enter(self) -> None:
@@ -182,39 +212,28 @@ class InteractiveWidget(Widget):
182
212
  pass
183
213
 
184
214
  def draw_focused(self, camera: bf.Camera) -> None:
215
+ prop = 16
216
+ pulse = int(prop * 0.75 - (prop * cos(pygame.time.get_ticks() / 100) / 4))
217
+ delta = (pulse // 2) * 2 # ensure even
218
+
219
+ # Get rect in screen space, inflated for visual effect
220
+ screen_rect = camera.world_to_screen(self.rect.inflate(prop, prop))
221
+
222
+ # Shrink for inner pulsing border
223
+ inner = screen_rect.inflate(-delta, -delta)
224
+ inner.topleft = 0,0
225
+ inner.w = round(inner.w)
226
+ inner.h = round(inner.h)
227
+
228
+ surface = InteractiveWidget.__focus_effect_cache.get(inner.size)
229
+ if surface is None:
230
+ surface = pygame.Surface(inner.size)
231
+ InteractiveWidget.__focus_effect_cache[inner.size] = surface
232
+
233
+ surface.set_colorkey((0,0,0))
234
+ pygame.draw.rect(surface, "white", inner, 2, *self.border_radius)
235
+ pygame.draw.rect(surface, "black", inner.inflate(-1 * min(16,inner.w*0.75),0), 2)
236
+ pygame.draw.rect(surface, "black", inner.inflate(0,-1 * min(16,inner.h*0.75)), 2)
237
+ inner.center = screen_rect.center
238
+ camera.surface.blit(surface,inner)
185
239
 
186
- proportion = 16
187
- surface = pygame.Surface(self.rect.inflate(proportion,proportion).size)
188
- surface.fill("black")
189
-
190
- delta = proportion*0.75 - int(proportion * cos(pygame.time.get_ticks() / 100) /4)
191
- delta = delta//2 * 2
192
- # Base rect centered in tmp surface
193
- base_rect = surface.get_frect()
194
-
195
- # Expanded white rectangle for border effect
196
- white_rect = base_rect.inflate(-delta,-delta)
197
- white_rect.center = base_rect.center
198
- pygame.draw.rect(surface, "white", white_rect, 2, *self.border_radius)
199
-
200
- # Black cutout rectangles to create the effect around the edges
201
- black_rect_1 = white_rect.copy()
202
- black_rect_1.w -= proportion
203
- black_rect_1.centerx = white_rect.centerx
204
-
205
- black_rect_2 = white_rect.copy()
206
- black_rect_2.h -= proportion
207
- black_rect_2.centery = white_rect.centery
208
-
209
- surface.fill("black", black_rect_1)
210
- surface.fill("black", black_rect_2)
211
-
212
- base_rect.center = self.rect.center
213
-
214
- surface.set_colorkey("black")
215
-
216
- # Blit the tmp surface onto the camera surface with adjusted position
217
- camera.surface.blit(
218
- surface,
219
- base_rect.move(-camera.rect.x, -camera.rect.y),
220
- )
batFramework/gui/label.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import batFramework as bf
2
2
  import pygame
3
3
  from .shape import Shape
4
- from typing import Self
4
+ from typing import Literal, Self,Union
5
5
  from math import ceil
6
6
 
7
7
  class Label(Shape):
@@ -10,6 +10,10 @@ class Label(Shape):
10
10
  def __init__(self, text: str = "") -> None:
11
11
  self.text = text
12
12
 
13
+ # Allows scrolling the text
14
+ self.allow_scroll : bool = True
15
+
16
+ # Scroll variable
13
17
  self.scroll :pygame.Vector2 = pygame.Vector2(0,0)
14
18
 
15
19
  self.resized_flag: bool = False
@@ -31,6 +35,7 @@ class Label(Shape):
31
35
 
32
36
  self._text_outline_mask = pygame.Mask((3, 3), fill=True)
33
37
 
38
+ self.line_alignment = pygame.FONT_LEFT
34
39
  # font name (given when loaded by utils) to use for the text
35
40
  self.font_name = None
36
41
  # reference to the font object
@@ -59,11 +64,23 @@ class Label(Shape):
59
64
  def __str__(self) -> str:
60
65
  return f"Label({repr(self.text)})"
61
66
 
67
+
68
+ def set_allow_scroll(self, value:bool)->Self:
69
+ if self.allow_scroll == value: return self
70
+ self.allow_scroll = value
71
+ self.dirty_shape = True
72
+ return self
73
+
62
74
  def set_text_color(self, color) -> Self:
63
75
  self.text_color = color
64
76
  self.dirty_surface = True
65
77
  return self
66
78
 
79
+ def set_line_alignment(self, alignment: Union[Literal["left"], Literal["right"], Literal["center"]]) -> Self:
80
+ self.line_alignment = alignment
81
+ self.dirty_surface = True
82
+ return self
83
+
67
84
  def set_italic(self, value: bool) -> Self:
68
85
  if value == self.is_italic:
69
86
  return self
@@ -136,10 +153,12 @@ class Label(Shape):
136
153
  return self
137
154
 
138
155
  def get_debug_outlines(self):
139
- if self.visible:
140
- offset = self._get_outline_offset() if self.show_text_outline else (0,0)
141
- yield (self.text_rect.move(self.rect.x - offset[0] - self.scroll.x,self.rect.y - offset[1] - self.scroll.y), "purple")
142
156
  yield from super().get_debug_outlines()
157
+ if self.visible:
158
+ yield (self.text_rect.move(self.rect.x - self.scroll.x,self.rect.y - self.scroll.y), "purple")
159
+
160
+ # offset = self._get_outline_offset() if self.show_text_outline else (0,0)
161
+ # yield (self.text_rect.move(self.rect.x - offset[0] - self.scroll.x,self.rect.y - offset[1] - self.scroll.y), "purple")
143
162
 
144
163
  def set_font(self, font_name: str = None, force: bool = False) -> Self:
145
164
  if font_name == self.font_name and not force:
@@ -153,13 +172,12 @@ class Label(Shape):
153
172
  return self
154
173
 
155
174
  def set_text_size(self, text_size: int) -> Self:
156
- text_size = round(text_size / 2) * 2
175
+ text_size = (text_size // 2) * 2
157
176
  if text_size == self.text_size:
158
177
  return self
159
178
  self.text_size = text_size
160
179
  self.font_object = bf.FontManager().get_font(self.font_name, self.text_size)
161
- if self.autoresize_h or self.autoresize_w:
162
- self.dirty_shape = True
180
+ self.dirty_shape = True
163
181
  return self
164
182
 
165
183
  def get_text_size(self) -> int:
@@ -181,15 +199,9 @@ class Label(Shape):
181
199
  return self
182
200
 
183
201
  def get_min_required_size(self) -> tuple[float, float]:
184
- if not (self.autoresize_w or self.autoresize_h):
185
- return self.rect.size
186
- if not self.text_rect:
187
- self.text_rect.size = self._get_text_rect_required_size()
188
- res = self.inflate_rect_by_padding((0, 0, *self.text_rect.size)).size
189
-
190
- return res[0] if self.autoresize_w else self.rect.w, (
191
- res[1] if self.autoresize_h else self.rect.h
192
- )
202
+ return self.expand_rect_with_padding(
203
+ (0, 0, *self._get_text_rect_required_size())
204
+ ).size
193
205
 
194
206
  def get_text(self) -> str:
195
207
  return self.text
@@ -203,28 +215,32 @@ class Label(Shape):
203
215
  old_italic = self.font_object.get_italic()
204
216
  old_bold = self.font_object.get_bold()
205
217
  old_underline = self.font_object.get_underline()
206
-
218
+ old_align = self.font_object.align
207
219
  # setup font
208
220
  self.font_object.set_italic(self.is_italic)
209
221
  self.font_object.set_bold(self.is_bold)
210
222
  self.font_object.set_underline(self.is_underlined)
211
-
223
+ self.font_object.align = self.line_alignment
212
224
  surf = self.font_object.render(**params)
213
225
 
214
226
  # reset font
215
227
  self.font_object.set_italic(old_italic)
216
228
  self.font_object.set_bold(old_bold)
217
229
  self.font_object.set_underline(old_underline)
230
+ self.font_object.align = old_align
218
231
  else:
219
232
  params.pop("font_name")
220
233
  surf = self.font_object.render(**params)
221
234
 
222
235
  return surf
223
-
236
+
237
+ def _get_outline_offset(self)->tuple[int,int]:
238
+ mask_size = self._text_outline_mask.get_size()
239
+ return mask_size[0]//2,mask_size[1]//2
240
+
224
241
  def _get_text_rect_required_size(self):
225
- font_height = self.font_object.get_linesize()
242
+ font_height = self.font_object.get_ascent() - self.font_object.get_descent()
226
243
  if not self.text:
227
- # font_height = self.font_object.get_ascent() - self.font_object.get_ascent()
228
244
  size = (0,font_height)
229
245
  else:
230
246
  tmp_text = self.text
@@ -236,49 +252,43 @@ class Label(Shape):
236
252
  "antialias": self.antialias,
237
253
  "color": self.text_color,
238
254
  "bgcolor": None, # if (self.has_alpha_color() or self.draw_mode == bf.drawMode.TEXTURED) else self.color,
239
- "wraplength": int(self.get_padded_width()) if self.auto_wraplength and not self.autoresize_w else 0,
255
+ "wraplength": int(self.get_inner_width()) if self.auto_wraplength and not self.autoresize_w else 0,
240
256
  }
241
257
 
242
258
  size = self._render_font(params).get_size()
243
259
  size = size[0],max(font_height,size[1])
260
+
261
+ # print(self.text,size)
244
262
  s = self._get_outline_offset() if self.show_text_outline else (0,0)
245
263
  return size[0] + s[0]*2, size[1] + s[1]*2
246
264
 
247
- def _build_layout(self) -> None:
265
+ def _build_layout(self) -> bool:
266
+ ret = False
267
+ target_size = self.resolve_size(self.get_min_required_size())
248
268
 
249
- # print(self.text_rect.size,self._get_text_rect_required_size(),repr(self.text))
250
- self.text_rect.size = self._get_text_rect_required_size()
269
+ if self.rect.size != target_size :
270
+ self.set_size(target_size)
271
+ ret = True
272
+ # self.apply_post_updates(skip_draw=True)
273
+ # return True
251
274
 
252
- if self.autoresize_h or self.autoresize_w:
253
- target_rect = self.inflate_rect_by_padding((0, 0, *self.text_rect.size))
254
- if not self.autoresize_w:
255
- target_rect.w = self.rect.w
256
- if not self.autoresize_h:
257
- target_rect.h = self.rect.h
258
- if self.rect.size != target_rect.size:
259
- # print("Size not good ! ",self.rect.size,target_rect.size,repr(self.text))
260
- self.set_size(target_rect.size)
261
- self.apply_updates()
262
-
263
- offset = self._get_outline_offset() if self.show_text_outline else (0,0)
264
- padded = self.get_padded_rect().move(-self.rect.x + offset[0], -self.rect.y + offset[1])
275
+ self.text_rect.size = self._get_text_rect_required_size()
276
+ padded = self.get_local_inner_rect()
265
277
  self.align_text(self.text_rect, padded, self.alignment)
266
-
267
- def _get_outline_offset(self)->tuple[int,int]:
268
- mask_size = self._text_outline_mask.get_size()
269
- return mask_size[0]//2,mask_size[1]//2
278
+ return ret
270
279
 
271
280
  def _paint_text(self) -> None:
272
281
  if self.font_object is None:
273
282
  print(f"No font for widget with text : '{self}' :(")
274
283
  return
284
+ wrap = int(self.get_inner_width()) if self.auto_wraplength and not self.autoresize_w else 0
275
285
  params = {
276
286
  "font_name": self.font_object.name,
277
287
  "text": self.text,
278
288
  "antialias": self.antialias,
279
289
  "color": self.text_color,
280
- "bgcolor": None, # if (self.has_alpha_color() or self.draw_mode == bf.drawMode.TEXTURED) else self.color,
281
- "wraplength": int(self.get_padded_width()) if self.auto_wraplength and not self.autoresize_w else 0,
290
+ "bgcolor": None,
291
+ "wraplength": wrap,
282
292
  }
283
293
 
284
294
  self.text_surface = self._render_font(params)
@@ -289,27 +299,22 @@ class Label(Shape):
289
299
  .to_surface(setcolor=self.text_outline_color, unsetcolor=(0, 0, 0, 0))
290
300
  )
291
301
 
292
- outline_offset = self._get_outline_offset() if self.show_text_outline else (0,0)
293
-
294
-
295
- # prepare fblit list
296
- l = []
297
- if self.show_text_outline:
298
- l.append(
299
- (self.text_outline_surface,
300
- (self.text_rect.x - outline_offset[0] - self.scroll.x,self.text_rect.y - outline_offset[1] - self.scroll.y))
301
- )
302
- l.append(
303
- (self.text_surface, self.text_rect.move(-self.scroll))
304
- )
302
+ outline_offset = list(self._get_outline_offset())
303
+ outline_offset[0]-=self.scroll.x
304
+ outline_offset[1]-=self.scroll.y
305
+ l = [
306
+ (self.text_outline_surface,(self.text_rect.x - self.scroll.x,self.text_rect.y - self.scroll.y)),
307
+ (self.text_surface, self.text_rect.move(*outline_offset))
308
+ ]
309
+ else:
310
+ l = [(self.text_surface, self.text_rect.move(-self.scroll))]
305
311
 
306
312
  # clip surface
307
313
 
308
314
  old = self.surface.get_clip()
309
- self.surface.set_clip(self.get_padded_rect().move(-self.rect.x,-self.rect.y))
315
+ self.surface.set_clip(self.get_local_inner_rect())
310
316
  self.surface.fblits(l)
311
317
  self.surface.set_clip(old)
312
-
313
318
 
314
319
  def align_text(
315
320
  self, text_rect: pygame.FRect, area: pygame.FRect, alignment: bf.alignment
@@ -323,12 +328,16 @@ class Label(Shape):
323
328
  text_rect.__setattr__(alignment.value, pos)
324
329
  text_rect.y = ceil(text_rect.y)
325
330
 
326
-
327
-
328
- def build(self) -> None:
331
+ def build(self) -> bool:
332
+ """
333
+ return True if size changed
334
+ """
335
+
336
+ size_changed = self._build_layout()
329
337
  super().build()
330
- self._build_layout()
331
-
338
+ return size_changed
339
+
340
+
332
341
  def paint(self) -> None:
333
342
  super().paint()
334
343
  if self.font_object: