batframework 1.0.8a14__py3-none-any.whl → 1.0.9a1__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.
@@ -11,181 +11,172 @@ from pygame.math import Vector2
11
11
  class Container(Shape, InteractiveWidget):
12
12
  def __init__(self, layout: Layout = None, *children: Widget) -> None:
13
13
  super().__init__()
14
- self.dirty_children = False
14
+ self.dirty_layout: bool = False
15
15
  self.set_debug_color("green")
16
- self.layout: Layout = layout
17
- self.scroll = Vector2(0, 0)
18
- if not self.layout:
19
- self.layout = Column()
16
+ self.layout = layout if layout else Column()
20
17
  self.layout.set_parent(self)
18
+ self.scroll = Vector2(0, 0)
21
19
  self.add(*children)
22
20
 
23
21
  def __str__(self) -> str:
24
22
  return f"Container({self.uid},{len(self.children)})"
25
23
 
26
24
  def get_min_required_size(self):
27
- if self.layout:
28
- return self.layout.get_auto_size()
29
- return self.rect.size
25
+ return self.layout.get_auto_size() if self.layout else self.rect.size
30
26
 
31
27
  def reset_scroll(self) -> Self:
28
+ if self.scroll == (0,0):
29
+ return self
32
30
  self.scroll.update(0, 0)
33
- self.dirty_children = True
31
+ self.dirty_layout = True
34
32
  return self
35
33
 
36
34
  def set_scroll(self, value: tuple) -> Self:
35
+ if (self.scroll.x,self.scroll.y) == value:
36
+ return self
37
37
  self.scroll.update(value)
38
- self.dirty_children = True
38
+ self.clamp_scroll()
39
+ self.dirty_layout = True
39
40
  return self
40
41
 
41
42
  def scrollX_by(self, x: float | int) -> Self:
43
+ if x == 0:
44
+ return self
42
45
  self.scroll.x += x
43
- self.dirty_children = True
46
+ self.clamp_scroll()
47
+ self.dirty_layout = True
44
48
  return self
45
49
 
46
50
  def scrollY_by(self, y: float | int) -> Self:
51
+ if y == 0:
52
+ return self
47
53
  self.scroll.y += y
48
- self.dirty_children = True
54
+ self.clamp_scroll()
55
+ self.dirty_layout = True
49
56
  return self
50
57
 
51
58
  def scroll_by(self, value: tuple[float | int, float | int]) -> Self:
59
+ if value[0] == 0 and value[1] == 0:
60
+ return self
52
61
  self.scroll += value
53
- self.dirty_children = True
62
+ self.clamp_scroll()
63
+ self.dirty_layout = True
54
64
  return self
55
65
 
56
66
  def clamp_scroll(self) -> Self:
57
67
  if not self.children:
58
- return Self
68
+ return self
59
69
  r = self.get_padded_rect()
60
- size = self.children[0].rect.unionall(self.children[1:]).size
70
+ size = self.children[0].rect.unionall([child.rect for child in self.children[1:]]).size
61
71
 
62
- # Calculate the maximum scroll values
63
72
  max_scroll_x = max(0, size[0] - r.width)
64
73
  max_scroll_y = max(0, size[1] - r.height)
65
74
 
66
- # Clamp the scroll values
67
75
  self.scroll.x = max(0, min(self.scroll.x, max_scroll_x))
68
76
  self.scroll.y = max(0, min(self.scroll.y, max_scroll_y))
69
-
70
- self.dirty_children = True
77
+ self.dirty_layout = True
71
78
  return self
72
79
 
73
80
  def set_layout(self, layout: Layout) -> Self:
74
81
  tmp = self.layout
75
82
  self.layout = layout
76
83
  if self.layout != tmp:
77
- self.dirty_children = True
84
+ tmp.set_parent(None)
85
+ self.layout.set_parent(self)
86
+ self.dirty_layout = True
78
87
  return self
79
88
 
80
89
  def get_interactive_children(self) -> list[InteractiveWidget]:
