Pygame Entrybox

Entrybox

import pygame
import string
from types import SimpleNamespace

class Recall:
    def __init__(self):
        self.pos = 0
        self.buffer = []
        self.max_count = 4

    def down(self):
        if self.pos > 0:
            self.pos -= 1
            text = self.buffer[self.pos]
            return text
        else:
            self.pos = -1

    def up(self):
        if self.pos < len(self.buffer) - 1:
            self.pos += 1
            return self.buffer[self.pos]

    def store(self, text):
        self.pos = -1
        if text not in self.buffer:
            if len(self.buffer) > self.max_count:
                self.buffer = [text] + self.buffer[:self.max_count]
            else:
                self.buffer.insert(0, text)
        elif self.buffer[0] != text:
            self.buffer.remove(text)
            self.buffer.insert(0, text)

class Carrot:
    def __init__(self, image, position, show=True):
        self.pos = 0
        self.image = image
        self.rect = image.get_rect(topleft=position)
        self.rect.y -= self.rect.h / 2
        self.sleep = 500
        self.awake = 1000
        self.show(show)

    def draw(self, surface):
        if self._show:
            surface.blit(self.image, self.rect)

    def show(self, show):
        self._show = show
        if show:
            self.tick = self.awake + pygame.time.get_ticks()
        else:
            self.tick = self.sleep + pygame.time.get_ticks()

    def on_tick(self, ticks):
        if ticks > self.tick:
            self.show(not self._show)


class EntryBox:
    def __init__(self, font, rect, callback, user_data=None,
            allowed_keys=string.digits + string.ascii_letters + \
                         string.punctuation + ' ',
            text_color = pygame.Color('white'),
            box_color = pygame.Color('cadetblue'),
            hover_color = pygame.Color('lightskyblue'),
            selected_color = pygame.Color('dodgerblue'),
            border_color = pygame.Color('navy')):

        self.user_data = user_data
        self.allowed_keys = allowed_keys
        self.offset = SimpleNamespace(x=0, y=0)
        self.rect = pygame.Rect(rect)
        self.callback = callback
        self.recall = Recall()
        self.selected = False
        self.hover = False
        self.buffer = []
        self.font = font

        self.text_color = text_color
        self.box_color = box_color
        self.hover_color = hover_color
        self.selected_color = selected_color
        self.border_color = border_color

        position = self.rect.left, self.rect.centery
        self.carrot = Carrot(font.render("|", 1, text_color), position)

        self.build_images()

        self.keydown_events = {
            pygame.K_BACKSPACE: self.keydown_backspace,
            pygame.K_RETURN: self.keydown_return,
            pygame.K_DELETE: self.keydown_delete,
            pygame.K_RIGHT: self.keydown_right,
            pygame.K_LEFT: self.keydown_left,
            pygame.K_DOWN: self.keydown_down,
            pygame.K_HOME: self.keydown_home,
            pygame.K_END: self.keydown_end,
            pygame.K_UP: self.keydown_up,
        }

    def build_images(self):
        self.normal_image = self.create_image(self.box_color)
        self.hover_image = self.create_image(self.hover_color)
        self.selected_image = self.create_image(self.selected_color)

    def create_image(self, color):
        surface = pygame.Surface(self.rect.size)
        surface.fill(color)
        pygame.draw.rect(surface, self.border_color, self.rect, 1)
        return surface

    def draw(self, surface):
        if self.selected:
            surface.blit(self.selected_image, self.rect)
            self.carrot.draw(surface)
        elif self.hover:
            surface.blit(self.hover_image, self.rect)
        else:
            surface.blit(self.normal_image, self.rect)

    def keydown_backspace(self):
        if self.carrot.pos > 1:
            front = self.buffer[:self.carrot.pos - 1]
            back = self.buffer[self.carrot.pos:]
            self.buffer = front + back
            self.carrot.pos -= 1
        else:
            self.keydown_delete()

    def keydown_delete(self):
        self.buffer = []
        self.text_image = None
        self.carrot.pos = 0
        self.carrot.rect.x = self.rect.x
        self.offset = SimpleNamespace(x=0, y=0)

    def keydown_down(self):
        self.buffer = self.recall.down()
        if self.buffer:
            self.carrot.pos = len(self.buffer)
        else:
            self.keydown_delete()

    def keydown_end(self):
        self.carrot.pos = len(self.buffer)

    def keydown_home(self):
        self.carrot.pos = 0

    def keydown_left(self):
        if self.carrot.pos > 0:
            self.carrot.pos -= 1

    def keydown_return(self):
        self.recall.store(self.buffer)
        self.callback(self)
        self.keydown_delete()

    def keydown_right(self):
        if self.carrot.pos < len(self.buffer):
            self.carrot.pos += 1

    def keydown_up(self):
        recall_buffer = self.recall.up()
        if recall_buffer:
            self.buffer = recall_buffer
            self.carrot.pos = len(self.buffer)

    def on_click(self, event):
        self.selected = False
        if event.button == 1:
            if self.rect.collidepoint(event.pos):
                self.selected = True

    def on_keydown(self, event):
        if self.selected:
            self.carrot.show(True)
            ctrl = event.mod & pygame.KMOD_CTRL

            if ctrl == 0 and event.unicode in self.allowed_keys and \
                    event.unicode != '':
                self.buffer.insert(self.carrot.pos, event.unicode)
                self.carrot.pos += 1
                self.render_text()
            elif ctrl == 0:
                if self.keydown_events.get(event.key, False):
                    self.keydown_events[event.key]()
                    self.render_text()

    def on_mousemotion(self, event):
        self.hover = self.rect.collidepoint(event.pos)

    def on_tick(self, ticks):
        self.carrot.on_tick(ticks)

    @property
    def text(self):
        return ''.join(self.buffer)

    def render_text(self):
        self.build_images()
        if len(self.buffer) > 0:
            text = self.text
            width = self.rect.width - 4

            if self.carrot.pos > self.offset.y:
                self.offset.y = self.carrot.pos
            elif self.carrot.pos < self.offset.x:
                self.offset.x = self.carrot.pos

            while self.font.size(text[self.offset.x:self.offset.y])[0] < width \
                    and self.offset.x > 0:
                self.offset.x -= 1

            while self.font.size(text[self.offset.x:self.offset.y])[0] > width \
                    and self.offset.x < self.carrot.pos:
                self.offset.x += 1

            while self.font.size(text[self.offset.x:self.offset.y])[0] < width \
                    and self.offset.y < len(self.buffer):
                self.offset.y += 1

            while self.font.size(text[self.offset.x:self.offset.y])[0] > width:
                self.offset.y -= 1

            t = text[self.offset.x:self.offset.y]
            label = self.font.render(t, 1, self.text_color)
            rect = label.get_rect()
            rect.centery = self.rect.centery - self.rect.y
            rect.x = 2
            t = text[int(self.offset.x):self.carrot.pos]
            self.carrot.rect.x = self.font.size(t)[0] + self.rect.x - 1
            #self.carrot.rect.centery = rect.centery

            self.normal_image.blit(label, rect)
            self.hover_image.blit(label, rect)
            self.selected_image.blit(label, rect)

        else:
            self.keydown_delete()

