batframework 1.0.9a6__py3-none-any.whl → 1.0.9a8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. batFramework/__init__.py +20 -11
  2. batFramework/action.py +1 -1
  3. batFramework/animatedSprite.py +47 -116
  4. batFramework/animation.py +30 -5
  5. batFramework/audioManager.py +16 -13
  6. batFramework/baseScene.py +240 -0
  7. batFramework/camera.py +4 -0
  8. batFramework/constants.py +6 -1
  9. batFramework/cutscene.py +221 -21
  10. batFramework/cutsceneManager.py +5 -2
  11. batFramework/drawable.py +7 -5
  12. batFramework/easingController.py +10 -11
  13. batFramework/entity.py +21 -2
  14. batFramework/enums.py +48 -33
  15. batFramework/gui/__init__.py +3 -1
  16. batFramework/gui/animatedLabel.py +10 -2
  17. batFramework/gui/button.py +4 -31
  18. batFramework/gui/clickableWidget.py +42 -30
  19. batFramework/gui/constraints/constraints.py +212 -136
  20. batFramework/gui/container.py +72 -48
  21. batFramework/gui/debugger.py +12 -17
  22. batFramework/gui/draggableWidget.py +8 -11
  23. batFramework/gui/image.py +3 -10
  24. batFramework/gui/indicator.py +73 -1
  25. batFramework/gui/interactiveWidget.py +117 -100
  26. batFramework/gui/label.py +73 -63
  27. batFramework/gui/layout.py +221 -452
  28. batFramework/gui/meter.py +21 -7
  29. batFramework/gui/radioButton.py +0 -1
  30. batFramework/gui/root.py +99 -29
  31. batFramework/gui/selector.py +257 -0
  32. batFramework/gui/shape.py +13 -5
  33. batFramework/gui/slider.py +260 -93
  34. batFramework/gui/textInput.py +45 -21
  35. batFramework/gui/toggle.py +70 -52
  36. batFramework/gui/tooltip.py +30 -0
  37. batFramework/gui/widget.py +203 -125
  38. batFramework/manager.py +7 -8
  39. batFramework/particle.py +4 -1
  40. batFramework/propertyEaser.py +79 -0
  41. batFramework/renderGroup.py +17 -50
  42. batFramework/resourceManager.py +43 -13
  43. batFramework/scene.py +15 -335
  44. batFramework/sceneLayer.py +138 -0
  45. batFramework/sceneManager.py +31 -36
  46. batFramework/scrollingSprite.py +8 -3
  47. batFramework/sprite.py +1 -1
  48. batFramework/templates/__init__.py +1 -2
  49. batFramework/templates/controller.py +97 -0
  50. batFramework/timeManager.py +76 -22
  51. batFramework/transition.py +37 -103
  52. batFramework/utils.py +121 -3
  53. {batframework-1.0.9a6.dist-info → batframework-1.0.9a8.dist-info}/METADATA +24 -3
  54. batframework-1.0.9a8.dist-info/RECORD +66 -0
  55. {batframework-1.0.9a6.dist-info → batframework-1.0.9a8.dist-info}/WHEEL +1 -1
  56. batFramework/character.py +0 -27
  57. batFramework/templates/character.py +0 -43
  58. batFramework/templates/states.py +0 -166
  59. batframework-1.0.9a6.dist-info/RECORD +0 -63
  60. /batframework-1.0.9a6.dist-info/LICENCE → /batframework-1.0.9a8.dist-info/LICENSE +0 -0
  61. {batframework-1.0.9a6.dist-info → batframework-1.0.9a8.dist-info}/top_level.txt +0 -0
@@ -4,7 +4,6 @@ from .shape import Shape
4
4
  from .interactiveWidget import InteractiveWidget
5
5
  from .layout import Layout, Column
6
6
  from typing import Self
7
- import pygame
8
7
  from pygame.math import Vector2
9
8
 
10
9
 
@@ -66,15 +65,21 @@ class Container(Shape, InteractiveWidget):
66
65
  def clamp_scroll(self) -> Self:
67
66
  if not self.children:
68
67
  return self
