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
batFramework/gui/meter.py CHANGED
@@ -1,3 +1,4 @@
1
+ import math
1
2
  import batFramework as bf
2
3
  from .shape import Shape
3
4
  from typing import Self
@@ -8,7 +9,6 @@ def custom_top_at(self, x, y):
8
9
  return self.parent
9
10
  return None
10
11
 
11
-
12
12
  class Meter(Shape):
13
13
  def __init__(self, min_value: float = 0, max_value: float = 1, step: float = 0.1):
14
14
  super().__init__()
@@ -16,14 +16,6 @@ class Meter(Shape):
16
16
  self.step = step
17
17
  self.snap: bool = False
18
18
  self.value = self.max_value
19
- self.content = Shape((0, 0)).set_color(bf.color.BLUE)
20
- self.content.set_debug_color("cyan")
21
- self.content.top_at = lambda x, y: custom_top_at(self.content, x, y)
22
- self.add(self.content)
23
- self.set_padding(4)
24
- self.set_color("gray20")
25
- self.set_outline_width(1)
26
- self.set_outline_color(bf.color.BLACK)
27
19
  self.set_debug_color("pink")
28
20
 
29
21
  def __str__(self) -> str:
@@ -36,18 +28,11 @@ class Meter(Shape):
36
28
 
37
29
  def set_range(self, range_min: float, range_max: float) -> Self:
38
30
  if range_min >= range_max:
39
- print(
40
- f"[Warning] : minimum value {range_min} is greater than or equal to maximum value {range_max}"
41
- )
42
31
  return self
43
32
  self.min_value = range_min
44
33
  self.max_value = range_max
45
34
  self.dirty_shape = True
46
35
 
47
- def get_debug_outlines(self):
48
- yield from super().get_debug_outlines()
49
- # yield from self.content.get_debug_outlines()
50
-
51
36
  def set_value(self, value: float) -> Self:
52
37
  value = max(self.min_value, min(self.max_value, value))
53
38
  value = round(value / self.step) * self.step
@@ -64,11 +49,48 @@ class Meter(Shape):
64
49
  def get_ratio(self) -> float:
65
50
  return (self.value-self.min_value) / (self.max_value - self.min_value)
66
51
 
52
+
53
+ class BarMeter(Meter):
54
+ def __init__(self, min_value: float = 0, max_value: float = 1, step: float = 0.1):
55
+ super().__init__(min_value, max_value, step)
56
+ self.axis: bf.axis = bf.axis.HORIZONTAL
57
+ self.content = Shape((0, 0)).set_color(bf.color.BLUE)
58
+ self.content.set_debug_color("cyan")
59
+ self.content.top_at = lambda x, y: custom_top_at(self.content, x, y)
60
+ self.add(self.content)
61
+ self.set_padding(4)
62
+ self.set_color("gray20")
63
+ self.set_outline_width(1)
64
+ self.set_outline_color(bf.color.BLACK)
65
+ self.set_debug_color("pink")
66
+
67
+ def __str__(self) -> str:
68
+ return "BarMeter"
69
+
70
+ def set_axis(self,axis:bf.axis)->Self:
71
+ self.axis = axis
72
+ self.dirty_shape = True
73
+ return self
74
+
67
75
  def _build_content(self) -> None:
68
- width = self.get_padded_width() * self.get_ratio()
69
- self.content.set_size((width, self.get_padded_height()))
70
- self.content.rect.topleft = self.get_padded_rect().topleft
76
+ padded = self.get_inner_rect()
77
+ ratio = self.get_ratio()
78
+
79
+ self.content.set_border_radius(*[round(b/2) for b in self.border_radius])
80
+
81
+ if self.axis == bf.axis.HORIZONTAL:
82
+ width = (padded.width- self.outline_width *2) * ratio
83
+ self.content.set_size((width, padded.height - self.outline_width*2))
84
+ self.content.rect.topleft = padded.move(self.outline_width, self.outline_width).topleft
85
+
86
+ else: # vertical
87
+ height = (padded.height - self.outline_width * 2) * ratio
88
+ self.content.set_size((padded.width - self.outline_width * 2, height))
89
+ self.content.rect.bottomleft = (
90
+ padded.move(self.outline_width,-self.outline_width).bottomleft
91
+ )
92
+ self.content.rect.height = math.ceil(self.content.rect.height)
71
93
 
72
94
  def build(self) -> None:
73
- super().build()
74
95
  self._build_content()