81
- return [
82
- child
83
- for child in self.children
84
- if isinstance(child, InteractiveWidget) and child.allow_focus_to_self()
85
- ]
86
-
87
- def focus_next_child(self) -> None:
88
- self.layout.focus_next_child()
89
-
90
- def focus_prev_child(self) -> None:
91
- self.layout.focus_prev_child()
90
+ return [child for child in self.children if isinstance(child, InteractiveWidget) and not isinstance(child,Container) and child.allow_focus_to_self()]
92
91
 
93
92
  def clear_children(self) -> None:
94
93
  self.children.clear()
95
- self.dirty_children = True
94
+ self.dirty_layout = True
96
95
 
97
96
  def add(self, *child: Widget) -> Self:
98
97
  super().add(*child)
99
- self.dirty_children = True
98
+ self.dirty_shape = True
99
+ self.clamp_scroll()
100
100
  return self
101
101
 
102
102
  def remove(self, *child: Widget) -> Self:
103
103
  super().remove(*child)
104
- self.dirty_children = True
104
+ self.dirty_shape = True
105
+ self.clamp_scroll()
105
106
  return self
106
107
 
107
- def resolve_constraints(self) -> None:
108
- super().resolve_constraints()
109
-
110
108
  def top_at(self, x: float | int, y: float | int) -> "None|Widget":
111
109
  if self.visible and self.rect.collidepoint(x, y):
112
- if self.children:
113
- for child in reversed(self.children):
114
- r = child.top_at(x, y)
115
- if r is not None:
116
- return r
110
+ for child in reversed(self.children):
111
+ result = child.top_at(x, y)
112
+ if result is not None:
113
+ return result
117
114
  return self
118
115
  return None
119
116
 
120
117
  def get_focus(self) -> bool:
121
- res = super().get_focus()
122
- if not res:
118
+ if not super().get_focus():
123
119
  return False
124
- l = l = self.get_interactive_children()
125
- if not l:
120
+ interactive_children = self.get_interactive_children()
121
+ if not interactive_children:
126
122
  return True
127
- self.focused_index = min(self.focused_index, len(l))
128
- return l[self.focused_index].get_focus()
123
+ self.focused_index = min(self.focused_index, len(interactive_children) - 1)
124
+ return interactive_children[self.focused_index].get_focus()
129
125
 
130
- def do_handle_event(self, event):
131
- self.layout.handle_event(event)
126
+ def do_handle_event(self, event) -> None:
127
+ if any(child.is_focused for child in self.get_interactive_children()):
128
+ self.layout.handle_event(event)
132
129
 
133
130
  def set_focused_child(self, child: InteractiveWidget) -> bool:
134
- l = self.get_interactive_children()
131
+ interactive_children = self.get_interactive_children()
135
132
  try:
136
- i = l.index(child)
133
+ index = interactive_children.index(child)
134
+ self.focused_index = index
135
+ return True
137
136
  except ValueError:
138
137
  return False
139
- if i >= 0:
140
- self.focused_index = i
141
- return True
142
- return False
143
138
 
144
139
  def allow_focus_to_self(self) -> bool:
145
- return len(self.get_interactive_children()) != 0 and self.visible
140
+ return bool(self.get_interactive_children()) and self.visible
146
141
 
147
- def draw(self, camera: bf.Camera) -> None:
148
- constraints_down = False
142
+
143
+ def apply_updates(self):
144
+ if any(child.dirty_shape for child in self.children):
145
+ self.dirty_layout = True # Mark layout as dirty if any child changed size
146
+
147
+ if self.dirty_constraints:
148
+ self.resolve_constraints() # Finalize positioning based on size
149
+
150
+ # Step 1: Build shape if needed
149
151
  if self.dirty_shape:
150
- self.dirty_constraints = True
151
- self.dirty_children = True
152
- self.dirty_surface = True
153
- self.build()
152
+ self.build() # Finalize size of the container
153
+ self.dirty_shape = False
154
+ self.dirty_surface = True # Mark surface for repaint
155
+ self.dirty_layout = True # Mark layout for arrangement
156
+ # Flag all children to update constraints after size is finalized
154
157
  for child in self.children:
155
158
  child.dirty_constraints = True
156
- self.dirty_shape = False
157
159
 
160
+ for child in self.children:
161
+ child.apply_updates()
162
+
163
+ # Step 2: Arrange layout if marked as dirty
164
+ if self.dirty_layout:
165
+ self.layout.arrange()
166
+ self.dirty_surface = True
167
+ self.dirty_layout = False
168
+ # Constraints may need to adjust based on the layout change
169
+ self.dirty_constraints = True
170
+
171
+ # Step 3: Resolve constraints now that size and layout are finalized
158
172
  if self.dirty_constraints:
159
- if self.parent and self.parent.dirty_constraints:
160
- self.parent.visit_up(self.selective_up)
161
- else:
162
- constraints_down = True
163
- if not self.dirty_children:
164
- self.dirty_children = any(c.dirty_shape for c in self.children)
165
- if self.dirty_children:
166
- if self.layout:
167
- self.layout.arrange()
168
- self.dirty_children = False
169
-
170
- if constraints_down:
171
- self.visit(lambda c: c.resolve_constraints())
173
+ self.resolve_constraints() # Finalize positioning based on size
174
+ for child in self.children:
175
+ child.dirty_constraints = True # Children inherit updated positioning
176
+ self.dirty_constraints = False
172
177
 
178
+ # Step 4: Paint the surface if marked as dirty
173
179
  if self.dirty_surface:
180
+ # print("PAINT !!")
174
181
  self.paint()
175
182
  self.dirty_surface = False
176
-
177
- bf.Drawable.draw(self, camera)
178
-
179
- if self.clip_children:
180
- new_clip = camera.world_to_screen(self.get_padded_rect())
181
- old_clip = camera.surface.get_clip()
182
- new_clip = new_clip.clip(old_clip)
183
- camera.surface.set_clip(new_clip)
184
- # Draw children with adjusted positions
185
- _ = [
186
- child.draw(camera)
187
- for child in sorted(self.children, key=lambda c: c.render_order)
188
- ]
189
-
190
- if self.clip_children:
191
- camera.surface.set_clip(old_clip)
@@ -29,11 +29,11 @@ class Indicator(Shape):
29
29
 
30
30
  class ToggleIndicator(Indicator):
31
31
  def __init__(self, default_value: bool) -> None:
32
+ super().__init__((20, 20))
32
33
  self.value: bool = default_value
33
34
  self.callback = lambda val: self.set_color("green" if val else "red")
34
- super().__init__((20, 20))
35
35
  self.set_value(default_value)
36
-
36
+ self.callback(default_value)
37
37
  # TODO aspect ratio would be good right about here
38
38
  # self.add_constraint(ConstraintAspectRatio(1))
39
39
 
@@ -55,3 +55,4 @@ class ToggleIndicator(Indicator):
55
55
  if r is self:
56
56
  return None
57
57
  return r
58
+
@@ -2,13 +2,13 @@ from .widget import Widget
2
2
  from typing import Self
3
3
  from typing import TYPE_CHECKING
4
4
  import pygame
5
- from math import cos
5
+ from math import cos,floor,ceil
6
6
 
7
7
  if TYPE_CHECKING:
8
8
  from .container import Container
9
9
  import batFramework as bf
10
10
 
11
- def children_has_focus(widget):
11
+ def children_has_focus(widget)->bool:
12
12
  if isinstance(widget,InteractiveWidget) and widget.is_focused:
13
13
  return True
14
14
  for child in widget.children:
@@ -56,6 +56,8 @@ class InteractiveWidget(Widget):
56
56
 
57
57
  def on_get_focus(self) -> None:
58
58
  self.is_focused = True
59
+ if isinstance(self.parent,bf.Container):
60
+ self.parent.layout.scroll_to_widget(self)
59
61
  self.do_on_get_focus()
60
62
 
