from contextlib import contextmanager
from dataclasses import dataclass
-from functools import partial
+from functools import cached_property, partial
from math import floor
from time import time
from typing import Optional, Callable
class TextInput(Focusable, Child):
- def __init__(self, parent, rect, callback, value="", value_filter=None):
+ LEFT = 0
+ RIGHT = 1
+ PADDING = 16
+
+ def __init__(
+ self,
+ parent,
+ rect,
+ callback,
+ value="",
+ value_filter=None,
+ unfocused_align=LEFT,
+ ):
super().__init__(parent)
+ assert rect.width > self.PADDING * 2
self.rect = rect
self.callback = callback
self.value = value
self.value_filter = value_filter
+ assert unfocused_align in (self.LEFT, self.RIGHT)
+ self.unfocused_align = unfocused_align
self.offset = 0
self.cursor = None
+ @cached_property
+ def padded_rect(self):
+ return pygame.Rect(
+ (self.rect.left + self.PADDING, self.rect.top),
+ (self.rect.width - self.PADDING * 2, self.rect.height),
+ )
+
@staticmethod
def maybe_scroll_font_surface(font, value_to_cursor, fs, width, height):
x = font.size(value_to_cursor)[0]
)
return fs, offset, x - offset
- def get_font_surface(self):
+ def get_focused_font_surface(self):
if self.cursor.pos > len(self.value):
self.cursor.pos = len(self.value)
- fs = self.font.render(self.value, True, "gray")
- rect = self.rect
- if self.value:
- fs, self.offset, x = self.maybe_scroll_font_surface(
- self.font,
- self.value[:self.cursor.pos],
- fs,
- rect.width - 24,
- rect.height,
- )
- if x == fs.get_width():
- x -= 1
- else:
- x = 0
+ if not self.value:
fs = pygame.Surface((1, self.font.size("W")[1]), pygame.SRCALPHA, 32)
+ pygame.draw.line(fs, "orange", (0, 0), (0, fs.get_height()))
+ return fs
+ fs = self.font.render(self.value, True, "gray")
+ fs, self.offset, x = self.maybe_scroll_font_surface(
+ self.font,
+ self.value[:self.cursor.pos],
+ fs,
+ *self.padded_rect.size,
+ )
+ if x == fs.get_width():
+ x -= 1
pygame.draw.line(fs, "orange", (x, 0), (x, fs.get_height()))
return fs
+ def get_unfocused_font_surface(self):
+ fs = self.font.render(self.value, True, "gray")
+ if self.unfocused_align == self.LEFT:
+ self.offset = 0
+ return fs
+ fs_size = fs.get_size()
+ if fs_size[0] <= self.padded_rect.width:
+ return fs
+ self.offset = fs_size[0] - self.padded_rect.width
+ return fs.subsurface(
+ pygame.Rect((self.offset, 0), (self.padded_rect.width, fs_size[1]))
+ )
+
def update(self):
if getattr(self.cursor, "key_callback", None) is None:
return
def draw(self):
pygame.draw.rect(self.surf, "black", self.rect)
if self.focused:
- fs = self.get_font_surface()
+ fs = self.get_focused_font_surface()
else:
- fs = self.font.render(self.value, True, "gray")
+ fs = self.get_unfocused_font_surface()
self.surf.subsurface(self.rect).blit(
- fs, (16, (self.rect.height - fs.get_height()) // 2)
+ fs, (self.PADDING, (self.rect.height - fs.get_height()) // 2)
)
pygame.draw.rect(self.surf, "gray", self.rect, 1)
def pos_from_offset(self, x_offset):
value = self.value
+ len_value = len(value)
if x_offset is None:
- return len(value)
+ return len_value
a, a_x = 0, 0
- b, b_x = len(value), self.font.size(value)[0]
+ b, b_x = len_value, self.font.size(value)[0]
if x_offset <= a_x:
return a
elif x_offset >= b_x:
c_x = self.font.size(value[:c])[0]
if c_x < x_offset:
a, a_x = c, c_x
- else:
+ elif c_x > x_offset:
b, b_x = c, c_x
- if abs(a_x - x_offset) < abs(b_x - x_offset):
- return a
- return b
+ else:
+ return c
+ return a if abs(a_x - x_offset) < abs(b_x - x_offset) else b
def handle_mousebuttondown(self, ev):
if ev.button != 1:
self.activate()
with self.check_dirty():
self.cursor.pos = self.pos_from_offset(
- ev.pos[0] - self.rect.left - 16 + self.offset
+ ev.pos[0] - self.padded_rect.left + self.offset
)
elif self.focused:
self.deactivate(True)