Rename BackendCurses to DriverCurses. Add DriverDummy - dummy driver for debugging purposes. Move clipping stack from driver to common.ClipStack class.
authorRadek Brich <radek.brich@devl.cz>
Sun, 09 Oct 2011 13:06:58 +0200
changeset 23 4e72fd2a0e14
parent 22 6ca8b2d221c3
child 24 b248ef500557
Rename BackendCurses to DriverCurses. Add DriverDummy - dummy driver for debugging purposes. Move clipping stack from driver to common.ClipStack class.
tuikit/application.py
tuikit/backend_curses.py
tuikit/common.py
tuikit/container.py
tuikit/driver_curses.py
tuikit/driver_dummy.py
--- a/tuikit/application.py	Sun Oct 09 11:17:42 2011 +0200
+++ b/tuikit/application.py	Sun Oct 09 13:06:58 2011 +0200
@@ -1,12 +1,12 @@
 # -*- coding: utf-8 -*-
 
-import curses.wrapper
 import logging
 import time
 import math
 
 from tuikit.container import Container
-from tuikit.backend_curses import BackendCurses
+from tuikit.driver_curses import DriverCurses
+from tuikit.driver_dummy import DriverDummy
 
 
 class TopWindow(Container):
@@ -79,12 +79,14 @@
     def __init__(self):
         '''Create application.'''
         
+        self.top = TopWindow()
         '''Top window.'''
-        self.top = TopWindow()
+        
         self.quit = False
         
-        '''Renderer class, i.e. BackendCurses.'''
-        self.screen = None
+        #self.driver = DriverDummy()
+        self.driver = DriverCurses()
+        '''Driver class (render + input), i.e. DriverCurses.'''
 
         self.log = logging.getLogger('tuikit')
         self.log.setLevel(logging.DEBUG)
@@ -94,40 +96,34 @@
         self.log.addHandler(handler)
         self.log.info('=== start ===')
 
-
     def start(self):
         '''Start application. Runs main loop.'''
-        curses.wrapper(self.mainloop)
-
+        self.driver.start(self.mainloop)
 
     def terminate(self):
         '''Terminate application.'''
         self.quit = True
 
-
-    def mainloop(self, screen):
+    def mainloop(self):
         '''The main loop.'''
-        self.screen = BackendCurses(screen)
         self.applytheme()
-        self.top.width, self.top.height = self.screen.width, self.screen.height
+        self.top.size = self.driver.size  # link top widget size to screen size 
         self.top.emit('resize')
 
         while True:
-            self.top.draw(self.screen)
-            self.screen.commit()
+            self.top.draw(self.driver)
+            self.driver.commit()
 
             timeout = None
             if self.top.has_timeout():
                 timeout = int(math.ceil(self.top.nearest_timeout() * 10))
 
-            events = self.screen.process_input(timeout)
+            events = self.driver.process_input(timeout)
 
             if self.top.has_timeout():
                 self.top.process_timeout()
 
             for event in events:
-                if event[0] == 'resize':
-                    self.top.width, self.top.height = self.screen.width, self.screen.height
                 self.top.emit(event[0], *event[1:])
 
             if self.quit:
@@ -135,15 +131,15 @@
 
 
     def applytheme(self):
-        screen = self.screen
-        screen.setcolor('normal',                  'white on black')
-        screen.setcolor('strong',                  'white on black, bold')
-        screen.setcolor('active',                  'black on cyan')
-        screen.setcolor('window:normal',           'white on blue')
-        screen.setcolor('window:controls',         'white on blue, bold')
-        screen.setcolor('window:controls-active',  'cyan on blue, bold')
-        screen.setcolor('button',                  'black on white')
-        screen.setcolor('button-active',           'black on cyan')
-        screen.setcolor('menu',                    'black on cyan')
-        screen.setcolor('menu-active',             'white on cyan, bold')
+        driver = self.driver
+        driver.setcolor('normal',                  'white on black')
+        driver.setcolor('strong',                  'white on black, bold')
+        driver.setcolor('active',                  'black on cyan')
+        driver.setcolor('window:normal',           'white on blue')
+        driver.setcolor('window:controls',         'white on blue, bold')
+        driver.setcolor('window:controls-active',  'cyan on blue, bold')
+        driver.setcolor('button',                  'black on white')
+        driver.setcolor('button-active',           'black on cyan')
+        driver.setcolor('menu',                    'black on cyan')
+        driver.setcolor('menu-active',             'white on cyan, bold')
 