Example

import pygame
import string
import itertools
from pygame.sprite import Group
from statemachine import State
from sprite_text import Text
from entrybox import EntryBox

class Example(State):
    def __init__(self):
        self.sprites = Group()
        font = pygame.font.Font(None, 36)
        position = State.machine.rect.centerx, 40
        color = pygame.Color('lawngreen')
        title = Text('EntryBox Example', font, color, position, True)
        title.add(self.sprites)

        self.font = pygame.font.Font(None, 24)
        y = itertools.count(152, 60)
        self.texts = [
            Text("Box hasn't send any message", self.font, color, (400, next(y))),
            Text("Box hasn't send any message", self.font, color, (400, next(y))),
            Text("Box hasn't send any message", self.font, color, (400, next(y)))
        ]

        y = itertools.count(132, 60)
        self.texts.extend([
            Text("Enter Name", self.font, color, (50, next(y))),
            Text("Enter Age", self.font, color, (50, next(y))),
            Text("Enter Address", self.font, color, (50, next(y))),
        ])

        for text in self.texts:
            text.add(self.sprites)

        y = itertools.count(150, 60)
        self.boxes = [
            # Restrict entrybox to only letters
            EntryBox(self.font, (50, next(y), 200, 30), self.enter_push, 0,
                     string.ascii_letters + ' '),
            # Restrict entrybox to only numbers
            EntryBox(self.font, (50, next(y), 200, 30), self.enter_push, 1,
                     string.digits),
            EntryBox(self.font, (50, next(y), 200, 30), self.enter_push, 2),
        ]

    def enter_push(self, box):
        self.texts[box.user_data].render_text(box.text)

    def on_draw(self, surface):
        surface.fill(pygame.Color('black'))
        self.sprites.draw(surface)
        for box in self.boxes:
            box.draw(surface)

    def on_event(self, event):
        if event.type == pygame.MOUSEMOTION:
            for box in self.boxes:
                box.on_mousemotion(event)
        elif event.type == pygame.MOUSEBUTTONDOWN:
            for box in self.boxes:
                box.on_click(event)
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                State.machine.running = False
            else:
                for box in self.boxes:
                    box.on_keydown(event)
        elif event.type == pygame.QUIT:
            State.machine.running = False

    def on_update(self, delta):
        ticks = pygame.time.get_ticks()
        for box in self.boxes:
            box.on_tick(ticks)

if __name__ == '__main__':
    pygame.init()
    State.machine_setup('EntryBox Example', 800, 600, True)
    State.machine.flip(Example())
    State.machine.mainloop()