Skip to content

Commit

Permalink
Input field (#263)
Browse files Browse the repository at this point in the history
Implement a text input field
  • Loading branch information
soerface authored Jan 4, 2025
1 parent 66af927 commit a03ae74
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 69 deletions.
32 changes: 22 additions & 10 deletions drinks_touch/elements/base_elm.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import annotations
import pygame
from pygame import Surface

Expand All @@ -17,6 +18,7 @@ def __init__(
):
if pos is None:
pos = (0, 0)
self.ts = 0
self.pos = pos
self._height = height
self._width = width
Expand Down Expand Up @@ -77,7 +79,10 @@ def screen_pos(self):
def box(self):
return self.screen_pos + (self.width, self.height)

def event(self, event, pos=None):
def event(self, event: pygame.event.Event, pos=None) -> "BaseElm" | None:
"""
Returns the element that consumed the event
"""
if pos is None and hasattr(event, "pos"):
pos = event.pos
if pos is None:
Expand All @@ -89,24 +94,31 @@ def event(self, event, pos=None):
)

for child in self.children:
if hasattr(event, "consumed") and event.consumed:
return
child.event(event, transformed_pos)
if consumed_by := child.event(event, transformed_pos):
return consumed_by

if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
if collides:
self.focus = True
event.consumed = True
return self
else:
self.focus = False

elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:

if self.focus and collides:
had_focus = self.focus
self.focus = False
if had_focus and collides:
if hasattr(self, "on_click"):
self.on_click(*transformed_pos)
event.consumed = True
self.focus = False
return self

def key_event(self, event: pygame.event.Event):
"""
Called when a keyboard key was pressed and the element is active
(active means: was clicked on before and therefore the ScreenManager
has set this element as active)
"""

@property
def visible(self):
Expand All @@ -120,9 +132,9 @@ def collides_with(self, pos: tuple[int, int]) -> bool:
and pygame.Rect(self.box).collidepoint(pos[0], pos[1])
)

def render(self, *args, **kwargs) -> Surface:
def render(self, *args, **kwargs) -> Surface | None:
surface = pygame.font.SysFont("monospace", 25).render(
"Return surface in render()", 1, (255, 0, 255)
"Return surface or None in render()", 1, (255, 0, 255)
)
return surface

Expand Down
42 changes: 0 additions & 42 deletions drinks_touch/elements/elm_list.py

This file was deleted.

103 changes: 103 additions & 0 deletions drinks_touch/elements/input_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from __future__ import annotations

import enum
import logging

import pygame

from config import Color, Font
from elements.base_elm import BaseElm
from screens.screen_manager import ScreenManager


logger = logging.getLogger(__name__)


class InputType(enum.Enum):
TEXT = "text"
NUMBER = "number"
POSITIVE_NUMBER = "positive_number"


NUMERIC = list(range(pygame.K_0, pygame.K_9 + 1))
ALPHABET = list(range(pygame.K_a, pygame.K_z + 1)) + [
223, # ß
228, # ä
246, # ö
252, # ü
]
ALPHA_NUMERIC = NUMERIC + ALPHABET


class InputField(BaseElm):

def __init__(self, *args, width, height, input_type=InputType.TEXT, **kwargs):
if input_type in [InputType.NUMBER, InputType.POSITIVE_NUMBER]:
self.valid_chars = NUMERIC + [
pygame.K_PERIOD,
pygame.K_COMMA,
]
if input_type == InputType.NUMBER:
self.valid_chars.append(pygame.K_MINUS)
self.max_decimal_places = kwargs.pop("max_decimal_places", None)
elif input_type == InputType.TEXT:
self.valid_chars = ALPHA_NUMERIC + [
pygame.K_SPACE,
pygame.K_COMMA,
pygame.K_PERIOD,
pygame.K_MINUS,
pygame.K_UNDERSCORE,
]

super().__init__(*args, **kwargs, width=width, height=height)
self.input_type = input_type
self.text = ""

def render(self, *args, **kwargs):
is_active = ScreenManager.get_instance().active_object is self
surface = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
pygame.draw.rect(
surface, Color.NAVBAR_BACKGROUND.value, (0, 0, self.width, self.height)
)
pygame.draw.rect(surface, Color.BLACK.value, (0, 0, self.width, self.height), 1)
pygame.draw.line(
surface,
Color.PRIMARY.value,
(0, self.height - 1),
(self.width, self.height - 1),
)
font = pygame.font.Font(Font.SANS_SERIF.value, self.height - 10)
text_surface = font.render(self.text, 1, Color.PRIMARY.value)
surface.blit(text_surface, (5, 0))
if is_active:
pygame.draw.rect(
surface, Color.PRIMARY.value, (0, 0, self.width, self.height), 1
)
if self.ts // 1 % 2 == 0:
x = text_surface.get_width() + 5
pygame.draw.line(
surface, Color.PRIMARY.value, (x, 5), (x, self.height - 5), 2
)
return surface

def key_event(self, event: pygame.event.Event):
if event.key == pygame.K_BACKSPACE:
self.text = self.text[:-1]
elif event.key in self.valid_chars:
char = event.unicode
if self.input_type in (InputType.NUMBER, InputType.POSITIVE_NUMBER):
if event.key == pygame.K_MINUS and self.text:
# only allow minus at the start
return
if event.key in (pygame.K_PERIOD, pygame.K_COMMA):
# only allow one decimal point, and convert comma to period
if "." in self.text:
return
char = "."
if "." in self.text and self.max_decimal_places is not None:
before_comma, _, after_comma = self.text.partition(".")
if len(after_comma) >= self.max_decimal_places:
return
self.text += char
else:
logger.info(f"Invalid key: {event.key}")
2 changes: 1 addition & 1 deletion drinks_touch/elements/progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def __default_tick(self, old_value, dt):
else:
return old_value

