batframework 1.0.8a4__py3-none-any.whl → 1.0.8a6__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.
- batFramework/__init__.py +15 -1
- batFramework/animatedSprite.py +65 -50
- batFramework/character.py +27 -0
- batFramework/dynamicEntity.py +1 -0
- batFramework/enums.py +2 -2
- batFramework/fontManager.py +2 -2
- batFramework/gui/clickableWidget.py +6 -7
- batFramework/gui/constraints/constraints.py +125 -40
- batFramework/gui/image.py +14 -14
- batFramework/gui/interactiveWidget.py +15 -0
- batFramework/gui/label.py +44 -27
- batFramework/gui/layout.py +23 -14
- batFramework/gui/meter.py +10 -7
- batFramework/gui/radioButton.py +1 -1
- batFramework/gui/shape.py +3 -25
- batFramework/gui/slider.py +40 -30
- batFramework/gui/textInput.py +160 -50
- batFramework/gui/toggle.py +20 -22
- batFramework/gui/widget.py +65 -32
- batFramework/manager.py +17 -5
- batFramework/object.py +17 -8
- batFramework/particle.py +18 -4
- batFramework/scene.py +1 -1
- batFramework/sceneManager.py +42 -13
- batFramework/stateMachine.py +9 -6
- batFramework/templates/__init__.py +2 -0
- batFramework/templates/character.py +44 -0
- batFramework/templates/states.py +166 -0
- batFramework/time.py +30 -9
- batFramework/transition.py +2 -2
- batFramework/triggerZone.py +1 -1
- batFramework/utils.py +35 -6
- {batframework-1.0.8a4.dist-info → batframework-1.0.8a6.dist-info}/METADATA +3 -15
- batframework-1.0.8a6.dist-info/RECORD +62 -0
- {batframework-1.0.8a4.dist-info → batframework-1.0.8a6.dist-info}/WHEEL +1 -1
- batframework-1.0.8a4.dist-info/RECORD +0 -58
- {batframework-1.0.8a4.dist-info → batframework-1.0.8a6.dist-info}/LICENCE +0 -0
- {batframework-1.0.8a4.dist-info → batframework-1.0.8a6.dist-info}/top_level.txt +0 -0
batFramework/gui/layout.py
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
import batFramework as bf
|
2
2
|
from .widget import Widget
|
3
3
|
from .constraints.constraints import *
|
4
|
-
from typing import Self, TYPE_CHECKING
|
4
|
+
from typing import Self, TYPE_CHECKING,override
|
5
|
+
from abc import ABC,abstractmethod
|
5
6
|
import pygame
|
6
7
|
|
7
8
|
if TYPE_CHECKING:
|
8
9
|
from .container import Container
|
9
10
|
|
10
11
|
|
11
|
-
class Layout:
|
12
|
+
class Layout(ABC):
|
12
13
|
def __init__(self, parent: "Container" = None):
|
13
14
|
self.parent = parent
|
14
15
|
self.child_constraints: list[Constraint] = []
|
@@ -48,18 +49,10 @@ class Layout:
|
|
48
49
|
return target_size
|
49
50
|
|
50
51
|
def focus_next_child(self) -> None:
|
51
|
-
|
52
|
-
self.parent.focused_index = min(self.parent.focused_index + 1, len(l) - 1)
|
53
|
-
focused = l[self.parent.focused_index]
|
54
|
-
focused.get_focus()
|
55
|
-
self.scroll_to_widget(focused)
|
52
|
+
pass
|
56
53
|
|
57
54
|
def focus_prev_child(self) -> None:
|
58
|
-
|
59
|
-
self.parent.focused_index = max(self.parent.focused_index - 1, 0)
|
60
|
-
focused = l[self.parent.focused_index]
|
61
|
-
focused.get_focus()
|
62
|
-
self.scroll_to_widget(focused)
|
55
|
+
pass
|
63
56
|
|
64
57
|
def scroll_to_widget(self, widget: Widget) -> None:
|
65
58
|
padded = self.parent.get_padded_rect()
|
@@ -74,7 +67,23 @@ class Layout:
|
|
74
67
|
pass
|
75
68
|
|
76
69
|
|
77
|
-
class
|
70
|
+
class SingleAxisLayout(Layout):
|
71
|
+
def focus_next_child(self) -> None:
|
72
|
+
l = self.parent.get_interactive_children()
|
73
|
+
self.parent.focused_index = min(self.parent.focused_index + 1, len(l) - 1)
|
74
|
+
focused = l[self.parent.focused_index]
|
75
|
+
focused.get_focus()
|
76
|
+
self.scroll_to_widget(focused)
|
77
|
+
|
78
|
+
def focus_prev_child(self) -> None:
|
79
|
+
l = self.parent.get_interactive_children()
|
80
|
+
self.parent.focused_index = max(self.parent.focused_index - 1, 0)
|
81
|
+
focused = l[self.parent.focused_index]
|
82
|
+
focused.get_focus()
|
83
|
+
self.scroll_to_widget(focused)
|
84
|
+
|
85
|
+
|
86
|
+
class Column(SingleAxisLayout):
|
78
87
|
def __init__(self, gap: int = 0, spacing: bf.spacing = bf.spacing.MANUAL):
|
79
88
|
super().__init__()
|
80
89
|
self.gap = gap
|
@@ -157,7 +166,7 @@ class Column(Layout):
|
|
157
166
|
self.parent.clamp_scroll()
|
158
167
|
|
159
168
|
|
160
|
-
class Row(
|
169
|
+
class Row(SingleAxisLayout):
|
161
170
|
def __init__(self, gap: int = 0, spacing: bf.spacing = bf.spacing.MANUAL):
|
162
171
|
super().__init__()
|
163
172
|
self.gap = gap
|
batFramework/gui/meter.py
CHANGED
@@ -17,9 +17,11 @@ class Meter(Shape):
|
|
17
17
|
self.snap: bool = False
|
18
18
|
self.value = self.max_value
|
19
19
|
self.content = Shape((0, 0)).set_color(bf.color.BLUE)
|
20
|
+
self.content.set_debug_color("cyan")
|
20
21
|
self.content.top_at = lambda x, y: custom_top_at(self.content, x, y)
|
21
22
|
self.add(self.content)
|
22
|
-
self.set_padding(
|
23
|
+
self.set_padding(4)
|
24
|
+
self.set_color("gray20")
|
23
25
|
self.set_outline_width(1)
|
24
26
|
self.set_outline_color(bf.color.BLACK)
|
25
27
|
self.set_debug_color("pink")
|
@@ -38,18 +40,19 @@ class Meter(Shape):
|
|
38
40
|
f"[Warning] : minimum value {range_min} is greater than or equal to maximum value {range_max}"
|
39
41
|
)
|
40
42
|
return self
|
41
|
-
self.min_value
|
43
|
+
self.min_value = range_min
|
44
|
+
self.max_value = range_max
|
42
45
|
self.dirty_shape = True
|
43
46
|
|
44
47
|
def get_debug_outlines(self):
|
45
48
|
yield from super().get_debug_outlines()
|
46
|
-
yield from self.content.get_debug_outlines()
|
49
|
+
# yield from self.content.get_debug_outlines()
|
47
50
|
|
48
51
|
def set_value(self, value: float) -> Self:
|
49
52
|
value = max(self.min_value, min(self.max_value, value))
|
50
53
|
value = round(value / self.step) * self.step
|
51
|
-
self.value = value
|
52
|
-
self.
|
54
|
+
self.value = round(value,10)
|
55
|
+
self.dirty_shape = True
|
53
56
|
return self
|
54
57
|
|
55
58
|
def get_value(self) -> float:
|
@@ -59,12 +62,12 @@ class Meter(Shape):
|
|
59
62
|
return self.max_value - self.min_value
|
60
63
|
|
61
64
|
def get_ratio(self) -> float:
|
62
|
-
return self.value / (self.max_value - self.min_value)
|
65
|
+
return (self.value-self.min_value) / (self.max_value - self.min_value)
|
63
66
|
|
64
67
|
def _build_content(self) -> None:
|
65
68
|
width = self.get_padded_width() * self.get_ratio()
|
66
69
|
self.content.set_size((width, self.get_padded_height()))
|
67
|
-
self.content.
|
70
|
+
self.content.rect.topleft = self.get_padded_rect().topleft
|
68
71
|
|
69
72
|
def build(self) -> None:
|
70
73
|
super().build()
|
batFramework/gui/radioButton.py
CHANGED
@@ -7,7 +7,7 @@ class RadioVariable: ...
|
|
7
7
|
|
8
8
|
|
9
9
|
class RadioButton(Toggle):
|
10
|
-
def __init__(self, text: str, radio_value: Any = None) -> None:
|
10
|
+
def __init__(self, text: str = "", radio_value: Any = None) -> None:
|
11
11
|
super().__init__(text, None, False)
|
12
12
|
self.radio_value: Any = (
|
13
13
|
radio_value if radio_value is not None else text if text else None
|
batFramework/gui/shape.py
CHANGED
@@ -43,15 +43,10 @@ class Shape(Widget):
|
|
43
43
|
def set_relief(self, relief: int) -> Self:
|
44
44
|
if relief < 0:
|
45
45
|
return self
|
46
|
-
|
47
|
-
return self
|
46
|
+
self.dirty_shape = self.relief != relief
|
48
47
|
self.relief = relief
|
49
|
-
self.dirty_shape = True
|
50
48
|
return self
|
51
49
|
|
52
|
-
def get_relief(self) -> int:
|
53
|
-
return self.relief
|
54
|
-
|
55
50
|
def set_texture(
|
56
51
|
self, surface: pygame.SurfaceType, subsize: tuple[int, int] | None = None
|
57
52
|
) -> Self:
|
@@ -223,8 +218,8 @@ class Shape(Widget):
|
|
223
218
|
return pygame.FRect(0, self.rect.h - self.relief, self.rect.w, self.relief)
|
224
219
|
|
225
220
|
def _paint_shape(self) -> None:
|
221
|
+
self.surface.fill((0, 0, 0, 0))
|
226
222
|
if self.relief!=0:
|
227
|
-
self.surface.fill((0, 0, 0, 0))
|
228
223
|
self.surface.fill(self.shadow_color, self._get_base_rect())
|
229
224
|
self.surface.fill(self.color, self._get_elevated_rect())
|
230
225
|
else:
|
@@ -242,27 +237,10 @@ class Shape(Widget):
|
|
242
237
|
pygame.draw.rect(self.surface, self.color, e, 0, *self.border_radius)
|
243
238
|
|
244
239
|
def _paint_outline(self) -> None:
|
245
|
-
if self.relief:
|
246
|
-
pygame.draw.rect(
|
247
|
-
self.surface,
|
248
|
-
self.outline_color,
|
249
|
-
(
|
250
|
-
0,
|
251
|
-
self.relief - self.get_relief(),
|
252
|
-
self.rect.w,
|
253
|
-
self.rect.h - self.relief,
|
254
|
-
),
|
255
|
-
self.outline_width,
|
256
|
-
)
|
257
240
|
pygame.draw.rect(
|
258
241
|
self.surface,
|
259
242
|
self.outline_color,
|
260
|
-
(
|
261
|
-
0,
|
262
|
-
self.relief - self.get_relief(),
|
263
|
-
self.rect.w,
|
264
|
-
self.rect.h - (self.relief - self.get_relief()),
|
265
|
-
),
|
243
|
+
self._get_elevated_rect(),
|
266
244
|
self.outline_width,
|
267
245
|
)
|
268
246
|
|
batFramework/gui/slider.py
CHANGED
@@ -12,11 +12,13 @@ class SliderHandle(Indicator, DraggableWidget):
|
|
12
12
|
super().__init__()
|
13
13
|
self.set_color(bf.color.CLOUD_SHADE)
|
14
14
|
self.old_key_repeat: tuple = (0, 0)
|
15
|
-
|
15
|
+
self.parent : bf.ClickableWidget = self.parent
|
16
16
|
def __str__(self) -> str:
|
17
17
|
return "SliderHandle"
|
18
18
|
|
19
19
|
def on_click_down(self, button: int) -> None:
|
20
|
+
if not self.parent.is_enabled():
|
21
|
+
return
|
20
22
|
super().on_click_down(button)
|
21
23
|
if button == 1:
|
22
24
|
self.parent.get_focus()
|
@@ -28,6 +30,8 @@ class SliderHandle(Indicator, DraggableWidget):
|
|
28
30
|
def do_on_drag(
|
29
31
|
self, drag_start: tuple[float, float], drag_end: tuple[float, float]
|
30
32
|
) -> None:
|
33
|
+
if not self.parent.is_enabled():
|
34
|
+
return
|
31
35
|
super().do_on_drag(drag_start, drag_end)
|
32
36
|
m: Meter = self.parent.meter
|
33
37
|
r = m.get_padded_rect()
|
@@ -43,21 +47,20 @@ class SliderHandle(Indicator, DraggableWidget):
|
|
43
47
|
|
44
48
|
|
45
49
|
class SliderMeter(Meter, InteractiveWidget):
|
46
|
-
|
47
50
|
def __str__(self) -> str:
|
48
51
|
return "SliderMeter"
|
49
52
|
|
50
53
|
def on_click_down(self, button: int) -> None:
|
54
|
+
if not self.parent.is_enabled():
|
55
|
+
return
|
51
56
|
if button == 1:
|
52
57
|
self.parent.get_focus()
|
53
58
|
r = self.get_root()
|
54
59
|
if r:
|
55
60
|
pos = r.drawing_camera.screen_to_world(pygame.mouse.get_pos())[0]
|
56
61
|
self.parent.set_value(self.parent.position_to_value(pos))
|
57
|
-
|
58
62
|
self.do_on_click_down(button)
|
59
63
|
|
60
|
-
|
61
64
|
class Slider(Button):
|
62
65
|
def __init__(self, text: str, default_value: float = 1.0) -> None:
|
63
66
|
super().__init__(text, None)
|
@@ -92,13 +95,6 @@ class Slider(Button):
|
|
92
95
|
super().do_on_lose_focus()
|
93
96
|
pygame.key.set_repeat(*self.old_key_repeat)
|
94
97
|
|
95
|
-
def get_min_required_size(self) -> tuple[float, float]:
|
96
|
-
gap = self.gap if self.text else 0
|
97
|
-
if not self.text_rect:
|
98
|
-
self.text_rect.size = self._get_text_rect_required_size()
|
99
|
-
w, h = self.text_rect.size
|
100
|
-
return self.inflate_rect_by_padding((0, 0, w + gap + self.meter.rect.w, h)).size
|
101
|
-
|
102
98
|
def set_spacing(self, spacing: bf.spacing) -> Self:
|
103
99
|
if spacing == self.spacing:
|
104
100
|
return self
|
@@ -112,10 +108,12 @@ class Slider(Button):
|
|
112
108
|
|
113
109
|
def set_range(self, range_min: float, range_max: float) -> Self:
|
114
110
|
self.meter.set_range(range_min, range_max)
|
111
|
+
self.dirty_shape = True
|
115
112
|
return self
|
116
113
|
|
117
114
|
def set_step(self, step: float) -> Self:
|
118
115
|
self.meter.set_step(step)
|
116
|
+
self.dirty_shape = True
|
119
117
|
return self
|
120
118
|
|
121
119
|
def set_value(self, value, no_callback: bool = False) -> Self:
|
@@ -130,12 +128,16 @@ class Slider(Button):
|
|
130
128
|
return self.meter.get_value()
|
131
129
|
|
132
130
|
def do_on_key_down(self, key):
|
131
|
+
if not self.is_enabled():
|
132
|
+
return
|
133
133
|
if key == pygame.K_RIGHT:
|
134
134
|
self.set_value(self.meter.get_value() + self.meter.step)
|
135
135
|
elif key == pygame.K_LEFT:
|
136
136
|
self.set_value(self.meter.get_value() - self.meter.step)
|
137
137
|
|
138
138
|
def do_on_click_down(self, button) -> None:
|
139
|
+
if not self.is_enabled():
|
140
|
+
return
|
139
141
|
if button == 1:
|
140
142
|
self.get_focus()
|
141
143
|
|
@@ -168,22 +170,34 @@ class Slider(Button):
|
|
168
170
|
value = self.meter.min_value + position_ratio * value_range
|
169
171
|
return round(value / self.meter.step) * self.meter.step
|
170
172
|
|
173
|
+
def get_min_required_size(self) -> tuple[float, float]:
|
174
|
+
gap = self.gap if self.text else 0
|
175
|
+
if not self.text_rect:
|
176
|
+
self.text_rect.size = self._get_text_rect_required_size()
|
177
|
+
w, h = self.text_rect.size
|
178
|
+
return self.inflate_rect_by_padding((0, 0, w + gap + self.meter.get_min_required_size()[1], h)).size
|
179
|
+
|
171
180
|
def _build_layout(self) -> None:
|
172
181
|
|
173
182
|
gap = self.gap if self.text else 0
|
174
|
-
|
175
183
|
self.text_rect.size = self._get_text_rect_required_size()
|
176
184
|
|
177
|
-
|
185
|
+
#right part size
|
186
|
+
meter_width = self.text_rect.h * 10
|
178
187
|
if not self.autoresize_w:
|
179
|
-
|
188
|
+
meter_width = self.get_padded_width() - self.text_rect.w - gap
|
189
|
+
right_part_height = min(self.text_rect.h, self.font_object.point_size)
|
190
|
+
self.meter.set_size_if_autoresize((meter_width,right_part_height))
|
191
|
+
self.handle.set_size_if_autoresize((None,right_part_height))
|
180
192
|
|
181
|
-
|
182
|
-
|
193
|
+
|
194
|
+
#join left and right
|
195
|
+
joined_rect = pygame.FRect(
|
196
|
+
0, 0, self.text_rect.w + gap + meter_width, self.text_rect.h
|
183
197
|
)
|
184
198
|
|
185
199
|
if self.autoresize_h or self.autoresize_w:
|
186
|
-
target_rect = self.inflate_rect_by_padding(
|
200
|
+
target_rect = self.inflate_rect_by_padding(joined_rect)
|
187
201
|
if not self.autoresize_w:
|
188
202
|
target_rect.w = self.rect.w
|
189
203
|
if not self.autoresize_h:
|
@@ -194,14 +208,14 @@ class Slider(Button):
|
|
194
208
|
return
|
195
209
|
|
196
210
|
# ------------------------------------ size is ok
|
211
|
+
|
212
|
+
|
213
|
+
offset = self._get_outline_offset() if self.show_text_outline else (0,0)
|
197
214
|
padded_rect = self.get_padded_rect()
|
198
215
|
padded_relative = padded_rect.move(-self.rect.x, -self.rect.y)
|
199
216
|
|
200
|
-
|
201
|
-
self.
|
202
|
-
|
203
|
-
self.align_text(tmp_rect, padded_relative, self.alignment)
|
204
|
-
self.text_rect.midleft = tmp_rect.midleft
|
217
|
+
self.align_text(joined_rect, padded_relative.move( offset), self.alignment)
|
218
|
+
self.text_rect.midleft = joined_rect.midleft
|
205
219
|
|
206
220
|
if self.text:
|
207
221
|
match self.spacing:
|
@@ -212,17 +226,13 @@ class Slider(Button):
|
|
212
226
|
|
213
227
|
# place meter
|
214
228
|
|
215
|
-
self.
|
216
|
-
|
217
|
-
self.rect.
|
218
|
-
self.rect.y + (self.text_rect.h / 2) - meter_size[1] / 2,
|
229
|
+
pos = self.text_rect.move(
|
230
|
+
self.rect.x + gap -offset[0],
|
231
|
+
self.rect.y + (self.text_rect.h / 2) - (right_part_height / 2) -offset[1],
|
219
232
|
).topright
|
220
|
-
|
233
|
+
self.meter.rect.topleft = pos
|
221
234
|
# place handle
|
222
235
|
|
223
|
-
# print(self.meter.rect.top - self.rect.top)
|
224
|
-
# print(self.meter.rect.h)
|
225
|
-
|
226
236
|
x = self.value_to_position(self.meter.value)
|
227
237
|
r = self.meter.get_padded_rect()
|
228
238
|
self.handle.set_center(x, r.centery)
|
batFramework/gui/textInput.py
CHANGED
@@ -4,27 +4,77 @@ from .label import Label
|
|
4
4
|
from .interactiveWidget import InteractiveWidget
|
5
5
|
import pygame
|
6
6
|
|
7
|
+
def find_next_word(s: str, start_index: int) -> int:
|
8
|
+
length = len(s)
|
9
|
+
# Ensure the starting index is within the bounds of the string
|
10
|
+
if start_index < 0 or start_index >= length:
|
11
|
+
raise ValueError("Starting index is out of bounds")
|
12
|
+
index = start_index
|
13
|
+
# If the start_index is at a space, skip leading spaces
|
14
|
+
if s[index] in [' ','\n']:
|
15
|
+
while index < length and s[index] in [' ','\n']:
|
16
|
+
index += 1
|
17
|
+
# If we've reached the end of the string
|
18
|
+
if index >= length:
|
19
|
+
return -1
|
20
|
+
else:
|
21
|
+
# If the start_index is within a word, move to the end of that word
|
22
|
+
while index < length and s[index] not in [' ','\n']:
|
23
|
+
index += 1
|
24
|
+
if index == length:
|
25
|
+
return index
|
26
|
+
# Return the index of the start of the next word or -1 if no more words are found
|
27
|
+
return index if index < length else -1
|
28
|
+
|
29
|
+
def find_prev_word(s: str, start_index: int) -> int:
|
30
|
+
if start_index <= 0 : return 0
|
31
|
+
length = len(s)
|
32
|
+
|
33
|
+
# Ensure the starting index is within the bounds of the string
|
34
|
+
if start_index < 0 or start_index >= length:
|
35
|
+
raise ValueError("Starting index is out of bounds")
|
36
|
+
|
37
|
+
index = start_index
|
38
|
+
|
39
|
+
# If the start_index is at a space, skip trailing spaces
|
40
|
+
if s[index] in [' ', '\n']:
|
41
|
+
while index > 0 and s[index-1] in [' ', '\n']:
|
42
|
+
index -= 1
|
43
|
+
# If we've reached the beginning of the string
|
44
|
+
if index <= 0:
|
45
|
+
return 0 if s[0] not in [' ', '\n'] else -1
|
46
|
+
else:
|
47
|
+
# If the start_index is within a word, move to the start of that word
|
48
|
+
while index > 0 and s[index-1] not in [' ', '\n']:
|
49
|
+
index -= 1
|
50
|
+
if index == 0 and s[index] not in [' ', '\n']:
|
51
|
+
return 0
|
52
|
+
|
53
|
+
# Return the index of the start of the previous word or -1 if no more words are found
|
54
|
+
return index if index > 0 or (index == 0 and s[0] not in [' ', '\n']) else -1
|
55
|
+
|
7
56
|
|
8
57
|
class TextInput(Label, InteractiveWidget):
|
9
58
|
def __init__(self) -> None:
|
10
|
-
self.cursor_position = 0
|
11
|
-
self.old_key_repeat
|
59
|
+
self.cursor_position = (0, 0)
|
60
|
+
self.old_key_repeat = (0, 0)
|
12
61
|
self.cursor_timer = bf.Timer(0.3, self._cursor_toggle, loop=True).start()
|
13
62
|
self.cursor_timer.pause()
|
14
|
-
self.show_cursor
|
63
|
+
self.show_cursor = False
|
15
64
|
self.on_modify: Callable[[str], str] = None
|
16
65
|
self.set_focusable(True)
|
17
66
|
self.set_outline_color("black")
|
18
67
|
super().__init__("")
|
68
|
+
self.alignment = bf.alignment.TOPLEFT
|
19
69
|
|
20
70
|
def set_modify_callback(self, callback: Callable[[str], str]) -> Self:
|
21
71
|
self.on_modify = callback
|
22
72
|
return self
|
23
73
|
|
24
74
|
def __str__(self) -> str:
|
25
|
-
return f"TextInput({self.text})"
|
75
|
+
return f"TextInput({repr(self.text)})"
|
26
76
|
|
27
|
-
def _cursor_toggle(self, value: bool = None):
|
77
|
+
def _cursor_toggle(self, value: bool | None = None):
|
28
78
|
if value is None:
|
29
79
|
value = not self.show_cursor
|
30
80
|
self.show_cursor = value
|
@@ -44,7 +94,6 @@ class TextInput(Label, InteractiveWidget):
|
|
44
94
|
def do_on_get_focus(self):
|
45
95
|
self.cursor_timer.resume()
|
46
96
|
self._cursor_toggle(True)
|
47
|
-
self.set_cursor_position(len(self.get_text()))
|
48
97
|
self.old_key_repeat = pygame.key.get_repeat()
|
49
98
|
pygame.key.set_repeat(200, 50)
|
50
99
|
|
@@ -53,42 +102,105 @@ class TextInput(Label, InteractiveWidget):
|
|
53
102
|
self._cursor_toggle(False)
|
54
103
|
pygame.key.set_repeat(*self.old_key_repeat)
|
55
104
|
|
56
|
-
def
|
57
|
-
if
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
105
|
+
def get_line(self, line: int) -> str | None:
|
106
|
+
if line < 0:
|
107
|
+
return None
|
108
|
+
lines = self.text.split('\n')
|
109
|
+
if line >= len(lines):
|
110
|
+
return None
|
111
|
+
return lines[line]
|
112
|
+
|
113
|
+
def set_cursor_position(self, position: tuple[int, int]) -> Self:
|
114
|
+
x, y = position
|
115
|
+
|
116
|
+
lines = self.text.split('\n')
|
117
|
+
y = max(0, min(y, len(lines) - 1))
|
118
|
+
line_length = len(lines[y])
|
119
|
+
x = max(0, min(x, line_length))
|
120
|
+
|
121
|
+
self.cursor_position = (x, y)
|
62
122
|
self.show_cursor = True
|
63
|
-
self.
|
64
|
-
if self.text_rect.w > self.get_padded_width():
|
65
|
-
self.dirty_shape = True
|
123
|
+
self.dirty_shape = True
|
66
124
|
return self
|
67
125
|
|
126
|
+
def cursor_to_absolute(self, position: tuple[int, int]) -> int:
|
127
|
+
x, y = position
|
128
|
+
|
129
|
+
y = max(0, min(y, len(self.text.split('\n')) - 1))
|
130
|
+
lines = self.text.split('\n')
|
131
|
+
x = max(0, min(x, len(lines[y])))
|
132
|
+
|
133
|
+
absolute_position = sum(len(line) + 1 for line in lines[:y]) + x
|
134
|
+
return absolute_position
|
135
|
+
|
136
|
+
def absolute_to_cursor(self, absolute: int) -> tuple[int, int]:
|
137
|
+
text = self.text
|
138
|
+
lines = text.split('\n')
|
139
|
+
current_pos = 0
|
140
|
+
|
141
|
+
for line_no, line in enumerate(lines):
|
142
|
+
if absolute <= current_pos + len(line):
|
143
|
+
return (absolute - current_pos, line_no)
|
144
|
+
current_pos += len(line) + 1
|
145
|
+
|
146
|
+
return (len(lines[-1]), len(lines) - 1)
|
147
|
+
|
68
148
|
def do_handle_event(self, event):
|
69
149
|
if not self.is_focused:
|
70
150
|
return
|
71
|
-
text = self.get_text()
|
72
|
-
cursor_position = self.cursor_position
|
73
151
|
|
152
|
+
if event.type not in [pygame.TEXTINPUT, pygame.KEYDOWN]:
|
153
|
+
return
|
154
|
+
|
155
|
+
text = self.get_text()
|
156
|
+
current = self.cursor_to_absolute(self.cursor_position)
|
74
157
|
if event.type == pygame.TEXTINPUT:
|
75
|
-
self.set_text(text[:
|
76
|
-
self.set_cursor_position(
|
158
|
+
self.set_text(text[:current] + event.text + text[current:])
|
159
|
+
self.set_cursor_position(self.absolute_to_cursor(current + len(event.text)))
|
77
160
|
elif event.type == pygame.KEYDOWN:
|
161
|
+
pressed = pygame.key.get_pressed()
|
78
162
|
if event.key == pygame.K_ESCAPE:
|
79
163
|
self.lose_focus()
|
80
|
-
|
81
164
|
elif event.key == pygame.K_BACKSPACE:
|
82
|
-
if
|
83
|
-
self.set_text(text[:
|
84
|
-
self.set_cursor_position(
|
85
|
-
|
165
|
+
if current > 0:
|
166
|
+
self.set_text(text[:current - 1] + text[current:])
|
167
|
+
self.set_cursor_position(self.absolute_to_cursor(current - 1))
|
168
|
+
elif event.key == pygame.K_DELETE:
|
169
|
+
if current < len(text):
|
170
|
+
self.set_text(text[:current] + text[current + 1:])
|
86
171
|
elif event.key == pygame.K_RIGHT:
|
87
|
-
self.
|
88
|
-
|
172
|
+
if self.cursor_to_absolute(self.cursor_position)>=len(self.text):
|
173
|
+
return
|
174
|
+
if self.cursor_position[0] == len(self.get_line(self.cursor_position[1])):
|
175
|
+
self.set_cursor_position((0, self.cursor_position[1] + 1))
|
176
|
+
else:
|
177
|
+
if pressed[pygame.K_LCTRL] or pressed[pygame.K_RCTRL]:
|
178
|
+
index = find_next_word(self.text,current)
|
179
|
+
if index ==-1 : index = current+1
|
180
|
+
self.set_cursor_position(self.absolute_to_cursor(index))
|
181
|
+
else:
|
182
|
+
self.set_cursor_position(self.absolute_to_cursor(current + 1))
|
89
183
|
elif event.key == pygame.K_LEFT:
|
90
|
-
self.set_cursor_position(cursor_position - 1)
|
91
184
|
|
185
|
+
|
186
|
+
if self.cursor_position[0] == 0 and self.cursor_position[1] > 0:
|
187
|
+
self.set_cursor_position((len(self.get_line(self.cursor_position[1] - 1)), self.cursor_position[1] - 1))
|
188
|
+
else:
|
189
|
+
if pressed[pygame.K_LCTRL] or pressed[pygame.K_RCTRL]:
|
190
|
+
index = find_prev_word(self.text,current-1)
|
191
|
+
if index ==-1 : index = current-1
|
192
|
+
self.set_cursor_position(self.absolute_to_cursor(index))
|
193
|
+
else:
|
194
|
+
self.set_cursor_position(self.absolute_to_cursor(current - 1))
|
195
|
+
elif event.key == pygame.K_UP:
|
196
|
+
x, y = self.cursor_position
|
197
|
+
self.set_cursor_position((x, y - 1))
|
198
|
+
elif event.key == pygame.K_DOWN:
|
199
|
+
x, y = self.cursor_position
|
200
|
+
self.set_cursor_position((x, y + 1))
|
201
|
+
elif event.key == pygame.K_RETURN:
|
202
|
+
self.set_text(text[:current] + '\n' + text[current:])
|
203
|
+
self.set_cursor_position(self.absolute_to_cursor(current + 1))
|
92
204
|
else:
|
93
205
|
return
|
94
206
|
else:
|
@@ -104,34 +216,32 @@ class TextInput(Label, InteractiveWidget):
|
|
104
216
|
def _paint_cursor(self) -> None:
|
105
217
|
if not self.font_object or not self.show_cursor:
|
106
218
|
return
|
107
|
-
partial_text_size = self.font_object.size(
|
108
|
-
self.get_text()[: self.cursor_position]
|
109
|
-
)
|
110
219
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
220
|
+
lines = self.text.split('\n')
|
221
|
+
line_x, line_y = self.cursor_position
|
222
|
+
|
223
|
+
cursor_y = self.padding[1]
|
224
|
+
cursor_y += line_y * self.font_object.get_linesize()
|
225
|
+
cursor_x = self.padding[0]
|
226
|
+
cursor_x += self.font_object.size(lines[line_y][:line_x])[0] if line_x > 0 else 0
|
116
227
|
|
228
|
+
cursor_rect = pygame.Rect(cursor_x, cursor_y, 2, self.font_object.get_height())
|
117
229
|
pygame.draw.rect(self.surface, self.text_color, cursor_rect)
|
118
230
|
|
119
231
|
def paint(self) -> None:
|
120
232
|
super().paint()
|
121
233
|
self._paint_cursor()
|
122
234
|
|
123
|
-
def
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
elif self.text_rect.x + w < area.left:
|
137
|
-
self.text_rect.left = area.left - w
|
235
|
+
# def set_alignment(self, alignment: bf.alignment) -> Self:
|
236
|
+
# return self
|
237
|
+
|
238
|
+
# def align_text(
|
239
|
+
# self, text_rect: pygame.FRect, area: pygame.FRect, alignment: bf.alignment
|
240
|
+
# ):
|
241
|
+
# if alignment == bf.alignment.LEFT:
|
242
|
+
# alignment = bf.alignment.MIDLEFT
|
243
|
+
# elif alignment == bf.alignment.MIDRIGHT:
|
244
|
+
# alignment = bf.alignment.MIDRIGHT
|
245
|
+
# pos = area.__getattribute__(alignment.value)
|
246
|
+
# text_rect.__setattr__(alignment.value, pos)
|
247
|
+
# return
|