69
- r = self.get_padded_rect()
70
- size = self.children[0].rect.unionall([child.rect for child in self.children[1:]]).size
71
-
72
- max_scroll_x = max(0, size[0] - r.width)
73
- max_scroll_y = max(0, size[1] - r.height)
74
-
75
- self.scroll.x = max(0, min(self.scroll.x, max_scroll_x))
76
- self.scroll.y = max(0, min(self.scroll.y, max_scroll_y))
77
- self.dirty_layout = True
68
+ r = self.get_inner_rect()
69
+ # Compute the bounding rect of all children in one go
70
+ children_rect = self.children[0].rect.copy()
71
+ for child in self.children[1:]:
72
+ children_rect.union_ip(child.rect)
73
+ max_scroll_x = max(0, children_rect.width - r.width)
74
+ max_scroll_y = max(0, children_rect.height - r.height)
75
+
76
+ # Clamp scroll values only if needed
77
+ new_x = min(max(self.scroll.x, 0), max_scroll_x)
78
+ new_y = min(max(self.scroll.y, 0), max_scroll_y)
79
+
80
+ if self.scroll.x != new_x or self.scroll.y != new_y:
81
+ self.scroll.x = new_x
82
+ self.scroll.y = new_y
78
83
  return self
79
84
 
80
85
  def set_layout(self, layout: Layout) -> Self:
@@ -83,6 +88,7 @@ class Container(Shape, InteractiveWidget):
83
88
  if self.layout != tmp:
84
89
  tmp.set_parent(None)
85
90
  self.layout.set_parent(self)
91
+ self.reset_scroll()
86
92
  self.dirty_layout = True
87
93
  return self
88
94
 
@@ -96,13 +102,13 @@ class Container(Shape, InteractiveWidget):
96
102
  def add(self, *child: Widget) -> Self:
97
103
  super().add(*child)
98
104
  self.dirty_shape = True
99
- self.clamp_scroll()
105
+ self.dirty_layout = True
100
106
  return self
101
107
 
102
108
  def remove(self, *child: Widget) -> Self:
103
109
  super().remove(*child)
104
110
  self.dirty_shape = True
105
- self.clamp_scroll()
111
+ self.dirty_layout = True
106
112
  return self
107
113
 
108
114
  def top_at(self, x: float | int, y: float | int) -> "None|Widget":
@@ -123,60 +129,78 @@ class Container(Shape, InteractiveWidget):
123
129
  self.focused_index = min(self.focused_index, len(interactive_children) - 1)
124
130
  return interactive_children[self.focused_index].get_focus()
125
131
 
132
+ def children_has_focus(self)->bool:
133
+ return any(child.is_focused for child in self.get_interactive_children())
134
+
126
135
  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)
136
+ if event.consumed:
137
+ return
138
+ self.layout.handle_event(event)
129
139
 
130
140
  def set_focused_child(self, child: InteractiveWidget) -> bool:
131
141
  interactive_children = self.get_interactive_children()
132
142
  try:
133
143
  index = interactive_children.index(child)
134
144
  self.focused_index = index
145
+ if self.layout :
146
+ self.layout.scroll_to_widget(child)
135
147
  return True
136
148
  except ValueError:
137
149
  return False
138
150
 
139
151
  def allow_focus_to_self(self) -> bool:
140
152
  return bool(self.get_interactive_children()) and self.visible
153
+
154
+ def build(self) -> None:
155
+ if self.layout is not None:
156
+ # print("I'm building !",self)
157
+ # size = self.expand_rect_with_padding((0,0,*self.layout.get_auto_size())).size
158
+ size = self.layout.get_auto_size()
159
+ self.set_size(self.resolve_size(size))
160
+ super().build()
161
+
162
+ def apply_pre_updates(self):
163
+ if self.dirty_size_constraints or self.dirty_shape:
164
+ self.resolve_constraints(size_only=True)
165
+ self.dirty_size_constraints = False
166
+ self.dirty_position_constraints = True
141
167
 
168
+ if self.dirty_layout:
169
+ self.layout.update_child_constraints()
170
+ self.layout.arrange()
171
+ self.dirty_layout = False
142
172
 
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
173
+ def apply_post_updates(self,skip_draw:bool=False):
174
+ """
175
+ BOTTOM TO TOP
176
+ for cases when widget attributes depend on children attributes
177
+ """
151
178
  if self.dirty_shape:
152
- self.build() # Finalize size of the container
179
+ self.layout.update_child_constraints()
180
+ self.build()
153
181
  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