--- a/tuikit/backend_curses.py	Sun Oct 09 11:17:42 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,549 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import curses
-import curses.ascii
-import locale
-import logging
-
-from tuikit.common import Rect
-
-
-class MouseEvent:
-    def __init__(self, x=0, y=0):
-        self.x = x   # global coordinates
-        self.y = y
-        self.wx = x  # local widget coordinates
-        self.wy = y
-        self.px = 0  # parent coordinates
-        self.py = 0
-        self.button = 0
-
-
-    def childevent(self, child):
-        ev = MouseEvent(self.x, self.y)
-        # original local coordinates are new parent coordinates
-        ev.px = self.wx
-        ev.py = self.wy
-        # update local coordinates
-        ev.wx = self.wx - child.x
-        ev.wy = self.wy - child.y
-
-        return ev
-
-
-class BackendCurses:
-    xterm_codes = (
-        (0x09,                      'tab'           ),
-        (0x0a,                      'enter'         ),
-        (0x7f,                      'backspace'     ),
-        (0x1b,                      'escape'        ),
-        (0x1b,0x4f,0x50,            'f1'            ),
-        (0x1b,0x4f,0x51,            'f2'            ),
-        (0x1b,0x4f,0x52,            'f3'            ),
-        (0x1b,0x4f,0x53,            'f4'            ),
-        (0x1b,0x5b,0x31,0x35,0x7e,  'f5'            ),
-        (0x1b,0x5b,0x31,0x37,0x7e,  'f6'            ),
-        (0x1b,0x5b,0x31,0x38,0x7e,  'f7'            ),
-        (0x1b,0x5b,0x31,0x39,0x7e,  'f8'            ),
-        (0x1b,0x5b,0x31,0x7e,       'home'          ),  # linux
-        (0x1b,0x5b,0x32,0x30,0x7e,  'f9'            ),
-        (0x1b,0x5b,0x32,0x31,0x7e,  'f10'           ),
-        (0x1b,0x5b,0x32,0x33,0x7e,  'f11'           ),
-        (0x1b,0x5b,0x32,0x34,0x7e,  'f12'           ),
-        (0x1b,0x5b,0x32,0x7e,       'insert'        ),
-        (0x1b,0x5b,0x33,0x7e,       'delete'        ),
-        (0x1b,0x5b,0x34,0x7e,       'end'           ),  # linux
-        (0x1b,0x5b,0x35,0x7e,       'pageup'        ),
-        (0x1b,0x5b,0x36,0x7e,       'pagedown'      ),
-        (0x1b,0x5b,0x41,            'up'            ),
-        (0x1b,0x5b,0x42,            'down'          ),
-        (0x1b,0x5b,0x43,            'right'         ),
-        (0x1b,0x5b,0x44,            'left'          ),
-        (0x1b,0x5b,0x46,            'end'           ),
-        (0x1b,0x5b,0x48,            'home'          ),
-        (0x1b,0x5b,0x4d,            'mouse'         ),
-        (0x1b,0x5b,0x5b,0x41,       'f1'            ),  # linux
-        (0x1b,0x5b,0x5b,0x42,       'f2'            ),  # linux
-        (0x1b,0x5b,0x5b,0x43,       'f3'            ),  # linux
-        (0x1b,0x5b,0x5b,0x44,       'f4'            ),  # linux
-        (0x1b,0x5b,0x5b,0x45,       'f5'            ),  # linux
-    )
-
-    color_names = {
-        'black'   : curses.COLOR_BLACK,
-        'blue'    : curses.COLOR_BLUE,
-        'cyan'    : curses.COLOR_CYAN,
-        'green'   : curses.COLOR_GREEN,
-        'magenta' : curses.COLOR_MAGENTA,
-        'red'     : curses.COLOR_RED,
-        'white'   : curses.COLOR_WHITE,
-        'yellow'  : curses.COLOR_YELLOW,
-    }
-
-    def __init__(self, screen):
-        self.screen = screen
-        self.height, self.width = screen.getmaxyx()
-
-        self.cursor = None
-        self.clipstack = []
-
-        self.colors = {}     # maps names to curses attributes
-        self.colorpairs = {} # maps tuple (fg,bg) to curses color_pair
-        self.colorstack = [] # pushcolor/popcolor puts or gets attributes from this
-        self.colorprefix = [] # stack of color prefixes
-
-        self.inputqueue = []
-        self.mbtnstack = []
-
-        self.log = logging.getLogger('tuikit')
-
-        # initialize curses
-        curses.curs_set(False)
-        curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION)
-        curses.mouseinterval(0)  # do not wait to detect clicks, we use only press/release
-
-        screen.immedok(0)
-        screen.keypad(0)
-
-        # http://en.wikipedia.org/wiki/List_of_Unicode_characters#Geometric_shapes
-        self.UP_ARROW = '▲' #curses.ACS_UARROW
-        self.DOWN_ARROW = '▼' #curses.ACS_DARROW
-
-        # http://en.wikipedia.org/wiki/Box-drawing_characters
-        self.LIGHT_SHADE = '░' #curses.ACS_BOARD
-        self.MEDIUM_SHADE = '▒'
-        self.DARK_SHADE = '▓'
-        self.BLOCK = '█'
-
-        self.COLUMN = '▁▂▃▄▅▆▇█'
-        self.CORNER_ROUND = '╭╮╰╯'
-        self.CORNER = '┌┐└┘'
-        self.LINE = '─━│┃┄┅┆┇┈┉┊┋'
-
-        self.HLINE = '─' # curses.ACS_HLINE
-        self.VLINE = '│' # curses.ACS_VLINE
-        self.ULCORNER = '┌' # curses.ACS_ULCORNER
-        self.URCORNER = '┐' # curses.ACS_URCORNER
-        self.LLCORNER = '└' # curses.ACS_LLCORNER
-        self.LRCORNER = '┘' # curses.ACS_LRCORNER
-        self.LTEE = '├'
-        self.RTEE = '┤'
-
-
-    ## clip operations ##
-
-    def pushclip(self, x, y, w, h):
-        newclip = Rect(x, y, w, h)
-        if len(self.clipstack):
-            oldclip = self.clipstack[-1]
-            newclip = self.intersect(oldclip, newclip)
-        self.clipstack.append(newclip)
-
-
-    def popclip(self):
-        self.clipstack.pop()
-
-
-    def testclip(self, x, y):
-        # no clip rectangle on stack => passed
-        if not len(self.clipstack):
-            return True
-        # test against top clip rect from stack
-        clip = self.clipstack[-1]
-        if x < clip.x or y < clip.y \
-        or x >= clip.x + clip.w or y >= clip.y + clip.h:
-            return False
-        # passed
-        return True
-
-
-    def intersect(self, r1, r2):
-        x1 = max(r1.x, r2.x)
-        y1 = max(r1.y, r2.y)
-        x2 = min(r1.x + r1.w, r2.x + r2.w)
-        y2 = min(r1.y + r1.h, r2.y + r2.h)
-        if x1 >= x2 or y1 >= y2:
-            return Rect()
-        return Rect(x1, y1, x2-x1, y2-y1)
-
-
-    def union(self, r1, r2):
-        x = min(r1.x, r2.x)
-        y = min(r1.y, r2.y)
-        w = max(r1.x + r1.w, r2.x + r2.w) - x
-        h = max(r1.y + r1.h, r2.y + r2.h) - y
-        return Rect(x, y, w, h)
-
-
-    ## colors, attributes ##
-
-    def _parsecolor(self, name):
-        name = name.lower().strip()
-        return self.color_names[name]
-
-
-    def _getcolorpair(self, fg, bg):
-        pair = (fg, bg)
-        if pair in self.colorpairs:
-            return self.colorpairs[pair]
-        num = len(self.colorpairs) + 1
-        curses.init_pair(num, fg, bg)
-        self.colorpairs[pair] = num
-        return num
-
-
-    def _parseattrs(self, attrs):
-        res = 0
-        for a in attrs:
-            a = a.lower().strip()
-            trans = {
-                'blink'     : curses.A_BLINK,
-                'bold'      : curses.A_BOLD,
-                'dim'       : curses.A_DIM,
-                'standout'  : curses.A_STANDOUT,
-                'underline' : curses.A_UNDERLINE,
-            }
-            res = res | trans[a]
-        return res
-
-
-    def setcolor(self, name, desc):
-        parts = desc.split(',')
-        fg, bg = parts[0].split(' on ')
-        attrs = parts[1:]
-        fg = self._parsecolor(fg)
-        bg = self._parsecolor(bg)
-        col = self._getcolorpair(fg, bg)
-        attr = self._parseattrs(attrs)
-        self.colors[name] = curses.color_pair(col) | attr
-
-
-    def pushcolor(self, name):
-        # add prefix if available
-        if len(self.colorprefix):
-            prefixname = self.colorprefix[-1] + name
-            if prefixname in self.colors:
-                name = prefixname
-        attr = self.colors[name]
-        self.screen.attrset(attr)
-        self.colorstack.append(attr)
-
-
-    def popcolor(self):
-        self.colorstack.pop()
-        if len(self.colorstack):
-            attr = self.colorstack[-1]
-        else:
-            attr = 0
-        self.screen.attrset(attr)
-
-
-    def pushcolorprefix(self, name):
-        self.colorprefix.append(name)
-
-
-    def popcolorprefix(self):
-        self.colorprefix.pop()
-
-
-    ## drawing ##
-
-    def putch(self, x, y, c):
-        if not self.testclip(x, y):
-            return
-        try:
-            if isinstance(c, str) and len(c) == 1:
-                self.screen.addstr(y, x, c)
-            else:
-                self.screen.addch(y, x, c)
-        except curses.error:
-            pass
-
-
-    def puts(self, x, y, s):
-        for c in s:
-            self.putch(x, y, c)
-            x += 1
-
-
-    def hline(self, x, y, w, c=' '):
-        if isinstance(c, str):
-            s = c*w
-        else:
-            s = [c]*w
-        self.puts(x, y, s)
-
-
-    def vline(self, x, y, h, c=' '):
-        for i in range(h):
-            self.putch(x, y+i, c)
-
-
-    def frame(self, x, y, w, h):
-        self.putch(x, y, self.ULCORNER)
-        self.putch(x+w-1, y, self.URCORNER)
-        self.putch(x, y+h-1, self.LLCORNER)
-        self.putch(x+w-1, y+h-1, self.LRCORNER)
-        self.hline(x+1, y, w-2, self.HLINE)
-        self.hline(x+1, y+h-1, w-2, self.HLINE)
-        self.vline(x, y+1, h-2, self.VLINE)
-        self.vline(x+w-1, y+1, h-2, self.VLINE)
-
-
-    def fill(self, x, y, w, h, c=' '):
-        for i in range(h):
-            self.hline(x, y + i, w, c)
-
-
-    def erase(self):
-        self.screen.erase()
-
-
-    def commit(self):
-        if self.cursor:
-            self.screen.move(*self.cursor)
-            curses.curs_set(True)
-        else:
-            curses.curs_set(False)
-        self.screen.refresh()
-
-
-    ## cursor ##
-
-    def showcursor(self, x, y):
-        if not self.testclip(x, y):
-            return
-        self.cursor = (y, x)
-
-
-    def hidecursor(self):
-        curses.curs_set(False)
-        self.cursor = None
-
-
-    ## input ##
-
-    def inputqueue_fill(self, timeout=None):
-        if timeout is None:
-            # wait indefinitely
-            c = self.screen.getch()
-            self.inputqueue.insert(0, c)
-
-        elif timeout > 0:
-            # wait
-            curses.halfdelay(timeout)
-            c = self.screen.getch()
-            curses.cbreak()
-            if c == -1:
-                return
-            self.inputqueue.insert(0, c)
-
-        # timeout = 0 -> no wait
-
-        self.screen.nodelay(1)
-
-        while True:
-            c = self.screen.getch()
-            if c == -1:
-                break
-            self.inputqueue.insert(0, c)
-
-        self.screen.nodelay(0)
-
-
-    def inputqueue_top(self, num=0):
-        return self.inputqueue[-1-num]
-
-
-    def inputqueue_get(self):
-        c = None
-        try:
-            c = self.inputqueue.pop()
-        except IndexError:
-            pass
-        return c
-
-
-    def inputqueue_get_wait(self):
-        c = None
-        while c is None:
-            try:
-                c = self.inputqueue.pop()
-            except IndexError:
-                curses.napms(25)
-                self.inputqueue_fill(0)
-        return c
-
-
-    def inputqueue_unget(self, c):
-        self.inputqueue.append(c)
-
-
-    def process_input(self, timeout=None):
-        # empty queue -> fill
-        if len(self.inputqueue) == 0:
-            self.inputqueue_fill(timeout)
-
-        res = []
-        while len(self.inputqueue):
-            c = self.inputqueue_get()
-
-            if c == curses.KEY_MOUSE:
-                res += self.process_mouse()
-            
-            elif c == curses.KEY_RESIZE:
-                self.height, self.width = self.screen.getmaxyx()
-                res.append(('resize',))
-
-            elif curses.ascii.isctrl(c):
-                self.inputqueue_unget(c)
-                res += self.process_control_chars()
-
-            elif c >= 192 and c <= 255:
-                self.inputqueue_unget(c)
-                res += self.process_utf8_chars()
-
-            elif curses.ascii.isprint(c):
-                res += [('keypress', None, str(chr(c)))]
-
-            else:
-                #self.top.keypress(None, unicode(chr(c)))
-                self.inputqueue_unget(c)
-                res += self.process_control_chars()
-
-        return res
-
-
-    def process_mouse(self):
-        try:
-            id, x, y, z, bstate = curses.getmouse()
-        except curses.error:
-            return []
-
-        ev = MouseEvent(x, y)
-
-        out = []
-
-        if bstate & curses.REPORT_MOUSE_POSITION:
-            out += [('mousemove', ev)]
-
-        if bstate & curses.BUTTON1_PRESSED:
-            ev.button = 1
-            out += [('mousedown', ev)]
-
-        if bstate & curses.BUTTON3_PRESSED:
-            ev.button = 3
-            out += [('mousedown', ev)]
-
-        if bstate & curses.BUTTON1_RELEASED:
-            ev.button = 1
-            out += [('mouseup', ev)]
-
-        if bstate & curses.BUTTON3_RELEASED:
-            ev.button = 3
-            out += [('mouseup', ev)]
-
-        return out
-
-
-    def process_utf8_chars(self):
-        #FIXME read exact number of chars as defined by utf-8
-        utf = []
-        while len(utf) <= 6:
-            c = self.inputqueue_get_wait()
-            utf.append(c)
-            try:
-                uni = str(bytes(utf), 'utf-8')
-                return [('keypress', None, uni)]
-            except UnicodeDecodeError:
-                continue
-        raise Exception('Invalid UTF-8 sequence: %r' % utf)
-
-
-    def process_control_chars(self):
-        codes = self.xterm_codes
-        matchingcodes = []
-        match = None
-        consumed = []
-
-        # consume next char, filter out matching codes
-        c = self.inputqueue_get_wait()
-        consumed.append(c)
-
-        while True:
-            self.log.debug('c=%s len=%s', c, len(codes))
-            for code in codes:
-                if c == code[len(consumed)-1]:
-                    if len(code) - 1 == len(consumed):
-                        match = code
-                    else:
-                        matchingcodes += [code]
-
-            self.log.debug('matching=%s', len(matchingcodes))
-
-            # match found, or no matching code found -> stop
-            if len(matchingcodes) == 0:
-                break
-
-            # match found and some sequencies still match -> continue
-            if len(matchingcodes) > 0:
-                if len(self.inputqueue) == 0:
-                    self.inputqueue_fill(1)
-
-            c = self.inputqueue_get()
-            if c:
-                consumed.append(c)
-                codes = matchingcodes
-                matchingcodes = []
-            else:
-                break
-
-        keyname = None
-        if match:
-            # compare match to consumed, return unused chars
-            l = len(match) - 1
-            while len(consumed) > l:
-                self.inputqueue_unget(consumed[-1])
-                del consumed[-1]
-            keyname = match[-1]
-
-        if match is None:
-            self.log.debug('Unknown control sequence: %s',
-                ','.join(['0x%x'%x for x in consumed]))
-            return [('keypress', 'Unknown', None)]
-
-        if keyname == 'mouse':
-            return self.process_xterm_mouse()
-
-        return [('keypress', keyname, None)]
-
-
-    def process_xterm_mouse(self):
-        t = self.inputqueue_get_wait()
-        x = self.inputqueue_get_wait() - 0x21
-        y = self.inputqueue_get_wait() - 0x21
-
-        ev = MouseEvent(x, y)
-        out = []
-
-        if t in (0x20, 0x21, 0x22): # button press
-            btn = t - 0x1f
-            ev.button = btn
-            if not btn in self.mbtnstack:
-                self.mbtnstack.append(btn)
-                out += [('mousedown', ev)]
-            else:
-                out += [('mousemove', ev)]
-
-        elif t == 0x23: # button release
-            ev.button = self.mbtnstack.pop()
-            out += [('mouseup', ev)]
-
-        elif t in (0x60, 0x61): # wheel up, down
-            ev.button = 4 + t - 0x60
-            out += [('mousewheel', ev)]
-
-        else:
-            raise Exception('Unknown mouse event: %x' % t)
-
-        return out
--- a/tuikit/common.py	Sun Oct 09 11:17:42 2011 +0200
+++ b/tuikit/common.py	Sun Oct 09 13:06:58 2011 +0200
@@ -2,6 +2,9 @@
 
 
 class Coords:
+    
+    '''2D coordinates.'''
+    
     def __init__(self, x=0, y=0):
         self.x = x
         self.y = y
@@ -25,6 +28,14 @@
 
 
 class Size:
+    
+    '''Size class.
+    
+    Implements attribute access (.w, .h), list-like access([0],[1])
+    and dict-like access (['w'],['h']).
+    
+    '''
+    
     def __init__(self, w=None, h=None):
         self.w = w
         self.h = h
@@ -38,9 +49,9 @@
         return self.__dict__[key]
 
     def __setitem__(self, key, value):
-        if key == 0:
+        if key in [0, 'w']:
             self.w = value
-        if key == 1:
+        if key in [1, 'h']:
             self.h = value
     
     def __repr__(self):
@@ -48,6 +59,9 @@
 
 
 class Rect:
+    
+    '''Rectangle is defined by 2D coordinates and size.'''
+    
     def __init__(self, x=0, y=0, w=0, h=0):
         self.x = x
         self.y = y
@@ -59,6 +73,15 @@
 
 
 class Borders:
+    
+    '''Borders are defined by left, top, right, bottom border size.
+    
+    Ordering is clock-wise, starting with left. This may seem weird,
+    but it corresponds to X/Y or W/H used elsewhere. Left and right are
+    on X axis, so they are defined first.
+    
+    '''
+    
     def __init__(self, l=0, t=0, r=0, b=0):
         self.l = l # left
         self.t = t # top
