Skip to content

Commit b560863

Browse files
authored
Merge pull request #2588 from gresm/multiline-more-examples
Multiline more examples
2 parents c291e9e + 79c803e commit b560863

File tree

2 files changed

+185
-106
lines changed

2 files changed

+185
-106
lines changed

examples/textinput.py

Lines changed: 177 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55
66
Shows how to use the TEXTEDITING and TEXTINPUT events.
77
"""
8+
from typing import Tuple, List
89
import sys
910
import os
1011

1112
import pygame
12-
import pygame
13-
import pygame.freetype as freetype
1413

1514
# This environment variable is important
1615
# If not added the candidate list will not show
@@ -37,7 +36,13 @@ class TextInput:
3736
]
3837

3938
def __init__(
40-
self, prompt: str, pos, screen_dimensions, print_event: bool, text_color="white"
39+
self,
40+
prompt: str,
41+
pos: Tuple[int, int],
42+
screen_dimensions: Tuple[int, int],
43+
print_event: bool,
44+
text_color="white",
45+
fps: int = 50,
4146
) -> None:
4247
self.prompt = prompt
4348
self.print_event = print_event
@@ -53,124 +58,197 @@ def __init__(
5358
self._ime_text_pos = 0
5459
self._ime_editing_text = ""
5560
self._ime_editing_pos = 0
56-
self.chat_list = []
61+
self.chat = ""
5762

58-
# Freetype
5963
# The font name can be a comma separated list
6064
# of font names to search for.
61-
self.FONT_NAMES = ",".join(str(x) for x in self.FONT_NAMES)
62-
self.font = freetype.SysFont(self.FONT_NAMES, 24)
63-
self.font_small = freetype.SysFont(self.FONT_NAMES, 16)
65+
self.font_names = ",".join(self.FONT_NAMES)
66+
self.font = pygame.font.SysFont(self.font_names, 24)
67+
self.font_height = self.font.get_height()
68+
self.font_small = pygame.font.SysFont(self.font_names, 16)
6469
self.text_color = text_color
6570

71+
self.prompt_surf = self.font.render(self.prompt, True, self.text_color)
72+
self.prompt_rect = self.prompt_surf.get_rect(topleft=self.CHAT_BOX_POS.topleft)
73+
74+
self.fps = fps
75+
self.second_counter = 0
76+
6677
print("Using font: " + self.font.name)
6778

68-
def update(self, events) -> None:
79+
def update(self, events: List[pygame.Event]) -> None:
6980
"""
7081
Updates the text input widget
7182
"""
7283
for event in events:
73-
if event.type == pygame.KEYDOWN:
74-
if self.print_event:
75-
print(event)
84+
self.handle_event(event)
85+
86+
self.second_counter += 1
87+
88+
if self.second_counter >= self.fps:
89+
self.second_counter = 0
90+
91+
# Check if input fits in chat box
92+
input_size = self.font.size(self._get_ime_text())
93+
while input_size[0] > self.CHAT_BOX_POS.w - self.prompt_rect.w:
94+
if self._ime_editing_text:
95+
# Don't block.
96+
break
97+
self._ime_text = self._ime_text[:-1]
98+
input_size = self.font.size(self._get_ime_text())
99+
100+
def _clamp_to_text_range(self, num: int):
101+
return min(len(self._ime_text), max(0, num))
102+
103+
def move_cursor_by(self, by: int):
104+
self._ime_text_pos = self._clamp_to_text_range(self._ime_text_pos + by)
105+
106+
def replace_chars(
107+
self,
108+
remove_count: int = 0,
109+
to_insert: str = "",
110+
text_after_cursor: bool = False,
111+
):
112+
"""
113+
Removes given number of characters from the cursor location
114+
and adds an optional string there, then adjusts the cursor location.
115+
"""
116+
loc = self._clamp_to_text_range(remove_count + self._ime_text_pos)
76117

77-
if self._ime_editing:
78-
if len(self._ime_editing_text) == 0:
79-
self._ime_editing = False
80-
continue
118+
if remove_count < 0:
119+
self._ime_text = (
120+
self._ime_text[0:loc] + to_insert + self._ime_text[self._ime_text_pos :]
121+
)
122+
123+
if text_after_cursor:
124+
self.move_cursor_by(remove_count)
125+
else:
126+
self.move_cursor_by(remove_count + len(to_insert))
127+
else:
128+
self._ime_text = (
129+
self._ime_text[0 : self._ime_text_pos]
130+
+ to_insert
131+
+ self._ime_text[loc:]
132+
)
133+
134+
# Don't move cursor if not inserting text
135+
# after removing the characters in front of the cursor
136+
if not text_after_cursor:
137+
self.move_cursor_by(len(to_insert))
138+
139+
def handle_event(self, event: pygame.Event):
140+
"""
141+
Handle an event
142+
"""
143+
if self.print_event:
144+
print(event)
145+
146+
if event.type == pygame.KEYDOWN:
147+
if self._ime_editing:
148+
if len(self._ime_editing_text) == 0:
149+
self._ime_editing = False
150+
return
151+
152+
if event.key == pygame.K_BACKSPACE:
153+
self.replace_chars(-1)
154+
155+
elif event.key == pygame.K_DELETE:
156+
self.replace_chars(1)
157+
158+
elif event.key == pygame.K_LEFT:
159+
self.move_cursor_by(-1)
160+
161+
elif event.key == pygame.K_RIGHT:
162+
self.move_cursor_by(1)
163+
164+
# Handle ENTER key
165+
elif event.key in (pygame.K_RETURN, pygame.K_KP_ENTER):
166+
# Block if we have no text to append
167+
if len(self._ime_text) == 0:
168+
return
169+
170+
# Add to chat log
171+
self.chat += self._ime_text + "\n"
81172

82-
if event.key == pygame.K_BACKSPACE:
83-
if len(self._ime_text) > 0 and self._ime_text_pos > 0:
84-
self._ime_text = (
85-
self._ime_text[0 : self._ime_text_pos - 1]
86-
+ self._ime_text[self._ime_text_pos :]
87-
)
88-
self._ime_text_pos = max(0, self._ime_text_pos - 1)
89-
90-
elif event.key == pygame.K_DELETE:
91-
self._ime_text = (
92-
self._ime_text[0 : self._ime_text_pos]
93-
+ self._ime_text[self._ime_text_pos + 1 :]
94-
)
95-
elif event.key == pygame.K_LEFT:
96-
self._ime_text_pos = max(0, self._ime_text_pos - 1)
97-
elif event.key == pygame.K_RIGHT:
98-
self._ime_text_pos = min(
99-
len(self._ime_text), self._ime_text_pos + 1
100-
)
101-
# Handle ENTER key
102-
elif event.key in [pygame.K_RETURN, pygame.K_KP_ENTER]:
103-
# Block if we have no text to append
104-
if len(self._ime_text) == 0:
105-
continue
106-
107-
# Append chat list
108-
self.chat_list.append(self._ime_text)
109-
if len(self.chat_list) > self.CHAT_LIST_MAXSIZE:
110-
self.chat_list.pop(0)
111-
self._ime_text = ""
112-
self._ime_text_pos = 0
113-
114-
elif event.type == pygame.TEXTEDITING:
115-
if self.print_event:
116-
print(event)
117-
self._ime_editing = True
118-
self._ime_editing_text = event.text
119-
self._ime_editing_pos = event.start
120-
121-
elif event.type == pygame.TEXTINPUT:
122-
if self.print_event:
123-
print(event)
124-
self._ime_editing = False
125-
self._ime_editing_text = ""
126-
self._ime_text = (
127-
self._ime_text[0 : self._ime_text_pos]
128-
+ event.text
129-
+ self._ime_text[self._ime_text_pos :]
130-
)
131-
self._ime_text_pos += len(event.text)
173+
chat_lines = self.chat.split("\n")
174+
if len(chat_lines) > self.CHAT_LIST_MAXSIZE:
175+
chat_lines.pop(0)
176+
self.chat = "\n".join(chat_lines)
177+
178+
self._ime_text = ""
179+
self._ime_text_pos = 0
180+
181+
elif event.type == pygame.TEXTEDITING:
182+
self._ime_editing = True
183+
self._ime_editing_text = event.text
184+
self._ime_editing_pos = event.start
185+
186+
elif event.type == pygame.TEXTINPUT:
187+
self._ime_editing = False
188+
self._ime_editing_text = ""
189+
self.replace_chars(to_insert=event.text)
190+
191+
def _get_ime_text(self):
192+
"""
193+
Returns text that is currently in input.
194+
"""
195+
if self._ime_editing_text:
196+
return (
197+
f"{self._ime_text[0: self._ime_text_pos]}"
198+
f"[{self._ime_editing_text}]"
199+
f"{self._ime_text[self._ime_text_pos:]}"
200+
)
201+
return (
202+
f"{self._ime_text[0: self._ime_text_pos]}"
203+
f"{self._ime_text[self._ime_text_pos:]}"
204+
)
132205

133206
def draw(self, screen: pygame.Surface) -> None:
134207
"""
135208
Draws the text input widget onto the provided surface
136209
"""
137210

138-
# Chat List updates
139-
chat_height = self.CHAT_LIST_POS.height / self.CHAT_LIST_MAXSIZE
140-
for i, chat in enumerate(self.chat_list):
141-
self.font_small.render_to(
142-
screen,
143-
(self.CHAT_LIST_POS.x, self.CHAT_LIST_POS.y + i * chat_height),
144-
chat,
145-
self.text_color,
146-
)
211+
chat_list_surf = self.font_small.render(
212+
self.chat, True, self.text_color, wraplength=self.CHAT_LIST_POS.width
213+
)
214+
215+
screen.blit(chat_list_surf, self.CHAT_LIST_POS)
147216

148217
# Chat box updates
149-
start_pos = self.CHAT_BOX_POS.copy()
150-
ime_text_l = self.prompt + self._ime_text[0 : self._ime_text_pos]
151-
ime_text_m = (
152-
self._ime_editing_text[0 : self._ime_editing_pos]
153-
+ "|"
154-
+ self._ime_editing_text[self._ime_editing_pos :]
155-
)
156-
ime_text_r = self._ime_text[self._ime_text_pos :]
218+
cursor_loc = self._ime_text_pos + self._ime_editing_pos
219+
ime_text = self._get_ime_text()
157220

158-
rect_text_l = self.font.render_to(
159-
screen, start_pos, ime_text_l, self.text_color
160-
)
161-
start_pos.x += rect_text_l.width
162-
163-
# Editing texts should be underlined
164-
rect_text_m = self.font.render_to(
165-
screen,
166-
start_pos,
167-
ime_text_m,
168-
self.text_color,
169-
None,
170-
freetype.STYLE_UNDERLINE,
221+
text_surf = self.font.render(
222+
ime_text, True, self.text_color, wraplength=self.CHAT_BOX_POS.width
171223
)
172-
start_pos.x += rect_text_m.width
173-
self.font.render_to(screen, start_pos, ime_text_r, self.text_color)
224+
225+
text_rect = text_surf.get_rect(topleft=self.prompt_rect.topright)
226+
screen.blit(self.prompt_surf, self.prompt_rect)
227+
screen.blit(text_surf, text_rect)
228+
229+
# Show blinking cursor, blink twice a second.
230+
if self.second_counter * 2 < self.fps:
231+
# Characters can have different widths,
232+
# so calculating the correct location for the cursor is required.
233+
metrics = self.font.metrics(ime_text)
234+
x_location = 0
235+
236+
index = 0
237+
for metric in metrics:
238+
if metric is None:
239+
continue
240+
241+
_, _, _, _, x_advance = metric
242+
243+
if index >= cursor_loc:
244+
break
245+
x_location += x_advance
246+
index += 1
247+
248+
cursor_rect = pygame.Rect(
249+
x_location + text_rect.x, text_rect.y, 2, self.font_height
250+
)
251+
pygame.draw.rect(screen, self.text_color, cursor_rect)
174252

175253

176254
class Game:
@@ -201,6 +279,7 @@ def __init__(self, caption: str) -> None:
201279
screen_dimensions=(self.SCREEN_WIDTH, self.SCREEN_HEIGHT),
202280
print_event=self.print_event,
203281
text_color="green",
282+
fps=self.FPS,
204283
)
205284

206285
def main_loop(self) -> None:

examples/vgrade.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
method of importing numpy and pygame.surfarray. This method
77
will fail 'gracefully' if it is not available.
88
I've tried mixing in a lot of comments where the code might
9-
not be self explanatory, nonetheless it may still seem a bit
9+
not be self-explanatory, nonetheless it may still seem a bit
1010
strange. Learning to use numpy for images like this takes a
1111
bit of learning, but the payoff is extremely fast image
1212
manipulation in python.
@@ -40,7 +40,7 @@
4040

4141

4242
def stopwatch(message=None):
43-
"simple routine to time python code"
43+
"""simple routine to time python code"""
4444
global timer
4545
if not message:
4646
timer = pygame.time.get_ticks()
@@ -51,8 +51,8 @@ def stopwatch(message=None):
5151
timer = now
5252

5353

54-
def VertGradientColumn(surf, topcolor, bottomcolor):
55-
"creates a new 3d vertical gradient array"
54+
def vert_gradient_column(surf, topcolor, bottomcolor):
55+
"""creates a new 3d vertical gradient array"""
5656
topcolor = np.array(topcolor, copy=False)
5757
bottomcolor = np.array(bottomcolor, copy=False)
5858
diff = bottomcolor - topcolor
@@ -68,11 +68,11 @@ def VertGradientColumn(surf, topcolor, bottomcolor):
6868
return pygame.surfarray.map_array(surf, column)
6969

7070

71-
def DisplayGradient(surf):
72-
"choose random colors and show them"
71+
def display_gradient(surf):
72+
"""choose random colors and show them"""
7373
stopwatch()
7474
colors = np_random.randint(0, 255, (2, 3))
75-
column = VertGradientColumn(surf, colors[0], colors[1])
75+
column = vert_gradient_column(surf, colors[0], colors[1])
7676
pygame.surfarray.blit_array(surf, column)
7777
pygame.display.flip()
7878
stopwatch("Gradient:")
@@ -95,7 +95,7 @@ def main():
9595
if event.type in (pygame.QUIT, pygame.KEYDOWN, pygame.MOUSEBUTTONDOWN):
9696
break
9797
elif event.type == TIMER_EVENT:
98-
DisplayGradient(screen)
98+
display_gradient(screen)
9999

100100
pygame.quit()
101101

0 commit comments

Comments
 (0)