157
- for child in self.children:
158
- child.dirty_constraints = True
182
+ self.dirty_surface = True
183
+ self.dirty_layout = True
184
+ self.dirty_size_constraints = True
185
+ self.dirty_position_constraints = True
186
+ from .container import Container
187
+ if self.parent and isinstance(self.parent, Container):
188
+ self.parent.dirty_layout = True
189
+ self.parent.dirty_shape = True
159
190
 
160
- for child in self.children:
161
- child.apply_updates()
191
+ # trigger layout or constraint updates in parent
192
+
162
193
 
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
172
- if self.dirty_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
177
-
178
- # Step 4: Paint the surface if marked as dirty
179
- if self.dirty_surface:
180
- # print("PAINT !!")
194
+ # force recheck of constraints
195
+
196
+
197
+ if self.dirty_position_constraints:
198
+ self.resolve_constraints(position_only=True)
199
+ self.dirty_position_constraints= False
200
+
201
+
202
+ if self.dirty_surface and not skip_draw:
181
203
  self.paint()
182
204
  self.dirty_surface = False
205
+
206
+
@@ -2,6 +2,7 @@ from .label import Label
2
2
  from typing import Self,Callable,Any
3
3
  import batFramework as bf
4
4
  import pygame
5
+ import sys
5
6
 
6
7
 
7
8
  def convert_to_int(*args):
@@ -46,7 +47,7 @@ class Debugger(Label):
46
47
 
47
48
  def set_parent_scene(self, scene) -> Self:
48
49
  super().set_parent_scene(scene)
49
- self.set_render_order(99)
50
+ self.set_render_order(sys.maxsize-100)
50
51
  self.update_text()
51
52
  return self
52
53
 
@@ -72,15 +73,19 @@ class Debugger(Label):
72
73
  def update(self, dt: float) -> None:
73
74
  if not self.parent_scene:
74
75
  return
76
+
75
77
  if bf.ResourceManager().get_sharedVar("debug_mode") != bf.debugMode.DEBUGGER:
76
78
  self.set_visible(False)
77
79
  return
80
+
78
81
  self.set_visible(True)
79
82
  self.refresh_counter = self.refresh_counter + (dt * 60)
83
+
80
84
  if self.refresh_counter > self.refresh_rate:
81
85
  self.refresh_counter = 0
82
86
  self.update_text()
83
87
 
88
+
84
89
  def __str__(self) -> str:
85
90
  return "Debugger"
86
91
 
@@ -109,23 +114,13 @@ class BasicDebugger(FPSDebugger):
109
114
  "Resolution", lambda: "x".join(str(i) for i in bf.const.RESOLUTION)
110
115
  )
111
116
  super().do_when_added()
112
- parent_scene = self.parent_scene
113
-
114
117
  self.add_dynamic("Mouse", pygame.mouse.get_pos)
115
- self.add_dynamic(
116
- "World",
117
- lambda: convert_to_int(
118
- *parent_scene.camera.screen_to_world(pygame.mouse.get_pos())
119
- ),
120
- )
121
- self.add_dynamic(
122
- "Hud",
123
- lambda: convert_to_int(
124
- *parent_scene.hud_camera.screen_to_world(pygame.mouse.get_pos())
125
- ),
126
- )
127
- self.add_dynamic("W. Ent.", lambda: parent_scene.get_world_entity_count())
128
- self.add_dynamic("H. Ent.", lambda: parent_scene.get_hud_entity_count())
118
+
119
+ if not hasattr(self.parent_scene,"root"):
120
+ print("Debugger couldn't find 'root' widget in parent scene")
121
+ return
122
+
123
+ parent_scene = self.parent_scene
129
124
 
