55
66Shows how to use the TEXTEDITING and TEXTINPUT events.
77"""
8+ from typing import Tuple , List
89import sys
910import os
1011
1112import 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
176254class 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 :
0 commit comments