96
+ super().build()
@@ -1,84 +1,35 @@
1
1
  import batFramework as bf
2
2
  from typing import Self, Any, Callable
3
3
  from .toggle import Toggle
4
-
5
-
6
- class RadioVariable: ...
4
+ from .syncedVar import SyncedVar
7
5
 
8
6
 
9
7
  class RadioButton(Toggle):
10
- def __init__(self, text: str = "", radio_value: Any = None) -> None:
8
+ def __init__(self, text: str = "", radio_value: Any = None, synced_var: SyncedVar = None) -> None:
11
9
  super().__init__(text, None, False)
12
- self.radio_value: Any = (
13
- radio_value if radio_value is not None else text if text else None
14
- )
15
- self.radio_variable: RadioVariable = None
10
+ self.radio_value: Any = radio_value if radio_value is not None else text if text else None
11
+ self.synced_var : SyncedVar = None
12
+ if synced_var:
13
+ self.link(synced_var)
14
+
15
+ def link(self,synced_var:SyncedVar)->Self:
16
+ self.synced_var = synced_var
17
+ synced_var.bind_widget(self,self._update_state)
18
+ return self
16
19
 
17
20
  def __str__(self) -> str:
18
- return (
19
- f"RadioButton({self.radio_value}|{'Active' if self.value else 'Inactive'})"
20
- )
21
+ return f"RadioButton({self.radio_value}|{'Active' if self.value else 'Inactive'})"
21
22
 
22
23
  def set_radio_value(self, value: Any) -> Self:
23
24
  self.radio_value = value
24
-
25
- if self.radio_variable:
26
- self.radio_variable.update_buttons()
27
25
  return self
28
26
 
29
- def set_text(self, text: str) -> Self:
30
- flag = False
31
- if self.value == self.text or self.value is None:
32
- flag = True
33
- super().set_text(text)
34
- if flag:
35
- self.set_radio_value(self.text)
36
- return self
37
-
38
- def click(self) -> None:
39
- if self.radio_variable is None:
40
- return
41
- self.radio_variable.set_value(self.radio_value)
42
-
43
-
44
- class RadioVariable:
45
- def __init__(self) -> None:
46
- self.buttons: list[RadioButton] = []
47
- self.value = None
48
- self.modify_callback: Callable[[bool],Any] = None
49
-
50
- def set_modify_callback(self, callback:Callable[[bool],Any]) -> Self:
51
- self.modify_callback = callback
52
- return self
53
-
54
- def link(self, *buttons: RadioButton) -> Self:
55
- if not buttons:
56
- return self
57
- for b in buttons:
58
- b.radio_variable = self
59
- self.buttons.extend(buttons)
60
-
61
- if self.value is None:
62
- self.set_value(buttons[0].radio_value)
63
- else:
64
- self.update_buttons()
65
- return self
66
-
67
- def unlink(self, *buttons) -> Self:
68
- for b in self.buttons:
69
- if b in buttons:
70
- b.radio_variable = None
71
- self.buttons = [b for b in self.buttons if b not in buttons]
72
- return self
73
-
74
- def set_value(self, value: Any) -> Self:
75
- if value == self.value:
76
- return
77
- self.value = value
78
- if self.modify_callback:
79
- self.modify_callback(value)
80
- self.update_buttons()
81
- return self
27
+ def set_value(self, value : bool, do_callback=False):
28
+ super().set_value(value, do_callback)
29
+ if value : self.synced_var.value = self.radio_value
82
30
 
83
- def update_buttons(self):
84
- _ = [b.set_value(b.radio_value == self.value, False) for b in self.buttons]
31
+ def _update_state(self, synced_value: Any) -> None:
32
+ """
33
+ Updates the state of the RadioButton based on the synced variable's value.
34
+ """
35
+ self.set_value(self.radio_value == synced_value, False)
batFramework/gui/root.py CHANGED
@@ -15,15 +15,28 @@ class Root(InteractiveWidget):
15
15
  self.rect.size = pygame.display.get_surface().get_size()
16
16
  self.focused: InteractiveWidget | None = self
17
17
  self.hovered: Widget | None = self
18
- self.set_debug_color("yellow")
19
- self.set_render_order(sys.maxsize)
20
18
  self.clip_children = False
19
+ self.set_debug_color("yellow")
21
20
 