130
125
  self.add_dynamic(
131
126
  "Hover",
@@ -5,34 +5,31 @@ import pygame
5
5
 
6
6
  class DraggableWidget(InteractiveWidget):
7
7
  def __init__(self, *args, **kwargs) -> None:
8
- self.drag_action = bf.Action("dragging").add_mouse_control(1).set_holding()
9
8
 
10
9
  self.drag_start = None
11
10
  self.offset = None
12
11
  super().__init__(*args, **kwargs)
13
12
 
14
- def do_process_actions(self, event: pygame.Event) -> None:
15
- self.drag_action.process_event(event)
16
-
17
- def do_reset_actions(self) -> None:
18
- self.drag_action.reset()
19
-
13
+ def on_click_down(self, button):
14
+ if super().on_click_down(button)==False:
15
+ return button == 1 # capture event
16
+
20
17
  def do_on_drag(
21
18
  self, drag_start: tuple[float, float], drag_end: tuple[float, float]
22
19
  ) -> None:
23
20
  self.set_position(drag_end[0] - self.offset[0], drag_end[1] - self.offset[1])
24
21
 
22
+
25
23
  def update(self, dt: float):
26
- if self.drag_action.active and self.is_clicked_down:
24
+ if self.is_clicked_down and pygame.mouse.get_pressed(3)[0]:
27
25
  r = self.get_root()
28
26
  x, y = r.drawing_camera.screen_to_world(pygame.mouse.get_pos())
29
- if self.drag_start == None and self.drag_action.active:
27
+ if self.drag_start == None and self.is_clicked_down:
30
28
  self.offset = x - self.rect.x, y - self.rect.y
31
29
  self.drag_start = x, y
32
- return
33
30
  else:
34
31
  self.do_on_drag(self.drag_start, (x, y))
35
- return
32
+
36
33
  else:
37
34
  self.drag_start = None
38
35
  self.offset = None
batFramework/gui/image.py CHANGED
@@ -23,7 +23,7 @@ class Image(Shape):
23
23
  super().paint()
24
24
  if self.original_surface is None:
25
25
  return
26
- padded = self.get_padded_rect().move(-self.rect.x,-self.rect.y)
26
+ padded = self.get_inner_rect().move(-self.rect.x,-self.rect.y)
27
27
  target_size = padded.size
28
28
  if self.original_surface.get_size() != target_size:
29
29
  self.surface.blit(pygame.transform.scale(self.original_surface, target_size), padded.topleft)
@@ -32,17 +32,11 @@ class Image(Shape):
32
32
 
33
33
  def build(self) -> None:
34
34
  if self.original_surface is not None:
35
- self.set_size_if_autoresize(
36
- self.inflate_rect_by_padding((0,0,*self.original_surface.get_size())).size
35
+ self.set_size(
36
+ self.expand_rect_with_padding((0,0,*self.original_surface.get_size())).size
37
37
  )
38
38
  super().build()
39
39
 
40
- def get_min_required_size(self) -> tuple[float, float]:
41
- res = self.rect.size
42
- return self.inflate_rect_by_padding((0, 0, *res)).size
43
-
44
-
45
-
46
40
 
47
41
  def from_path(self, path: str) -> Self:
48
42
  tmp = bf.ResourceManager().get_image(path, self.convert_alpha)
@@ -60,6 +54,5 @@ class Image(Shape):
60
54
  self.original_surface = surface
61
55
  size = self.original_surface.get_size()
62
56
  self.set_size(size)
63
-
64
57
  self.dirty_surface = True
65
58
  return self
@@ -8,6 +8,12 @@ import batFramework as bf
8
8
 
9
9
 
10
10
  class Indicator(Shape):
11
+ """
12
+ Shape intended to be used as icons/indicators
13
+ due to its nature, it overrides the top_at function (it can not be 'seen' by the mouse)
14
+
15
+ """
16
+
11
17
  def __init__(self, size: tuple[int | float] = (10, 10)) -> None:
12
18
  super().__init__(size)
13
19
  self.debug_color = "magenta"
@@ -35,7 +41,7 @@ class ToggleIndicator(Indicator):
35
41
  self.set_value(default_value)
36
42
  self.callback(default_value)
37
43
  # TODO aspect ratio would be good right about here
38
- # self.add_constraint(ConstraintAspectRatio(1))
44
+ self.add_constraints(bf.gui.AspectRatio(1,reference_axis=bf.axis.VERTICAL))
39
45
 
40
46
  def set_callback(self, callback : Callable[[bool],Any]) -> Self:
41
47
  self.callback = callback
@@ -56,3 +62,69 @@ class ToggleIndicator(Indicator):
56
62
  return None
57
63
  return r
58
64
 
65
+ class ArrowIndicator(Indicator):
66
+ def __init__(self,direction:bf.direction):
67
+ super().__init__()
68
+ self.direction : bf.direction = direction
69
+ self.arrow_color = bf.color.WHITE
70
+ self.line_width : int = 1
71
+ self.angle : float = 45
72
+ self.spread : float = None
73
+ self.draw_stem : bool = True
74
+
75
+ def set_draw_stem(self,value:bool)->Self:
76
+ self.draw_stem = value
77
+ self.dirty_surface = False
78
+ return self
79
+
80
+ def set_spread(self,value:float)->Self:
81
+ self.spread = value
82
+ self.dirty_surface = True
83
+ return self
84
+
85
+ def set_angle(self,value:float)->Self:
86
+ self.angle = value
87
+ self.dirty_surface = True
88
+ return self
89
+
90
+ def set_arrow_color(self,color)-> Self:
91
+ self.arrow_color = color
92
+ self.dirty_surface = True
93
+ return self
94
+
95
+ def set_direction(self,direction:bf.direction)->Self:
96
+ self.direction = direction
97
+ self.dirty_surface = True
98
+ return self
99
+
100
+ def set_line_width(self,value:int)->Self:
101
+ self.line_width = value
102
+ self.dirty_surface = True
103
+ return self
104
+
105
+ def paint(self):
106
+ super().paint()
107
+ r = self.get_local_padded_rect()
108
+ size = min(r.width, r.height)
109
+ if size %2 == 0:
110
+ size -= 1
111
+ r.width = size
112
+ r.height = size
113
+ if (self.padding[1]+self.padding[3] )%2 ==0:
114
+ r.height-=1
115
+ if (self.padding[0]+self.padding[2] )%2 ==0:
116
+ r.width-=1
117
+ r.center = self.get_local_padded_rect().center
118
+ # r.inflate_ip(3,3)
119
+ # r.normalize()
120
+ # r.move_ip(-self.rect.left,-self.rect.right)
121
+ bf.utils.draw_triangle(
122
+ surface = self.surface,
123
+ color = self.arrow_color,
124
+ rect =r,
125
+ direction = self.direction,
126
+
127
+ )
128
+
129
+
130
+
@@ -56,7 +56,7 @@ 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):
59
+ if isinstance(self.parent,bf.gui.Container):
60
60
  self.parent.layout.scroll_to_widget(self)
61
61
  self.do_on_get_focus()
62
62
 
@@ -64,97 +64,131 @@ class InteractiveWidget(Widget):
64
64
  self.is_focused = False
65
65
  self.do_on_lose_focus()
66
66
 
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)
67
+
91
68
 
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)
69
+ def get_interactive_widgets(self):
70
+ """Retrieve all interactive widgets in the tree, in depth-first order."""
71
+ widgets = []
72
+ stack = [self]
73
+ while stack:
74
+ widget = stack.pop()
75
+ if isinstance(widget, InteractiveWidget) and widget.allow_focus_to_self():
76
+ widgets.append(widget)
77
+ stack.extend(reversed(widget.children)) # Add children in reverse for left-to-right traversal
78
+ return widgets
79
+
80
+ def find_next_widget(self, current):
81
+ """Find the next interactive widget, considering parent and sibling relationships."""
82
+ if current.is_root:
83
+ return None # Root has no parent
84
+
85
+ siblings = current.parent.children
86
+ start_index = siblings.index(current)
87
+ good_index = -1
88
+ for i in range(start_index + 1, len(siblings)):
89
+ if isinstance(siblings[i], InteractiveWidget) and siblings[i].allow_focus_to_self():
90
+ good_index = i
91
+ break
92
+ if good_index >= 0:
93
+ # Not the last child, return the next sibling
94
+ return siblings[good_index]
95
+ else:
96
+ # Current is the last child, move to parent's next sibling
97
+ return self.find_next_widget(current.parent)
98
+
99
+ def find_prev_widget(self, current : "Widget"):
100
+ """Find the previous interactive widget, considering parent and sibling relationships."""
101
+ if current.is_root:
102
+ return None # Root has no parent
103
+
104
+ # siblings = [c for c in current.parent.children if isinstance(c,InteractiveWidget) and c.allow_focus_to_self()]
105
+ siblings = current.parent.children
106
+ start_index = siblings.index(current)
107
+ good_index = -1
108
+ for i in range(start_index-1,-1,-1):
109
+ sibling = siblings[i]
110
+ if isinstance(sibling,InteractiveWidget):
111
+ if sibling.allow_focus_to_self():
112
+ good_index = i
113
+ break
114
+ if good_index >= 0:
115
+ # Not the first child, return the previous sibling
116
+ return siblings[good_index]
117
+ else:
118
+ # Current is the first child, move to parent's previous sibling
119
+ return self.find_prev_widget(current.parent)
115
120
 
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]:
121
+ def focus_next_tab(self, previous_widget):
122
+ """Focus the next interactive widget."""
123
+ if previous_widget:
124
+ next_widget = self.find_next_widget(previous_widget)
125
+ if next_widget:
126
+ next_widget.get_focus()
120
127
 