@@ -76,3 +99,49 @@
     def __repr__(self):
         return 'Borders(l={0.l},t={0.t},r={0.r},b={0.b})'.format(self)
 
+
+class ClipStack:
+    
+    '''Stack of clipping regions.'''
+    
+    def __init__(self):
+        self.stack = []
+    
+    def push(self, x, y, w, h):
+        newclip = Rect(x, y, w, h)
+        if len(self.stack):
+            oldclip = self.stack[-1]
+            newclip = self.intersect(oldclip, newclip)
+        self.stack.append(newclip)
+
+    def pop(self):
+        self.stack.pop()
+
+    def test(self, x, y):
+        # no clip rectangle on stack => passed
+        if not len(self.stack):
+            return True
+        # test against top clip rect from stack
+        clip = self.stack[-1]
+        if x < clip.x or y < clip.y \
+        or x >= clip.x + clip.w or y >= clip.y + clip.h:
+            return False
+        # passed
+        return True
+
+    def intersect(self, r1, r2):
+        x1 = max(r1.x, r2.x)
+        y1 = max(r1.y, r2.y)
+        x2 = min(r1.x + r1.w, r2.x + r2.w)
+        y2 = min(r1.y + r1.h, r2.y + r2.h)
+        if x1 >= x2 or y1 >= y2:
+            return Rect()
+        return Rect(x1, y1, x2-x1, y2-y1)
+
+    def union(self, r1, r2):
+        x = min(r1.x, r2.x)
+        y = min(r1.y, r2.y)
+        w = max(r1.x + r1.w, r2.x + r2.w) - x
+        h = max(r1.y + r1.h, r2.y + r2.h) - y
+        return Rect(x, y, w, h)
+
--- a/tuikit/container.py	Sun Oct 09 11:17:42 2011 +0200
+++ b/tuikit/container.py	Sun Oct 09 13:06:58 2011 +0200
@@ -81,30 +81,30 @@
             child.emit('resize')
 
 
