"""
This file contains the implementation of the class TextInput: a
single-line editable text field (a player name, a chat message, a
join code) driven by raw keyboard events.
Author: Alejandro Mujica (aledrums@gmail.com)
"""
from typing import Callable, Optional, Tuple
import pygame
from gale.input_handler import (
KEY_BACKSPACE,
KEY_DELETE,
KEY_LEFT,
KEY_RETURN,
KEY_RIGHT,
KeyboardData,
MouseClickData,
)
from gale.text import Text
from .theme import Theme
from .widget import Widget
[docs]
class TextInput(Widget):
"""
A single-line editable text field, for things like a player name,
a chat message, or a server address/join code. Needs raw keyboard
events rather than the confirm/navigate vocabulary, so its owner
(typically UIManager) must call handle_key directly while this
widget is focused (see wants_raw_keyboard).
Limitation: built on pygame's KEYDOWN.unicode, so it only supports
ASCII-range input, not full IME/composed-character entry. That is
enough for names, chat, and join codes, but not for every language.
Usage example:
name_input = TextInput(40, 40, 240, 32, initial_text="Player", max_length=16)
"""
def __init__(
self,
x: float,
y: float,
width: float,
height: float,
initial_text: str = "",
max_length: Optional[int] = None,
font: Optional[pygame.font.Font] = None,
on_submit: Optional[Callable[[str], None]] = None,
theme: Optional[Theme] = None,
) -> None:
"""
:param x: The field's x position.
:param y: The field's y position.
:param width: The field's width.
:param height: The field's height.
:param initial_text: The starting text. The default value is "".
:param max_length: Maximum number of characters accepted. The default value is None, meaning no limit.
:param font: The font used to render the text. The default value is None, so theme.font is used.
:param on_submit: Called with the current text when Enter is pressed. The default value is None.
:param theme: This widget's own theme. The default value is None.
"""
super().__init__(x, y, width, height, theme=theme)
self.text: str = initial_text
self.cursor_index: int = len(initial_text)
self.max_length: Optional[int] = max_length
self._font = font
self.on_submit: Optional[Callable[[str], None]] = on_submit
self.wants_raw_keyboard = True
self.focusable = True
[docs]
def handle_key(self, data: KeyboardData) -> bool:
"""
:param data: The keyboard event to react to. Only KEYDOWN events (data.pressed) cause a change.
:returns: Whether the key was consumed.
"""
if not self.enabled or not data.pressed:
return False
key = data.key
if key == KEY_BACKSPACE:
if self.cursor_index > 0:
self.text = (
self.text[: self.cursor_index - 1] + self.text[self.cursor_index :]
)
self.cursor_index -= 1
elif key == KEY_DELETE:
self.text = (
self.text[: self.cursor_index] + self.text[self.cursor_index + 1 :]
)
elif key == KEY_LEFT:
self.cursor_index = max(0, self.cursor_index - 1)
elif key == KEY_RIGHT:
self.cursor_index = min(len(self.text), self.cursor_index + 1)
elif key == KEY_RETURN:
if self.on_submit is not None:
self.on_submit(self.text)
elif data.unicode and data.unicode.isprintable():
if self.max_length is None or len(self.text) < self.max_length:
self.text = (
self.text[: self.cursor_index]
+ data.unicode
+ self.text[self.cursor_index :]
)
self.cursor_index += 1
return True
[docs]
def render(self, surface: pygame.Surface) -> None:
if not self.visible:
return
pygame.draw.rect(surface, self.theme.background_color, self.rect)
border_color = (
self.theme.focus_color if self.focused else self.theme.border_color
)
pygame.draw.rect(
surface, border_color, self.rect, max(1, self.theme.border_width)
)
font = self._font if self._font is not None else self.theme.font
text_obj = Text(
self.text,
font,
self.x + self.theme.padding,
self.rect.centery - font.get_linesize() // 2,
self.theme.text_color,
)
text_obj.render(surface)
if self.focused:
cursor_x = (
self.x
+ self.theme.padding
+ font.size(self.text[: self.cursor_index])[0]
)
pygame.draw.line(
surface,
self.theme.text_color,
(cursor_x, self.y + 4),
(cursor_x, self.y + self.height - 4),
)
[docs]
def on_mouse_click(
self, position: Tuple[float, float], data: MouseClickData
) -> bool:
if not self.enabled or not self.contains(position):
return False
if data.released:
self.cursor_index = len(self.text)
return True