def render(self, dt) -> pygame.Surface:
def render(self, dt, *args, **kwargs) -> pygame.Surface:
if self.tick is not None:
self.value = self.tick(self.value, dt)

Expand Down
14 changes: 14 additions & 0 deletions drinks_touch/elements/spacer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from elements.base_elm import BaseElm


class Spacer(BaseElm):
"""
Doesn't render anything, just takes up space.
Use "width" and "height" to set the size of the spacer.
"""

def __init__(self, *args, width=0, height=0, **kwargs):
super().__init__(*args, **kwargs, width=width, height=height)

def render(self, *args, **kwargs):
return None
3 changes: 2 additions & 1 deletion drinks_touch/elements/vbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ def render(self, *args, **kwargs) -> pygame.Surface:
for element in self.children:
element.pos = (self.padding_left, y)
element_surface = element.render(*args, **kwargs)
surface.blit(element_surface, element.pos)
if element_surface:
surface.blit(element_surface, element.pos)
y += element.height + self.gap
if self.focus:
pygame.draw.rect(
Expand Down
6 changes: 4 additions & 2 deletions drinks_touch/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from database.storage import init_db, Session
from drinks.drinks_manager import DrinksManager
from notifications.notification import send_low_balances, send_summaries
from overlays import MouseOverlay
from overlays import MouseOverlay, BaseOverlay
from screens.screen_manager import ScreenManager
from screens.tasks_screen import TasksScreen
from stats.stats import run as stats_send
Expand Down Expand Up @@ -45,7 +45,7 @@
set_library_log_detail_level(EXTENDED)

event_queue = queue.Queue()
overlays = []
overlays: list[BaseOverlay] = []
screen_manager: ScreenManager | None = None


Expand Down Expand Up @@ -124,6 +124,8 @@ def main(argv):
if env.is_pi():
os.system("rsync -a sounds/ pi@pixelfun:sounds/ &")

pygame.key.set_repeat(500, 50)

t = 0
done = False

Expand Down
7 changes: 4 additions & 3 deletions drinks_touch/screens/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from .id_card_screen import IDCardScreen
from .screen import Screen
from .screen_manager import ScreenManager
from .transfer_balance_screen import TransferBalanceScreen


class ProfileScreen(Screen):
Expand Down Expand Up @@ -85,10 +86,10 @@ def on_start(self, *args, **kwargs):
padding=20,
),
Button(
text="Guthaben übertragen",
text="Guthaben übertragen (WIP)",
on_click=functools.partial(
self.alert,
"Nicht implementiert",
self.goto,
TransferBalanceScreen(self.account),
),
padding=20,
),
Expand Down
7 changes: 3 additions & 4 deletions drinks_touch/screens/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,14 @@ def render_alert(self) -> pygame.Surface:
)
return alert

def event(self, event):
def event(self, event) -> BaseElm | None:
if self._alert:
if event.type == pygame.MOUSEBUTTONUP:
self._alert = None
return
for obj in self.objects[::-1]:
if getattr(event, "consumed", False):
break
obj.event(event)
if consumed_by := obj.event(event):
return consumed_by

@staticmethod
def back():
Expand Down
29 changes: 23 additions & 6 deletions drinks_touch/screens/screen_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class ScreenManager:
MENU_BAR_HEIGHT = 65

def __init__(self):
self.ts = 0
self.current_screen = None
self.surface = get_screen_surface()
self.screen_history: list[Screen] = []
Expand All @@ -35,6 +36,7 @@ def __init__(self):
),
self.timeout_widget,
]
self.active_object = None

def set_idle_timeout(self, timeout: int):
"""
Expand Down Expand Up @@ -88,6 +90,9 @@ def nav_bar_visible(self):
return len(self.screen_history) > 1

def render(self, dt):
self.ts += dt
if self.active_object:
self.active_object.ts += dt
self.surface.fill(Color.BACKGROUND.value)
current_screen = self.get_active()
surface, debug_surface = current_screen.render(dt)
Expand Down Expand Up @@ -120,21 +125,33 @@ def events(self, events):
if pygame.mouse.get_pressed()[0]:
self.set_idle_timeout(0)
for event in events:
if event.type == pygame.MOUSEBUTTONUP and event.button == 1:
event_consumed = False
if event.type == pygame.MOUSEBUTTONDOWN:
ScreenManager.get_instance().active_object = None
if (
event.type == pygame.MOUSEBUTTONUP
and event.button == 1
or event.type == pygame.KEYDOWN
):
self.set_idle_timeout(screen.idle_timeout)
if self.nav_bar_visible and hasattr(event, "pos"):
transformed_pos = (
event.pos[0],
event.pos[1] - self.surface.get_height() + self.MENU_BAR_HEIGHT,
)
for obj in self.objects:
if getattr(event, "consumed", False):
if obj.event(event, transformed_pos):
event_consumed = True
continue
obj.event(event, transformed_pos)
if getattr(event, "consumed", False):
if event_consumed:
continue
screen.event(event)
# screen.events(events)
if active_object := screen.event(event):
active_object.ts = 0
ScreenManager.get_instance().active_object = active_object
if event.type == pygame.KEYDOWN and (
active_object := ScreenManager.get_instance().active_object
):
active_object.key_event(event)

@staticmethod
def get_instance() -> "ScreenManager":
Expand Down
Loading

0 comments on commit a03ae74

Please sign in to comment.