-    def draw(self, screen, x=0, y=0):
+    def draw(self, driver, x=0, y=0):
         if self.hidden:
             return
 
-        screen.pushclip(x, y, self.width, self.height)
+        driver.clipstack.push(x, y, self.width, self.height)
         if self.colorprefix:
-            screen.pushcolorprefix(self.colorprefix)
+            driver.pushcolorprefix(self.colorprefix)
 
-        Widget.draw(self, screen, x, y)
+        Widget.draw(self, driver, x, y)
 
         for child in [x for x in self.children if not x.allowlayout]:
-            child.draw(screen, x + child.x, y + child.y)
+            child.draw(driver, x + child.x, y + child.y)
 
         l, t, r, b = self.borders
-        screen.pushclip(x+l, y+t, self.width-l-r, self.height-t-b)
+        driver.clipstack.push(x+l, y+t, self.width-l-r, self.height-t-b)
 
         for child in [x for x in self.children if x.allowlayout]:
-            child.draw(screen, x + child.x, y + child.y)
+            child.draw(driver, x + child.x, y + child.y)
 
-        screen.popclip()
+        driver.clipstack.pop()
 
         if self.colorprefix:
-            screen.popcolorprefix()
-        screen.popclip()
+            driver.popcolorprefix()
+        driver.clipstack.pop()
 
 
     def mousedown(self, ev):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/driver_curses.py	Sun Oct 09 13:06:58 2011 +0200