21
+ self.show_tooltip : bool = True
22
+ self.tooltip = bf.gui.ToolTip("").set_visible(False)
23
+ self.add(self.tooltip)
24
+
25
+ def toggle_tooltip(self,value:bool)->Self:
26
+ self.show_tooltip = value
27
+ return self
28
+
22
29
  def __str__(self) -> str:
23
30
  return "Root"
31
+
32
+ def to_ascii_tree(self) -> str:
33
+ def f(w, depth):
34
+ prefix = " " * (depth * 4) + ("└── " if depth > 0 else "")
35
+ children = "\n".join(f(c, depth + 1) for c in w.children) if w.children else ""
36
+ return f"{prefix}{str(w)}\n{children}"
37
+ return f(self, 0)
24
38
 
25
39
  def set_parent_scene(self, parent_scene: bf.Scene) -> Self:
26
- bf.StyleManager().register_widget(self)
27
40
  return super().set_parent_scene(parent_scene)
28
41
 
29
42
  def get_focused(self) -> Widget | None:
@@ -91,28 +104,35 @@ class Root(InteractiveWidget):
91
104
  if not force:
92
105
  return self
93
106
  self.rect.size = size
107
+ self.dirty_shape = True
108
+ self.dirty_size_constraints = True
94
109
  return self
95
110
 
96
- def do_handle_event(self, event):
111
+ def process_event(self,event):
112
+ if not event.consumed : self.do_handle_event_early(event)
113
+ if not event.consumed : super().process_event(event)
114
+
115
+ def do_handle_event_early(self, event):
116
+ if event.type == pygame.VIDEORESIZE and not pygame.SCALED & bf.const.FLAGS:
117
+ self.set_size((event.w,event.h),force=True)
97
118
  if self.focused:
98
119
  if event.type == pygame.KEYDOWN:
99
- if self.focused.on_key_down(event.key):
100
- event.consumed = True
120
+ event.consumed = self.focused.on_key_down(event.key)
121
+ if not event.consumed :
122
+ event.consumed = self._handle_alt_tab(event.key)
101
123
  elif event.type == pygame.KEYUP:
102
- if self.focused.on_key_up(event.key):
103
- event.consumed = True
124
+ event.consumed = self.focused.on_key_up(event.key)
104
125
 
105
126
  if not self.hovered or (not isinstance(self.hovered, InteractiveWidget)):
127
+ event.consumed = True
106
128
  return
107
129
 
108
130
  if event.type == pygame.MOUSEBUTTONDOWN:
109
- if self.hovered.on_click_down(event.button):
110
- event.consumed = True
131
+ event.consumed = self.hovered.on_click_down(event.button)
111
132
 
112
133
  elif event.type == pygame.MOUSEBUTTONUP:
113
- if self.hovered.on_click_up(event.button):
114
- event.consumed = True
115
-
134
+ event.consumed = self.hovered.on_click_up(event.button)
135
+
116
136
  def do_on_click_down(self, button: int) -> None:
117
137
  if button == 1:
118
138
  self.clear_focused()
@@ -127,25 +147,69 @@ class Root(InteractiveWidget):
127
147
 
128
148
  def update(self, dt: float) -> None:
129
149
  super().update(dt)
130
- old = self.hovered
131
- transposed = self.drawing_camera.screen_to_world(pygame.mouse.get_pos())
132
- self.hovered = self.top_at(*transposed) if self.top_at(*transposed) else None
133
- if old == self.hovered and isinstance(self.hovered, InteractiveWidget):
134
- self.hovered.on_mouse_motion(*transposed)
150
+ self.update_tree()
151
+
152
+ mouse_screen = pygame.mouse.get_pos()
153
+ mouse_world = self.drawing_camera.screen_to_world(mouse_screen)
154
+ prev_hovered = self.hovered
155
+ self.hovered = self.top_at(*mouse_world) or None
156
+
157
+ # Tooltip logic
158
+ if self.hovered and self.hovered.tooltip_text:
159
+ self.tooltip.set_text(self.hovered.tooltip_text)
160
+
161
+ tooltip_size = self.tooltip.get_min_required_size()
162
+ screen_w, screen_h = self.drawing_camera.rect.size
163
+ tooltip_x, tooltip_y = self.drawing_camera.world_to_screen_point(mouse_world)
164
+
165
+ tooltip_x = min(tooltip_x, screen_w - tooltip_size[0])
166
+ tooltip_y = min(tooltip_y, screen_h - tooltip_size[1])
167
+ tooltip_x = max(0, tooltip_x)
168
+ tooltip_y = max(0, tooltip_y)
169
+
170
+ self.tooltip.set_position(tooltip_x, tooltip_y)
171
+ self.tooltip.fade_in()
172
+ else:
173
+ self.tooltip.fade_out()
174
+
175
+ if self.hovered == prev_hovered:
176
+ if isinstance(self.hovered, InteractiveWidget):
177
+ self.hovered.on_mouse_motion(*mouse_world)
135
178
  return