121
- self.focus_prev_tab(self)
122
- else:
123
- self.focus_next_tab(self)
124
- return True
125
- else:
128
+ def focus_prev_tab(self, previous_widget):
129
+ """Focus the previous interactive widget."""
130
+ if previous_widget:
131
+ prev_widget = self.find_prev_widget(previous_widget)
132
+ if prev_widget:
133
+ prev_widget.get_focus()
126
134
 
127
- return self.do_on_key_down(key)
128
135
 
129
- return False
136
+
137
+ def on_key_down(self, key) -> bool:
138
+ """
139
+ return True to stop event progpagation
140
+ """
141
+ return self.do_on_key_down(key)
130
142
 
131
143
  def on_key_up(self, key) -> bool:
144
+ """
145
+ return True to stop event progpagation
146
+ """
132
147
  return self.do_on_key_up(key)
133
148
 
149
+ def do_on_get_focus(self) -> None:
150
+ pass
151
+
152
+ def do_on_lose_focus(self) -> None:
153
+ pass
154
+
134
155
  def do_on_key_down(self, key) -> bool:
156
+ """
157
+ return True to stop event progpagation
158
+ """
135
159
  return False
136
160
 
137
161
  def do_on_key_up(self, key) -> bool:
162
+ """
163
+ return True to stop event progpagation
164
+ """
138
165
  return False