61
63
  def on_lose_focus(self) -> None:
@@ -84,7 +86,7 @@ class InteractiveWidget(Widget):
84
86
  i_children[index + 1].get_focus()
85
87
  return
86
88
 
87
- if self.parent:
89
+ if self.parent and isinstance(self.parent,InteractiveWidget):
88
90
  self.parent.focus_next_tab(self)
89
91
 
90
92
  def focus_prev_tab(self, previous_widget):
@@ -108,30 +110,18 @@ class InteractiveWidget(Widget):
108
110
  i_children[index - 1].get_focus()
109
111
  return
110
112
 
111
- if self.parent:
113
+ if self.parent and isinstance(self.parent,InteractiveWidget):
112
114
  self.parent.focus_prev_tab(self)
113
115
 
114
- def focus_next_sibling(self) -> None:
115
- if isinstance(self.parent, bf.Container):
116
- self.parent.focus_next_child()
117
-
118
- def focus_prev_sibling(self) -> None:
119
- if isinstance(self.parent, bf.Container):
120
- self.parent.focus_prev_child()
121
-
122
116
  def on_key_down(self, key) -> bool:
123
- if key == pygame.K_DOWN:
124
- self.focus_next_sibling()
125
- elif key == pygame.K_UP:
126
- self.focus_prev_sibling()
127
- elif key == pygame.K_TAB and self.parent:
117
+ if key == pygame.K_TAB and self.parent:
128
118
  keys = pygame.key.get_pressed()
129
119
  if keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]:
130
120
 
131
121
  self.focus_prev_tab(self)
132
122
  else:
133
123
  self.focus_next_tab(self)
134
-
124
+ return True
135
125
  else:
136
126
 
137
127
  return self.do_on_key_down(key)
@@ -192,10 +182,39 @@ class InteractiveWidget(Widget):
192
182
  pass
193
183
 
194
184
  def draw_focused(self, camera: bf.Camera) -> None:
195
- delta = 4 + ((2 * cos(pygame.time.get_ticks() / 100)) // 2) * 2
196
- pygame.draw.rect(
197
- camera.surface,
198
- "white",
199
- self.rect.move(-camera.rect.x, -camera.rect.y),#.inflate(delta, delta),
200
- 1,
185
+
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),
201
220
  )
batFramework/gui/label.py CHANGED
@@ -10,12 +10,14 @@ class Label(Shape):
10
10
  def __init__(self, text: str = "") -> None:
11
11
  self.text = text
12
12
 
13
+ self.scroll :pygame.Vector2 = pygame.Vector2(0,0)
14
+
13
15
  self.resized_flag: bool = False
14
16
 
15
17
  # Enable/Disable antialiasing
16
18
  self.antialias: bool = bf.FontManager().DEFAULT_ANTIALIAS
17
19
 
18
- self.text_size = bf.FontManager().DEFAULT_TEXT_SIZE
20
+ self.text_size = bf.FontManager().DEFAULT_FONT_SIZE
19
21
 
20
22
  self.auto_wraplength: bool = False
21
23
 
@@ -53,6 +55,7 @@ class Label(Shape):
53
55
  self.set_autoresize(True)
54
56
  self.set_font(force=True)
55
57
 
58
+
56
59
  def __str__(self) -> str:
57
60
  return f"Label({repr(self.text)})"
58
61
 
@@ -135,7 +138,7 @@ class Label(Shape):
135
138
  def get_debug_outlines(self):
136
139
  if self.visible:
137
140
  offset = self._get_outline_offset() if self.show_text_outline else (0,0)
138
- yield (self.text_rect.move(self.rect.x - offset[0],self.rect.y - offset[1]), "purple")
141
+ yield (self.text_rect.move(self.rect.x - offset[0] - self.scroll.x,self.rect.y - offset[1] - self.scroll.y), "purple")
139
142
  yield from super().get_debug_outlines()
140
143
 
141
144
  def set_font(self, font_name: str = None, force: bool = False) -> Self:
@@ -183,7 +186,7 @@ class Label(Shape):
183
186
  if not self.text_rect:
184
187
  self.text_rect.size = self._get_text_rect_required_size()
185
188
  res = self.inflate_rect_by_padding((0, 0, *self.text_rect.size)).size
186
-
189
+
187
190
  return res[0] if self.autoresize_w else self.rect.w, (
188
191
  res[1] if self.autoresize_h else self.rect.h
189
192
  )
@@ -217,6 +220,7 @@ class Label(Shape):
217
220
  surf = self.font_object.render(**params)
218
221
 
219
222
  return surf
223
+
220
224
  def _get_text_rect_required_size(self):
221
225
  font_height = self.font_object.get_linesize()
222
226
  if not self.text:
@@ -245,7 +249,6 @@ class Label(Shape):
245
249
  self.text_rect.size = self._get_text_rect_required_size()
246
250
  # self.text_rect.w = ceil(self.text_rect.w)
247
251
  # self.text_rect.h = ceil(self.text_rect.h)
248
-
249
252
  if self.autoresize_h or self.autoresize_w:
250
253
  target_rect = self.inflate_rect_by_padding((0, 0, *self.text_rect.size))
251
254
  if not self.autoresize_w:
@@ -254,7 +257,7 @@ class Label(Shape):
254
257
  target_rect.h = self.rect.h
255
258
  if self.rect.size != target_rect.size:
256
259
  self.set_size(target_rect.size)
257
- self.build()
260
+ self.apply_updates()
258
261
  return
259
262
  offset = self._get_outline_offset() if self.show_text_outline else (0,0)
260
263
  padded = self.get_padded_rect().move(-self.rect.x + offset[0], -self.rect.y + offset[1])
@@ -279,7 +282,6 @@ class Label(Shape):
279
282
  }
280
283
 
281
284
  self.text_surface = self._render_font(params)
282
-
283
285
  if self.show_text_outline:
284
286
  self.text_outline_surface = (
285
287
  pygame.mask.from_surface(self.text_surface)
@@ -287,19 +289,28 @@ class Label(Shape):
287
289
  .to_surface(setcolor=self.text_outline_color, unsetcolor=(0, 0, 0, 0))
288
290
  )
289
291
 
290
- l = []
291
292
  outline_offset = self._get_outline_offset() if self.show_text_outline else (0,0)
292
293
 
294
+
295
+ # prepare fblit list
296
+ l = []
293
297
  if self.show_text_outline:
294
298
  l.append(
295
299
  (self.text_outline_surface,
296
- (self.text_rect.x - outline_offset[0],self.text_rect.y - outline_offset[1]))
300
+ (self.text_rect.x - outline_offset[0] - self.scroll.x,self.text_rect.y - outline_offset[1] - self.scroll.y))
297
301
  )
298
302
  l.append(
299
- (self.text_surface, self.text_rect)
303
+ (self.text_surface, self.text_rect.move(-self.scroll))
300
304
  )
305
+
306
+ # clip surface
307
+
308
+ old = self.surface.get_clip()
309
+ self.surface.set_clip(self.get_padded_rect().move(-self.rect.x,-self.rect.y))
301
310
  self.surface.fblits(l)
311
+ self.surface.set_clip(old)
302
312
 
313
+
303
314
  def align_text(
304
315
  self, text_rect: pygame.FRect, area: pygame.FRect, alignment: bf.alignment
305
316
  ):
@@ -312,6 +323,8 @@ class Label(Shape):
312
323
  text_rect.__setattr__(alignment.value, pos)
313
324
  text_rect.y = ceil(text_rect.y)
314
325
 
326
+
327
+
315
328
  def build(self) -> None:
316
329
  super().build()
317
330
  self._build_layout()
@@ -320,3 +333,6 @@ class Label(Shape):
320
333
  super().paint()
321
334
  if self.font_object:
322
335
  self._paint_text()
336
+
337
+ # def set_alignment(self, alignment: bf.alignment) -> Self:
338
+ # return self