136
- if old and isinstance(old, InteractiveWidget):
137
- old.on_exit()
138
- if self.hovered and isinstance(self.hovered, InteractiveWidget):
179
+
180
+ if isinstance(prev_hovered, InteractiveWidget):
181
+ prev_hovered.on_exit()
182
+ if isinstance(self.hovered, InteractiveWidget):
139
183
  self.hovered.on_enter()
140
184
 
141
- def apply_updates(self):
142
- if any(child.dirty_shape for child in self.children):
143
- self.dirty_shape = True # Mark layout as dirty if any child changed size
144
185
 
145
- if self.dirty_shape:
146
- for child in self.children:
147
- child.apply_updates()
148
- self.dirty_shape = False
186
+ def _handle_alt_tab(self,key):
187
+ if self.focused is None:
188
+ return False
189
+ if key != pygame.K_TAB:
190
+ return False
191
+ keys = pygame.key.get_pressed()
192
+ if keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]:
193
+ self.focused.focus_prev_tab(self.focused)
194
+ else:
195
+ self.focused.focus_next_tab(self.focused)
196
+ return True
197
+
198
+ def update_tree(self):
199
+ # print("START updating tree")
200
+ self.apply_updates("pre")
201
+ self.apply_updates("post")
202
+
203
+ self.apply_updates("pre")
204
+ self.apply_updates("post")
205
+
206
+ # print("END updating tree")
207
+
208
+ def apply_pre_updates(self):
209
+ return
210
+
211
+ def apply_post_updates(self, skip_draw = False):
212
+ return
149
213
 
150
214
  def draw(self, camera: bf.Camera) -> None:
151
215
  super().draw(camera)
@@ -155,4 +219,10 @@ class Root(InteractiveWidget):
155
219
  and self.focused
156
220
  and self.focused != self
157
221
  ):
