view skaapsteker/widgets/text.py @ 632:0675f390653c

Initial port to Python 3 and Pygame 2.
author Simon Cross <hodgestar@gmail.com>
date Fri, 20 Jan 2023 20:01:06 +0100
parents 7d49f698eff7
children
line wrap: on
line source

# -*- coding: utf-8 -*-

import pygame
from pygame.locals import KEYDOWN, K_UP, K_DOWN, K_RETURN, K_SPACE

from ..widgets import Widget
from ..data import filepath

fonts = {
    'sans': 'DejaVuSans.ttf',
}

loaded_fonts = {}

def load_font(name, size):
    if (name, size) not in loaded_fonts:
        fontfn = filepath('fonts/' + fonts[name])
        fonts[(name, size)] = pygame.font.Font(fontfn, size)
    return fonts[(name, size)]

# Probably belongs in a utils module
def unindent_text(text):
    """Reformat docstring-style text blocks."""
    text = text.split('\n')
    while text[0].strip() == '':
        text.pop(0)
    while text[-1].strip() == '':
        text.pop(-1)
    indent = len(text[0]) - len(text[0].lstrip())
    indent_chars = text[0][:indent]
    for index, line in enumerate(text):
        if line.startswith(indent_chars):
            text[index] = line[indent:]
    return '\n'.join(text)


class Text(Widget):
    def __init__(self, text, pos, font='sans', size=16, color='black',
                 shadow=None, wrap=False):
        self.text = text
        if isinstance(pos, pygame.Rect):
            self.rect = pos
        else:
            self.rect = pygame.Rect(pos, (0, 0))
        self.font = load_font(font, size)
        self.color = pygame.Color(color)
        self.shadow = None
        if shadow:
            self.shadow = pygame.Color(shadow)
        if wrap:
            if not isinstance(pos, pygame.Rect):
                raise Exception("Cannot wrap without dimensions")
            self._wrap()

        self._lines = []
        offset = (0, 0)
        for line in self.text.split('\n'):
            if shadow is not None:
                self._lines.append(((offset[0] + 1, offset[1] + 1),
                                   self.font.render(line, True, self.shadow)))
            self._lines.append((offset,
                                self.font.render(line, True, self.color)))
            offset = (offset[0], offset[1] + self.font.get_linesize())
        self.rect.width = max(line[1].get_width() for line in self._lines)
        self.rect.height = self.font.get_linesize() * len(self._lines)
        if shadow is not None:
            self.rect.width += 1
            self.rect.height += 1

    def _wrap(self):
        unwrapped = [para.replace('\n', ' ')
                     for para in self.text.split('\n\n')]
        text = []
        for paragraph in unwrapped:
            words = paragraph.split(' ')
            nwords = len(words)
            from_ = 0
            to = 0
            while to < nwords:
                to += 1
                line = ' '.join(words[from_:to])
                if self.font.size(line)[0] <= self.rect.width:
                    continue
                if to - from_ > 1:
                    to -= 1
                text.append(' '.join(words[from_:to]))
                from_ = to
            text.append(' '.join(words[from_:to]))
            text.append('')
        if text:
            text.pop(-1)
        self.text = '\n'.join(text)

    def draw(self, surface):
        pos = self.rect
        for offset, line in self._lines:
            surface.blit(line, pos.move(offset))


class TextChoice(Widget):
    """Render a list of options, and a selector.
    options can be text or a tuple of (text, data).
    When selected, calls everything registered in callbacks with (index, data)
    """
    def __init__(self, options, pos, **kwargs):
        self.options = []
        self.option_widgets = []
        if isinstance(pos, pygame.Rect):
            self.rect = pos
        else:
            self.rect = pygame.Rect(pos, (0, 0))
            pos = self.rect
        self.selector = Text(u'ยป ', pos.move(0, 0), **kwargs)
        self.selected = 0
        self.callbacks = []

        for option in options:
            if not isinstance(option, tuple):
                option = (option, None)
            self.options.append(option)
            text, data = option
            t = Text(text, pos.move(0, 0), **kwargs)
            self.option_widgets.append(t)

        self.rect.width = max(line.rect.width for line in self.option_widgets
                             ) + self.selector.rect.width
        self.rect.height = sum(line.rect.height for line in self.option_widgets)

    def _update_rects(self):
        pos = self.rect.move(self.selector.rect.width, 0)
        for t in self.option_widgets:
            t.rect.topleft = pos.topleft
            pos = pos.move(0, t.rect.height)
        self.selector.rect.top = self.option_widgets[self.selected].rect.top
        self.selector.rect.left = self.rect.left

    def dispatch(self, ev):
        if ev.type == KEYDOWN:
            if ev.key == K_UP:
                self.selected -= 1
            elif ev.key == K_DOWN:
                self.selected += 1
            elif ev.key in [K_RETURN, K_SPACE]:
                for callback in self.callbacks:
                    callback(self.selected, self.options[self.selected][1])

            self.selected %= len(self.option_widgets)

    def draw(self, surface):
        self._update_rects()
        for option in self.option_widgets:
            option.draw(surface)
        self.selector.draw(surface)


class TextButton(Widget):
    def __init__(self, text, pos, margin=10, **kwargs):
        textpos = pygame.Rect((pos[0] + margin, pos[1] + margin), (0, 0))
        self.text = Text(text, textpos, **kwargs)
        self.rect = pygame.Rect(pos, (0, 0))
        self.rect.width = self.text.rect.width + 2 * margin
        self.rect.height = self.text.rect.height + 2 * margin

    def draw(self, surface, selected=False):
        self.text.draw(surface)
        pygame.draw.rect(surface, self.text.color, self.rect, 1)


class ButtonSet(Widget):
    def __init__(self):
        self.options = []
        self.selected = 0
        self.callbacks = []

    def append(self, widget, data=None):
        self.options.append((widget, data))

    def dispatch(self, ev):
        if ev.type == KEYDOWN:
            if ev.key == K_UP:
                self.selected -= 1
            elif ev.key == K_DOWN:
                self.selected += 1
            elif ev.key in [K_RETURN, K_SPACE]:
                for callback in self.callbacks:
                    callback(self.selected, self.options[self.selected][1])

            self.selected %= len(self.options)

    def draw(self, surface):
        for i, widget in enumerate(self.options):
            widget[0].draw(surface, i == self.selected)