tuikit/driver_pygame.py
author Radek Brich <radek.brich@devl.cz>
Sat, 05 Jan 2013 23:00:41 +0100
changeset 52 50a1857557da
parent 46 2b43a7f38c34
permissions -rw-r--r--
Update SDL driver: Enlarge char, attr to 32 bits, 64 bits per terminal cell. Colors and attributes are complete, only blink does not work.

# -*- coding: utf-8 -*-
'''PyGame driver.'''

import pygame
import logging

from tuikit.driver import Driver
from tuikit.common import Coords, Size


class TerminalScreen:

    '''Provide character-level output to screen SDL surface.

    This is performance bottleneck and should be optimized as much as possible.

    '''

    def __init__(self):
        fontselect = 'dejavusansmono,liberationmono,freemono'
        self.font = pygame.font.SysFont(fontselect, 14)
        self.font_bold = pygame.font.SysFont(fontselect, 14, True)
        # get advance of some random char (all should be same in monospace font)
        advance = self.font.metrics('Q')[0][4]
        height = self.font.get_height()
        self.charsize = Size(advance, height)
        self.ascent = self.font.get_ascent()

        # choose self.render() implementation
        if hasattr(self.font, 'render_glyph'):
            self.render_char = self.render_glyph
        else:
            self.render_char = self.render_noglyph

        self.chars = None
        self.attrs = None
        self.default_attr = None
        self.current_attr = None

    def set_default_attr(self, fg, bg, flags):
        self.default_attr = (fg, bg, flags)

    def set_attr(self, fg, bg, flags):
        self.current_attr = (fg, bg, flags)

    def reset_attr(self):
        self.current_attr = self.default_attr

    def reset(self, w, h):
        self.w, self.h = w, h
        numchars = w * h
        self.chars = [' '] * numchars
        self.attrs = [self.default_attr] * numchars

    def clear(self):
        numchars = self.w * self.h
        for pos in range(numchars):
            self.chars[pos] = ' '
            self.attrs[pos] = self.default_attr

    def putch(self, x, y, c):
        pos = y * self.w + x
        self.chars[pos] = c
        self.attrs[pos] = self.current_attr

    def update(self, surface):
        pos = 0
        for y in range(self.h):
            for x in range(self.w):
                fgcolor, bgcolor, flags = self.attrs[pos]
                c = self.chars[pos]
                self.render_char(surface, x, y, c,
                    fgcolor, bgcolor, flags)
                pos += 1

    def render_glyph(self, screen, x, y, c, fgcolor, bgcolor, flags):
        '''Render using render_glyph and metrics.

        This is the correct way, but the output seems same as of render_noglyph
        and this implementation requires patching PyGame to work.

        This implements render() method. See render_noglyph for other implementation.

        '''
        # draw background
        dest = Coords(x * self.charsize.w, y * self.charsize.h)
        if bgcolor != self.default_attr[1]:
            screen.fill(bgcolor, pygame.Rect(dest.x, dest.y, self.charsize.w, self.charsize.h))

        if c == ' ':
            return

        # choose font
        if flags == 1:
            font = self.font_bold
        else:
            font = self.font

        # render character, get metrics
        surface = font.render_glyph(c, True, fgcolor, bgcolor)
        metrics = font.metrics(c)[0]
        minx, maxx, miny, maxy, advance = metrics
        height, ascent = self.charsize.h, self.ascent

        # clips origin and area of rendered character according to metrics
        startx, starty = 0, 0
        if minx < 0:
            startx = abs(minx)
            minx = 0
        if maxy > ascent:
            starty = maxy - ascent
            maxy -= starty
        if ascent - miny > height:
            miny = ascent - height
        if maxx > advance:
            maxx = advance

        # draw character
        dest.x += minx
        dest.y += ascent - maxy
        area = pygame.Rect(startx, starty, maxx - minx, maxy - miny)
        screen.blit(surface, tuple(dest), area)

    def render_noglyph(self, screen, x, y, c, fgcolor, bgcolor, attr):
        '''Render character using normal text rendering.

        This implements render() method. See render_glyph for other implementation.

        '''
        if attr == 'bold':
            font = self.font_bold
        else:
            font = self.font

        # render character, get metrics
        surface = font.render(c, True, fgcolor, bgcolor)
        metrics = font.metrics(c)[0]
        minx = metrics[0]
        startx = 0
        if minx < 0:
            startx = abs(minx)

        # draw background
        dest = Coords(x * self.charsize.w, y * self.charsize.h)
        screen.fill(bgcolor, pygame.Rect(dest.x, dest.y, self.charsize.w, self.charsize.h))

        # draw character
        area = pygame.Rect(startx, 0, self.charsize.w, self.charsize.h)
        screen.blit(surface, tuple(dest), area)