158
- self.focused.draw_focused(camera)
222
+ clip:bool =self.focused.parent and self.focused.parent.clip_children
223
+ if clip:
224
+ old_clip = camera.surface.get_clip()
225
+ camera.surface.set_clip(camera.world_to_screen(self.focused.parent.rect))
226
+ if clip:
227
+ camera.surface.set_clip(old_clip)
228
+ self.focused.draw_focused(camera)
@@ -0,0 +1,250 @@
1
+ import batFramework as bf
2
+ import pygame
3
+ from typing import Self,Callable,Any
4
+ from .button import Button
5
+ from .indicator import ArrowIndicator
6
+ from .clickableWidget import ClickableWidget
7
+ from .widget import Widget
8
+
9
+ class MyArrow(ArrowIndicator,ClickableWidget):
10
+ def top_at(self,x,y):
11
+ return Widget.top_at(self,x,y)
12
+
13
+ def get_focus(self):
14
+ return self.parent.get_focus()
15
+
16
+ class Selector(Button):
17
+ def __init__(self,options:list[str]=None,default_value:str=None,allow_cycle:bool=False):
18
+ super().__init__('')
19
+ self.allow_cycle = allow_cycle
20
+ self.current_index = 0
21
+ self.on_modify_callback : Callable[[str,int],Any] = None
22
+ self.options = options if options else []
23
+ self.gap : int = 2
24
+ text_value = ""
25
+
26
+
27
+ if not (default_value is not None and default_value in self.options):
28
+ default_value = options[0]
29
+
30
+ text_value = default_value
31
+ self.current_index = self.options.index(default_value)
32
+
33
+ self.left_indicator : MyArrow = (MyArrow(bf.direction.LEFT)
34
+ .set_color((0,0,0,0)).set_arrow_color(self.text_color)
35
+ .set_callback(lambda : self.set_by_index(self.get_current_index()-1))
36
+ )
37
+
38
+ self.right_indicator:MyArrow =(MyArrow(bf.direction.RIGHT)
39
+ .set_color((0,0,0,0)).set_arrow_color(self.text_color)
40
+ .set_callback(lambda : self.set_by_index(self.get_current_index()+1))
41
+ )
42
+
43
+ self.add(self.left_indicator,self.right_indicator)
44
+ self.set_clip_children(False)
45
+ self.set_text(text_value)
46
+
47
+ def __str__(self):
48
+ return f"Selector[{self.options[self.current_index]}]"
49
+
50
+ def set_gap(self,value:int)->Self:
51
+ self.gap = value
52
+ self.dirty_shape = True
53
+ return self
54
+
55
+ def set_arrow_color(self,color)->Self:
56
+ self.left_indicator.set_arrow_color(color)
57
+ self.right_indicator.set_arrow_color(color)
58
+ return self
59
+
60
+ def disable(self):
61
+ super().disable()
62
+ self.left_indicator.disable()
63
+ self.right_indicator.disable()
64
+ return self
65
+
66
+ def enable(self):
67
+ super().enable()
68
+ self.right_indicator.enable()
69
+ self.left_indicator.enable()
70
+ index = self.current_index
71
+ if not self.allow_cycle:
72
+ if index == 0:
73
+ self.left_indicator.disable()
74
+ else:
75
+ self.left_indicator.enable()
76
+ if index == len(self.options)-1:
77
+ self.right_indicator.disable()
78
+ else:
79
+ self.right_indicator.enable()
80
+
81
+
82
+ return self
83
+
84
+ def set_tooltip_text(self, text):
85
+ self.left_indicator.set_tooltip_text(text)
86
+ self.right_indicator.set_tooltip_text(text)
87
+ return super().set_tooltip_text(text)
88
+
89
+ def get_min_required_size(self) -> tuple[float, float]:
90
+ """
91
+ Calculates the minimum size required for the selector, including text and indicators.
92
+ """
93
+
94
+ # Calculate the maximum size needed for the text
95
+ old_text = self.text
96
+ max_text_size = (0, 0)
97
+ for option in self.options:
98
+ self.text = option
99
+ text_size = self._get_text_rect_required_size()
100
+ max_text_size = max(max_text_size[0], text_size[0]), max(max_text_size[1], text_size[1])
101
+ self.text = old_text
102
+
103
+ # Ensure total_height is always an odd integer
104
+ total_height = max(self.font_object.get_height()+1, max_text_size[1] * 1.5)
105
+ total_height += self.unpressed_relief
106
+ total_height += max(self.right_indicator.outline_width,self.left_indicator.outline_width)
107
+
108
+ # Calculate total width and height
109
+ total_width = (
110
+ total_height*2+
111
+ max_text_size[0] + # Text width
112
+ self.gap * 2 # Gaps between text and indicators
113
+ )
114
+ # Inflate by padding at the very end
115
+ final_size = self.expand_rect_with_padding((0, 0, total_width, total_height)).size
116
+
117
+ return final_size
118
+
119
+ def _align_content(self):
120
+ """
121
+ Builds the selector layout (places and resizes the indicators)
122
+ """
123
+
124
+ # return
125
+ # Step 1: Calculate the padded area for positioning
126
+ padded = self.get_inner_rect()
127
+
128
+
129
+ # left_size = self.left_indicator.rect.size
130
+ right_size = self.right_indicator.rect.size
131
+
132
+ indicator_height = padded.h
133
+
134
+ self.left_indicator.set_size((indicator_height,indicator_height))
135
+ self.right_indicator.set_size((indicator_height,indicator_height))
136
+
137
+
138
+ # Step 3: Position indicators
139
+ self.left_indicator.set_position(padded.left, None)
140
+ self.left_indicator.set_center(None, padded.centery)
141
+
142
+ self.right_indicator.set_position(padded.right - right_size[0], None)
143
+ self.right_indicator.set_center(None, padded.centery)
144
+
145
+ def apply_post_updates(self, skip_draw = False):
146
+ super().apply_post_updates(skip_draw)
147
+ self._align_content()
148
+
149
+ def get_current_index(self)->int:
150
+ return self.current_index
151
+
152
+ def set_allow_cycle(self,value:bool)->Self:
153
+ if value == self.allow_cycle: return self
154
+ self.allow_cycle = value
155
+ self.dirty_surface = True
156
+ return self
157
+
158
+ def set_text_color(self,color)->Self:
159
+ super().set_text_color(color)
160
+ self.left_indicator.set_arrow_color(color)
161
+ self.right_indicator.set_arrow_color(color)
162
+ return self
163
+
164
+ def set_modify_callback(self,function:Callable[[str,int],Any]):
165
+ """
166
+ the function will receive the value and the index
167
+ """
168
+ self.on_modify_callback = function
169
+ return self
170
+
171
+ def set_by_index(self,index:int)->Self:
172
+ if self.allow_cycle:
173
+ index = index%len(self.options)
174
+ else:
175
+ index = max(min(len(self.options)-1,index),0)
176
+
177
+ if index == self.current_index:
178
+ return self
179
+
180
+ self.current_index = index
181
+ self.set_text(self.options[self.current_index])
182
+ if self.on_modify_callback:
183
+ self.on_modify_callback(self.options[self.current_index],self.current_index)
184
+ if not self.allow_cycle:
185
+ if index == 0:
186
+ self.left_indicator.disable()
187
+ else:
188
+ self.left_indicator.enable()
189
+ if index == len(self.options)-1:
190
+ self.right_indicator.disable()
191
+ else:
192
+ self.right_indicator.enable()
193
+ return self
194
+
195
+ def paint(self):
196
+ super().paint()
197
+ self.left_indicator.show()
198
+ self.right_indicator.show()
199
+ if not self.allow_cycle and self.current_index == 0:
200
+ self.left_indicator.hide()
201
+ elif not self.allow_cycle and self.current_index== len(self.options)-1:
202
+ self.right_indicator.hide()
203
+
204
+ def set_by_value(self,value:str)->Self:
205
+ if not self.is_enabled : return
206
+ if value not in self.options : return self
207
+ index = self.options.index(value)
208
+ self.set_by_index(index)
209
+ return self
210
+
211
+ def on_key_down(self, key: int) -> bool:
212
+ if not self.is_enabled:
213
+ return False
214
+
215
+ key_actions = {
216
+ pygame.K_RIGHT: self.right_indicator,
217
+ pygame.K_SPACE: self.right_indicator,
218
+ pygame.K_LEFT: self.left_indicator
219
+ }
220
+
221
+ indicator = key_actions.get(key)
222
+ if indicator and indicator.visible and indicator.is_enabled:
223
+ indicator.on_click_down(1)
224
+ return True
225
+
226
+ return False
227
+
228
+ def on_key_up(self, key: int) -> bool:
229
+ if not self.is_enabled:
230
+ return False
231
+
232
+ key_actions = {
233
+ pygame.K_RIGHT: self.right_indicator,
234
+ pygame.K_SPACE: self.right_indicator,
235
+ pygame.K_LEFT: self.left_indicator
236
+ }
237
+
238
+ indicator = key_actions.get(key)
239
+ if indicator and indicator.visible and indicator.is_enabled:
240
+ indicator.on_click_up(1)
241
+ return True
242
+
243
+ return False
244
+
245
+ def do_on_click_down(self, button) -> None:
246
+ if self.is_enabled and button == 1:
247
+ if not self.get_focus():
248
+ return True
249
+ return False
250
+
batFramework/gui/shape.py CHANGED
@@ -18,16 +18,24 @@ class Shape(Widget):
18
18
  self.shadow_color: tuple[int, int, int] | str = (0, 0, 0, 255)
19
19
  self.draw_mode = bf.drawMode.SOLID
20
20
 
21
- def get_padded_bottom(self) -> float:
21
+ def get_inner_bottom(self) -> float:
22
22
  return self.rect.bottom - self.padding[3] - self.relief
23
23
 
24
- def get_padded_height(self) -> float:
24
+ def get_inner_height(self) -> float:
25
25
  return self.rect.h - self.padding[1] - self.padding[3] - self.relief
26
26
 
27
- def get_padded_top(self) -> float:
28
- return self.rect.y + self.relief
27
+ def get_inner_top(self) -> float:
28
+ return self.rect.y + self.padding[1]
29
29
 
30
- def get_padded_rect(self) -> pygame.FRect:
30
+ def get_local_inner_rect(self)->pygame.FRect:
31
+ return pygame.FRect(
32
+ self.padding[0],
33
+ self.padding[1],
34
+ self.rect.w - self.padding[2] - self.padding[0],
35
+ self.rect.h - self.padding[1] - self.padding[3] - self.relief,
36
+ )
37
+
38
+ def get_inner_rect(self) -> pygame.FRect:
31
39
  return pygame.FRect(
32
40
  self.rect.x + self.padding[0],
33
41
  self.rect.y + self.padding[1],