139
166
 
140
- def do_on_get_focus(self) -> None:
141
- pass
142
-
143
- def do_on_lose_focus(self) -> None:
144
- pass
145
167
 
146
168
  def on_click_down(self, button: int) -> bool:
169
+ """
170
+ return True to stop event progpagation
171
+ """
147
172
  self.is_clicked_down = True
148
173
  return self.do_on_click_down(button)
149
174
 
150
175
  def on_click_up(self, button: int) -> bool:
176
+ """
177
+ return True to stop event progpagation
178
+ """
151
179
  self.is_clicked_down = False
152
180
  return self.do_on_click_up(button)
153
181
 
154
182
  def do_on_click_down(self, button: int) -> bool:
183
+ """
184
+ return True to stop event progpagation
185
+ """
155
186
  return False
156
187
 
157
188
  def do_on_click_up(self, button: int) -> bool:
189
+ """
190
+ return True to stop event progpagation
191
+ """
158
192
  return False
159
193
 
160
194
  def on_enter(self) -> None:
@@ -182,39 +216,22 @@ class InteractiveWidget(Widget):
182
216
  pass
183
217
 
184
218
  def draw_focused(self, camera: bf.Camera) -> None:
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),
220
- )
219
+ prop = 16
220
+ pulse = int(prop * 0.75 - (prop * cos(pygame.time.get_ticks() / 100) / 4))
221
+ delta = (pulse // 2) * 2 # ensure even
222
+
223
+ # Get rect in screen space, inflated for visual effect
224
+ screen_rect = camera.world_to_screen(self.rect.inflate(prop, prop))
225
+
226
+ # Shrink for inner pulsing border
227
+ inner = screen_rect.inflate(-delta, -delta)
228
+ inner.topleft = 0,0
229
+ surface = pygame.Surface(inner.size)
230
+ surface.set_colorkey((0,0,0))
231
+ pygame.draw.rect(surface, "white", inner, 2, *self.border_radius)
232
+ pygame.draw.rect(surface, "black", inner.inflate(-16,0), 2)
233
+ pygame.draw.rect(surface, "black", inner.inflate(0,-16), 2)
234
+ inner.center = screen_rect.center
235
+ camera.surface.blit(surface,inner)
236
+
237
+ # pygame.draw.rect(camera.surface, "white", inner, 2, *self.border_radius)