@@ -0,0 +1,511 @@
+# -*- coding: utf-8 -*-
+
+import curses
+import curses.ascii
+import curses.wrapper
+import locale
+import logging
+
+from tuikit.common import Size, Rect, ClipStack
+
+
+class MouseEvent:
+    def __init__(self, x=0, y=0):
+        self.x = x   # global coordinates
+        self.y = y
+        self.wx = x  # local widget coordinates
+        self.wy = y
+        self.px = 0  # parent coordinates
+        self.py = 0
+        self.button = 0
+
+
+    def childevent(self, child):
+        ev = MouseEvent(self.x, self.y)
+        # original local coordinates are new parent coordinates
+        ev.px = self.wx
+        ev.py = self.wy
+        # update local coordinates
+        ev.wx = self.wx - child.x
+        ev.wy = self.wy - child.y
+
+        return ev
+
+
+class DriverCurses:
+    xterm_codes = (
+        (0x09,                      'tab'           ),
+        (0x0a,                      'enter'         ),
+        (0x7f,                      'backspace'     ),
+        (0x1b,                      'escape'        ),
+        (0x1b,0x4f,0x50,            'f1'            ),
+        (0x1b,0x4f,0x51,            'f2'            ),
+        (0x1b,0x4f,0x52,            'f3'            ),
+        (0x1b,0x4f,0x53,            'f4'            ),
+        (0x1b,0x5b,0x31,0x35,0x7e,  'f5'            ),
+        (0x1b,0x5b,0x31,0x37,0x7e,  'f6'            ),
+        (0x1b,0x5b,0x31,0x38,0x7e,  'f7'            ),
+        (0x1b,0x5b,0x31,0x39,0x7e,  'f8'            ),
+        (0x1b,0x5b,0x31,0x7e,       'home'          ),  # linux
+        (0x1b,0x5b,0x32,0x30,0x7e,  'f9'            ),
+        (0x1b,0x5b,0x32,0x31,0x7e,  'f10'           ),
+        (0x1b,0x5b,0x32,0x33,0x7e,  'f11'           ),
+        (0x1b,0x5b,0x32,0x34,0x7e,  'f12'           ),
+        (0x1b,0x5b,0x32,0x7e,       'insert'        ),
+        (0x1b,0x5b,0x33,0x7e,       'delete'        ),
+        (0x1b,0x5b,0x34,0x7e,       'end'           ),  # linux
+        (0x1b,0x5b,0x35,0x7e,       'pageup'        ),
+        (0x1b,0x5b,0x36,0x7e,       'pagedown'      ),
+        (0x1b,0x5b,0x41,            'up'            ),
+        (0x1b,0x5b,0x42,            'down'          ),
+        (0x1b,0x5b,0x43,            'right'         ),
+        (0x1b,0x5b,0x44,            'left'          ),
+        (0x1b,0x5b,0x46,            'end'           ),
+        (0x1b,0x5b,0x48,            'home'          ),
+        (0x1b,0x5b,0x4d,            'mouse'         ),
+        (0x1b,0x5b,0x5b,0x41,       'f1'            ),  # linux
+        (0x1b,0x5b,0x5b,0x42,       'f2'            ),  # linux
+        (0x1b,0x5b,0x5b,0x43,       'f3'            ),  # linux
+        (0x1b,0x5b,0x5b,0x44,       'f4'            ),  # linux
+        (0x1b,0x5b,0x5b,0x45,       'f5'            ),  # linux
+    )
+
+    color_names = {
+        'black'   : curses.COLOR_BLACK,
+        'blue'    : curses.COLOR_BLUE,
+        'cyan'    : curses.COLOR_CYAN,
+        'green'   : curses.COLOR_GREEN,
+        'magenta' : curses.COLOR_MAGENTA,
+        'red'     : curses.COLOR_RED,
+        'white'   : curses.COLOR_WHITE,
+        'yellow'  : curses.COLOR_YELLOW,
+    }
+
+    def __init__(self):
+        '''Set driver attributes to default values.'''
+        self.screen = None
+        self.size = Size()
+        self.cursor = None
+        self.clipstack = ClipStack()
+        self.colors = {}     # maps names to curses attributes
+        self.colorpairs = {} # maps tuple (fg,bg) to curses color_pair
+        self.colorstack = [] # pushcolor/popcolor puts or gets attributes from this
+        self.colorprefix = [] # stack of color prefixes
+        self.inputqueue = []
+        self.mbtnstack = []
+
+        self.log = logging.getLogger('tuikit')
+
+        # http://en.wikipedia.org/wiki/List_of_Unicode_characters#Geometric_shapes
+        self.UP_ARROW = '▲' #curses.ACS_UARROW
+        self.DOWN_ARROW = '▼' #curses.ACS_DARROW
+
+        # http://en.wikipedia.org/wiki/Box-drawing_characters
+        self.LIGHT_SHADE = '░' #curses.ACS_BOARD
+        self.MEDIUM_SHADE = '▒'
+        self.DARK_SHADE = '▓'
+        self.BLOCK = '█'
+
+        self.COLUMN = '▁▂▃▄▅▆▇█'
+        self.CORNER_ROUND = '╭╮╰╯'
+        self.CORNER = '┌┐└┘'
+        self.LINE = '─━│┃┄┅┆┇┈┉┊┋'
+
+        self.HLINE = '─' # curses.ACS_HLINE
+        self.VLINE = '│' # curses.ACS_VLINE
+        self.ULCORNER = '┌' # curses.ACS_ULCORNER
+        self.URCORNER = '┐' # curses.ACS_URCORNER
+        self.LLCORNER = '└' # curses.ACS_LLCORNER
+        self.LRCORNER = '┘' # curses.ACS_LRCORNER
+        self.LTEE = '├'
+        self.RTEE = '┤'
+
+    def init(self):
+        '''Initialize curses'''
+        self.size.h, self.size.w = self.screen.getmaxyx()
+        self.screen.immedok(0)
+        self.screen.keypad(0)
+        curses.curs_set(False)  # hide cursor
+        curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION)
+        curses.mouseinterval(0)  # do not wait to detect clicks, we use only press/release
+
+    def start(self, mainfunc):
+        def main(screen):
+            self.screen = screen
+            self.init()
+            mainfunc()
+        curses.wrapper(main)
+
+
+    ## colors, attributes ##
+
+    def _parsecolor(self, name):
+        name = name.lower().strip()
+        return self.color_names[name]
+
+
+    def _getcolorpair(self, fg, bg):
+        pair = (fg, bg)
+        if pair in self.colorpairs:
+            return self.colorpairs[pair]
+        num = len(self.colorpairs) + 1
+        curses.init_pair(num, fg, bg)
+        self.colorpairs[pair] = num
+        return num
+
+
+    def _parseattrs(self, attrs):
+        res = 0
+        for a in attrs:
+            a = a.lower().strip()
+            trans = {
+                'blink'     : curses.A_BLINK,
+                'bold'      : curses.A_BOLD,
+                'dim'       : curses.A_DIM,
+                'standout'  : curses.A_STANDOUT,
+                'underline' : curses.A_UNDERLINE,
+            }
+            res = res | trans[a]
+        return res
+
+
+    def setcolor(self, name, desc):
+        parts = desc.split(',')
+        fg, bg = parts[0].split(' on ')
+        attrs = parts[1:]
+        fg = self._parsecolor(fg)
+        bg = self._parsecolor(bg)
+        col = self._getcolorpair(fg, bg)
+        attr = self._parseattrs(attrs)
+        self.colors[name] = curses.color_pair(col) | attr
+
+
+    def pushcolor(self, name):
+        # add prefix if available
+        if len(self.colorprefix):
+            prefixname = self.colorprefix[-1] + name
+            if prefixname in self.colors:
+                name = prefixname
+        attr = self.colors[name]
+        self.screen.attrset(attr)
+        self.colorstack.append(attr)
+
+
+    def popcolor(self):
+        self.colorstack.pop()
+        if len(self.colorstack):
+            attr = self.colorstack[-1]
+        else:
+            attr = 0
+        self.screen.attrset(attr)
+
+
+    def pushcolorprefix(self, name):
+        self.colorprefix.append(name)
+
+
+    def popcolorprefix(self):
+        self.colorprefix.pop()
+
+
+    ## drawing ##
+
+    def putch(self, x, y, c):
+        if not self.clipstack.test(x, y):
+            return
+        try:
+            if isinstance(c, str) and len(c) == 1:
+                self.screen.addstr(y, x, c)
+            else:
+                self.screen.addch(y, x, c)
+        except curses.error:
+            pass
+
+
+    def puts(self, x, y, s):
+        for c in s:
+            self.putch(x, y, c)
+            x += 1
+
+
+    def hline(self, x, y, w, c=' '):
+        if isinstance(c, str):
+            s = c*w
+        else:
+            s = [c]*w
+        self.puts(x, y, s)
+
+
+    def vline(self, x, y, h, c=' '):
+        for i in range(h):
+            self.putch(x, y+i, c)
+
+
+    def frame(self, x, y, w, h):
+        self.putch(x, y, self.ULCORNER)
+        self.putch(x+w-1, y, self.URCORNER)
+        self.putch(x, y+h-1, self.LLCORNER)
+        self.putch(x+w-1, y+h-1, self.LRCORNER)
+        self.hline(x+1, y, w-2, self.HLINE)
+        self.hline(x+1, y+h-1, w-2, self.HLINE)
+        self.vline(x, y+1, h-2, self.VLINE)
+        self.vline(x+w-1, y+1, h-2, self.VLINE)
+
+
+    def fill(self, x, y, w, h, c=' '):
+        for i in range(h):
+            self.hline(x, y + i, w, c)
+
+
+    def erase(self):
+        self.screen.erase()
+
+
+    def commit(self):
+        if self.cursor:
+            self.screen.move(*self.cursor)
+            curses.curs_set(True)
+        else:
+            curses.curs_set(False)
+        self.screen.refresh()
+
+
+    ## cursor ##
+
+    def showcursor(self, x, y):
+        if not self.clipstack.test(x, y):
+            return
+        self.cursor = (y, x)
+
+
+    def hidecursor(self):
+        curses.curs_set(False)
+        self.cursor = None
+
+
+    ## input ##
+
+    def inputqueue_fill(self, timeout=None):
+        if timeout is None:
+            # wait indefinitely
+            c = self.screen.getch()
+            self.inputqueue.insert(0, c)
+
+        elif timeout > 0:
+            # wait
+            curses.halfdelay(timeout)
+            c = self.screen.getch()
+            curses.cbreak()
+            if c == -1:
+                return
+            self.inputqueue.insert(0, c)
+
+        # timeout = 0 -> no wait
+
+        self.screen.nodelay(1)
+
+        while True:
+            c = self.screen.getch()
+            if c == -1:
+                break
+            self.inputqueue.insert(0, c)
+
+        self.screen.nodelay(0)
+
+
+    def inputqueue_top(self, num=0):
+        return self.inputqueue[-1-num]
+
+
+    def inputqueue_get(self):
+        c = None
+        try:
+            c = self.inputqueue.pop()
+        except IndexError:
+            pass
+        return c
+
+
+    def inputqueue_get_wait(self):
+        c = None
+        while c is None:
+            try:
+                c = self.inputqueue.pop()
+            except IndexError:
+                curses.napms(25)
+                self.inputqueue_fill(0)
+        return c
+
+
+    def inputqueue_unget(self, c):
+        self.inputqueue.append(c)
+
+
+    def process_input(self, timeout=None):
+        # empty queue -> fill
+        if len(self.inputqueue) == 0:
+            self.inputqueue_fill(timeout)
+
+        res = []
+        while len(self.inputqueue):
+            c = self.inputqueue_get()
+
+            if c == curses.KEY_MOUSE:
+                res += self.process_mouse()
+            
+            elif c == curses.KEY_RESIZE:
+                self.size.h, self.size.w = self.screen.getmaxyx()
+                res.append(('resize',))
+
+            elif curses.ascii.isctrl(c):
+                self.inputqueue_unget(c)
+                res += self.process_control_chars()
+
+            elif c >= 192 and c <= 255:
+                self.inputqueue_unget(c)
+                res += self.process_utf8_chars()
+
+            elif curses.ascii.isprint(c):
+                res += [('keypress', None, str(chr(c)))]
+
+            else:
+                #self.top.keypress(None, unicode(chr(c)))
+                self.inputqueue_unget(c)
+                res += self.process_control_chars()
+
+        return res
+
+
+    def process_mouse(self):
+        try:
+            id, x, y, z, bstate = curses.getmouse()
+        except curses.error:
+            return []
+
+        ev = MouseEvent(x, y)
+
+        out = []
+
+        if bstate & curses.REPORT_MOUSE_POSITION:
+            out += [('mousemove', ev)]
+
+        if bstate & curses.BUTTON1_PRESSED:
+            ev.button = 1
+            out += [('mousedown', ev)]
+
+        if bstate & curses.BUTTON3_PRESSED:
+            ev.button = 3
+            out += [('mousedown', ev)]
+
+        if bstate & curses.BUTTON1_RELEASED:
+            ev.button = 1
+            out += [('mouseup', ev)]
+
+        if bstate & curses.BUTTON3_RELEASED:
+            ev.button = 3
+            out += [('mouseup', ev)]
+
+        return out
+
+
+    def process_utf8_chars(self):
+        #FIXME read exact number of chars as defined by utf-8
+        utf = []
+        while len(utf) <= 6:
+            c = self.inputqueue_get_wait()
+            utf.append(c)
+            try:
+                uni = str(bytes(utf), 'utf-8')
+                return [('keypress', None, uni)]
+            except UnicodeDecodeError:
+                continue
+        raise Exception('Invalid UTF-8 sequence: %r' % utf)
+
+
+    def process_control_chars(self):
+        codes = self.xterm_codes
+        matchingcodes = []
+        match = None
+        consumed = []
+
+        # consume next char, filter out matching codes
+        c = self.inputqueue_get_wait()
+        consumed.append(c)
+
+        while True:
+            self.log.debug('c=%s len=%s', c, len(codes))
+            for code in codes:
+                if c == code[len(consumed)-1]:
+                    if len(code) - 1 == len(consumed):
+                        match = code
+                    else:
+                        matchingcodes += [code]
+
+            self.log.debug('matching=%s', len(matchingcodes))
+
+            # match found, or no matching code found -> stop
+            if len(matchingcodes) == 0:
+                break
+
+            # match found and some sequencies still match -> continue
+            if len(matchingcodes) > 0:
+                if len(self.inputqueue) == 0:
+                    self.inputqueue_fill(1)
+
+            c = self.inputqueue_get()
+            if c:
+                consumed.append(c)
+                codes = matchingcodes
+                matchingcodes = []
+            else:
+                break
+
+        keyname = None
+        if match:
+            # compare match to consumed, return unused chars
+            l = len(match) - 1
+            while len(consumed) > l:
+                self.inputqueue_unget(consumed[-1])
+                del consumed[-1]
+            keyname = match[-1]
+
+        if match is None:
+            self.log.debug('Unknown control sequence: %s',
+                ','.join(['0x%x'%x for x in consumed]))
+            return [('keypress', 'Unknown', None)]
+
+        if keyname == 'mouse':
+            return self.process_xterm_mouse()
+
+        return [('keypress', keyname, None)]
+
+
+    def process_xterm_mouse(self):
+        t = self.inputqueue_get_wait()
+        x = self.inputqueue_get_wait() - 0x21
+        y = self.inputqueue_get_wait() - 0x21
+
+        ev = MouseEvent(x, y)
+        out = []
+
+        if t in (0x20, 0x21, 0x22): # button press
+            btn = t - 0x1f
+            ev.button = btn
+            if not btn in self.mbtnstack:
+                self.mbtnstack.append(btn)
+                out += [('mousedown', ev)]
+            else:
+                out += [('mousemove', ev)]
+
+        elif t == 0x23: # button release
+            ev.button = self.mbtnstack.pop()
+            out += [('mouseup', ev)]
+
+        elif t in (0x60, 0x61): # wheel up, down
+            ev.button = 4 + t - 0x60
+            out += [('mousewheel', ev)]
+
+        else:
+            raise Exception('Unknown mouse event: %x' % t)
+
+        return out
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/driver_dummy.py	Sun Oct 09 13:06:58 2011 +0200
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+'''Dummy driver.
+
+Implements basic driver interface.
+This is useful for debugging or when writing new driver.
+
+@author: Radek Brich <radek.brich@devl.cz>
+
+'''
+
+import logging
+
+from tuikit.common import Size, ClipStack
+
+
+class DriverDummy:
+    
+    '''Dummy driver class'''
+    
+    def __init__(self):
+        '''Initialize instance attributes'''
+        self.log = logging.getLogger('tuikit')
+        self.size = Size()
+        self.clipstack = ClipStack()
+    
+    def start(self, mainfunc):
+        '''Start driver and run mainfunc.'''
+        self.size.w, self.size.h = 80, 25
+        mainfunc()
+
+
+    # colors
+    
+    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')
+        
+        '''
+        self.log.info('DummyDriver.setcolor(name=%r, desc=%r)', name, desc)
+
+    def pushcolor(self, name):
+        self.log.info('DummyDriver.pushcolor(name=%r)', name)
+    
+    def popcolor(self):
+        self.log.info('DummyDriver.popcolor()')
+
+
+    # drawing
+    
+    def erase(self):
+        '''Clear screen.'''
+        self.log.info('DummyDriver.erase()')
+
+    def puts(self, x, y, s):
+        '''Output string to specified coordinates.'''
+        self.log.info('DummyDriver.puts(x=%r, y=%r, s=%r)', x, y, s)
+
+    def commit(self):
+        '''Commit changes to the screen.'''
+        self.log.info('DummyDriver.commit()')
+
+
+    # input
+    
+    def process_input(self, timeout=None):
+        '''Process input, return list of events.
+        
+        This dummy implementation just returns 'q' and Escape key presses.
+        
+        '''
+        events = [('keypress', None, 'q'), ('keypress', 'escape', None)]
+        return events
+