class DriverPygame(Driver):

    '''PyGame driver class.'''

    keymap = {
        pygame.K_ESCAPE     : 'escape',
        pygame.K_TAB        : 'tab',
        pygame.K_RETURN     : 'enter',
        pygame.K_BACKSPACE  : 'backspace',
        pygame.K_F1         : 'f1',
        pygame.K_F2         : 'f2',
        pygame.K_F3         : 'f3',
        pygame.K_F4         : 'f4',
        pygame.K_F5         : 'f5',
        pygame.K_F6         : 'f6',
        pygame.K_F7         : 'f7',
        pygame.K_F8         : 'f8',
        pygame.K_F9         : 'f9',
        pygame.K_F10        : 'f10',
        pygame.K_F11        : 'f11',
        pygame.K_F12        : 'f12',
        pygame.K_INSERT     : 'insert',
        pygame.K_DELETE     : 'delete',
        pygame.K_HOME       : 'home',
        pygame.K_END        : 'end',
        pygame.K_PAGEUP     : 'pageup',
        pygame.K_PAGEDOWN   : 'pagedown',
        pygame.K_UP         : 'up',
        pygame.K_DOWN       : 'down',
        pygame.K_LEFT       : 'left',
        pygame.K_RIGHT      : 'right',
        pygame.K_PRINT      : 'print',
        pygame.K_SCROLLOCK  : 'scrollock',
        pygame.K_PAUSE      : 'pause',
        }

    colormap = {
        'black'         : (0,0,0),
        'blue'          : (23,23,178),
        'green'         : (23,178,23),
        'cyan'          : (23,178,178),
        'red'           : (178,23,23),
        'magenta'       : (178,23,178),
        'yellow'        : (178,103,23),
        'white'         : (178,178,178),
        'intenseblack'  : (104,104,104),
        'intenseblue'   : (84,84,255),
        'intensegreen'  : (84,255,84),
        'intensecyan'   : (84,255,255),
        'intensered'    : (255,84,84),
        'intensemagenta': (255,84,255),
        'intenseyellow' : (255,255,84),
        'intensewhite'  : (255,255,255),
        }

    def __init__(self):
        '''Initialize instance attributes'''
        Driver.__init__(self)
        self.log = logging.getLogger('tuikit')
        self.screen = None
        self.size.w, self.size.h = 120, 40  # screen size in characters
        self.term = None
        self.charsize = Size(16, 8)  # character size in pixels
        self.last_keypress = None
        self.last_key = None
        self.colors = {}     # maps names to curses attributes
        self.colorstack = [] # pushcolor/popcolor puts or gets attributes from this

    def start(self, mainfunc):
        pygame.init()
        self.term = TerminalScreen()
        self.charsize = self.term.charsize
        self.resize_screen()
        self.term.set_default_attr(self.colormap['white'], self.colormap['black'], 0)
        self.term.reset_attr()
        mainfunc()

    def resize_screen(self):
        mode = self.size.w * self.charsize.w, self.size.h * self.charsize.h
        self.screen = pygame.display.set_mode(mode, pygame.RESIZABLE)
        self.term.reset(self.size.w, self.size.h)

    ## input ##

    def getevents(self, timeout=None):
        '''Process input, return list of events.'''
        events = []
        for ev in pygame.event.get():
            # mouse
            if ev.type == pygame.MOUSEMOTION:
                mx = ev.pos[0] // self.charsize.w
                my = ev.pos[1] // self.charsize.h
                events.append(('mousemove', mx, my))
            elif ev.type in (pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP):
                mx = ev.pos[0] // self.charsize.w
                my = ev.pos[1] // self.charsize.h
                evname = {pygame.MOUSEBUTTONDOWN: 'mousedown', pygame.MOUSEBUTTONUP: 'mouseup'}
                events.append((evname[ev.type], mx, my, ev.button))

            # keyboard
            elif ev.type == pygame.KEYDOWN:
                keypress = self.keypress_from_pygame_event(ev)
                if keypress:
                    events.append(keypress)
                    self.last_keypress = keypress
                    self.last_key = ev.key
                    pygame.time.set_timer(pygame.USEREVENT, 200)
            elif ev.type == pygame.USEREVENT: # repeat last key press
                events.append(self.last_keypress)
                pygame.time.set_timer(pygame.USEREVENT, 50)
            elif ev.type == pygame.KEYUP:
                if ev.key == self.last_key:
                    pygame.time.set_timer(pygame.USEREVENT, 0)

            # window
            elif ev.type == pygame.VIDEORESIZE:
                neww, newh = ev.w // self.charsize.w, ev.h // self.charsize.h
                if neww != self.size.w or newh != self.size.h:
                    self.size.w, self.size.h = neww, newh
                    self.resize_screen()
                    events.append(('resize',))
            elif ev.type == pygame.QUIT:
                events.append(('quit',))

            else:
                self.log.warning('Unknown PyGame event: %r', ev.type)
        return events

    def keypress_from_pygame_event(self, ev):
        if ev.key in self.keymap:
            keypress = ('keypress', self.keymap[ev.key], None)
        elif ev.unicode:
            keypress = ('keypress', None, ev.unicode)
        else:
            self.log.debug('Unknown key: key=%r unicode=%r' % (ev.key, ev.unicode))
            keypress = None
        return keypress

    ## drawing ##

    def erase(self):
        '''Clear screen.'''
        self.term.clear()
        self.screen.fill(self.term.default_attr[1])

    def putch(self, x, y, c):
        if not self.clipstack.test(x, y):
            return
        self.term.putch(x, y, c)

    def commit(self):
        '''Commit changes to the screen.'''
        self.term.update(self.screen)
        pygame.display.flip()


    ## colors ##

    def _parsecolor(self, name, attr=None):
        name = name.lower().strip()
        if attr == 'bold':
            name = 'intense' + name
        return self.colormap[name]

    def _parseattrs(self, attrs):
        res = ''
        for a in attrs:
            a = a.lower().strip()
            if a == 'bold':
                res = 1
        return res

    def setcolor(self, name, desc):
        '''Define color name.

        name - name of color (e.g. 'normal', 'active')
        desc - color description - foreground, background, attributes (e.g. 'black on white, bold')

        '''
        parts = desc.split(',')
        fg, bg = parts[0].split(' on ')
        attrs = parts[1:]
        attr = self._parseattrs(attrs)
        fg = self._parsecolor(fg, attr)
        bg = self._parsecolor(bg)
        self.colors[name] = (fg, bg, attr)

    def pushcolor(self, name):
        '''Add color on top of stack and use this color for following output.'''
        # add prefix if such color is available
        if len(self.colorprefix):
            prefixname = self.colorprefix[-1] + name
            if prefixname in self.colors:
                name = prefixname
        col = self.colors[name]
        self.current_color = col
        self.colorstack.append(col)

    def popcolor(self):
        '''Remove color from top of stack and use new top color for following output.'''
        self.colorstack.pop()
        if len(self.colorstack):
            col = self.colorstack[-1]
            self.term.set_attr(col[0], col[1], col[2])
        else:
            self.term.reset_attr()


    ## cursor ##

    def showcursor(self, x, y):
        '''Set cursor to be shown at x, y coordinates.'''

    def hidecursor(self):
        '''Hide cursor.'''


driverclass = DriverPygame