Rework layouts: Layout is now normal Container which places its children upon resize event.
authorRadek Brich <radek.brich@devl.cz>
Fri, 18 Jan 2013 22:36:50 +0100
changeset 62 2f61931520c9
parent 61 15088f62c4ac
child 63 2a0e04091898
Rework layouts: Layout is now normal Container which places its children upon resize event. Drop TopWindow, top is now any subclass of Container. Add floater concept: floaters are widgets drawn over normal widgets, not clipped by parent. Add HScrollbar and Scrollbar abstract base class.
demo_anchorlayout.py
demo_checkbox.py
demo_colors.py
demo_editor.py
demo_input.py
demo_layout.py
demo_treeview.py
demo_window.py
docs/index.rst
docs/widget.rst
sdlterm/src/sdlterm.cc
tuikit/__init__.py
tuikit/application.py
tuikit/button.py
tuikit/combobox.py
tuikit/common.py
tuikit/container.py
tuikit/driver_sdl.py
tuikit/editbox.py
tuikit/editfield.py
tuikit/events.py
tuikit/label.py
tuikit/layout.py
tuikit/menu.py
tuikit/scrollbar.py
tuikit/scrollview.py
tuikit/textedit.py
tuikit/treeview.py
tuikit/widget.py
tuikit/window.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/demo_anchorlayout.py	Fri Jan 18 22:36:50 2013 +0100
@@ -0,0 +1,65 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import locale
+import os
+
+from tuikit.application import Application
+from tuikit.window import Window
+from tuikit.label import Label
+from tuikit.button import Button
+from tuikit.layout import AnchorLayout
+from tuikit.common import Borders
+
+
+class MyApplication(Application):
+    def __init__(self):
+        Application.__init__(self)
+        self.top = AnchorLayout()
+        self.top.name = 'top'
+        self.top.add_handler('keypress', self.on_top_keypress)
+
+        win = Window()
+        win.name = 'window'
+        win.title = 'AnchorLayout demo'
+        win.resize(80, 25)
+        self.top.add(win, halign='left', valign='top')
+        self.win = win
+
+        button_valign = Button('valign: ' + self.win.hints['valign'].selected)
+        button_valign.name = 'button_valign'
+        button_valign.add_handler('click', self.on_button_align_click)
+        win.add(button_valign, halign='center', margin=Borders(t=2))
+
+        button_halign = Button('halign: ' + self.win.hints['halign'].selected)
+        button_halign.name = 'button_halign'
+        button_halign.add_handler('click', self.on_button_align_click)
+        win.add(button_halign, halign='center', margin=Borders(t=4))
+
+        label_margin = Label(str(self.win.hints['margin']))
+        label_margin.name = 'label_margin'
+        label_margin.add_handler('draw', self.on_label_margin_draw)
+        win.add(label_margin, halign='center', margin=Borders(t=6))
+
+    def on_button_align_click(self, ev):
+        align_type = ev.originator.label.split(':', 1)[0]
+        align = self.win.hints[align_type]
+        align.select_next()
+        ev.originator.label = '%s: %s' % (align_type, align.selected)
+        self.top.emit('resize')
+        return True
+
+    def on_label_margin_draw(self, ev):
+        ev.originator.label = str(self.win.hints['margin'])
+
+    def on_top_keypress(self, ev):
+        if ev.keyname == 'escape':
+            self.terminate()
+            return True
+
+
+if __name__ == '__main__':
+    locale.setlocale(locale.LC_ALL, '')
+    os.environ['ESCDELAY'] = '25' # do not wait 1 second after pressing Escape key
+    app = MyApplication()
+    app.start()
--- a/demo_checkbox.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/demo_checkbox.py	Fri Jan 18 22:36:50 2013 +0100
@@ -4,16 +4,15 @@
 import locale
 locale.setlocale(locale.LC_ALL, '')
 
-from tuikit import *
+from tuikit import Application, VerticalLayout, ComboBox, Checkbox
 
 
 class MyApplication(Application):
     def __init__(self):
         Application.__init__(self)
+        self.top = VerticalLayout(homogeneous=False)
         self.top.add_handler('keypress', self.on_top_keypress)
 
-        self.top.layout = VerticalLayout(homogeneous=False)
-
         combo = ComboBox(items=['abc', 'xyz'])
         self.top.add(combo)
 
--- a/demo_colors.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/demo_colors.py	Fri Jan 18 22:36:50 2013 +0100
@@ -4,27 +4,27 @@
 import locale
 locale.setlocale(locale.LC_ALL, '')
 
-from tuikit import Application, Window, Label, VerticalLayout
+from tuikit import Application, Window, Label, AnchorLayout, VerticalLayout
 
 
 class MyApplication(Application):
     def __init__(self):
-        Application.__init__(self)
+        Application.__init__(self, top_layout=AnchorLayout)
         self.top.add_handler('keypress', self.on_top_keypress)
 
-        win = Window()
-        self.top.add(win)
-        win.layout = VerticalLayout()
+        win = Window(inner_layout=VerticalLayout)
+        win.resize(30,10)
+        win.title = 'styles'
 
         for attr in ['blink', 'bold', 'standout', 'underline']:
             label = Label(attr)
             label.color = 'test-' + attr
-            self.top.add(label)
+            win.add(label)
 
-        self.top.layout = VerticalLayout()
+        self.top.add(win)
 
-    def applytheme(self):
-        Application.applytheme(self)
+    def apply_theme(self):
+        Application.apply_theme(self)
         self.driver.setcolor('test-blink',              'cyan on blue, blink')
         self.driver.setcolor('test-bold',               'cyan on blue, bold')
         self.driver.setcolor('test-standout',           'cyan on blue, standout')
--- a/demo_editor.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/demo_editor.py	Fri Jan 18 22:36:50 2013 +0100
@@ -7,7 +7,6 @@
 import os
 
 from tuikit.application import Application
-from tuikit.editfield import EditField
 from tuikit.textedit import TextEdit
 
 
@@ -16,14 +15,9 @@
         Application.__init__(self)
         self.top.add_handler('keypress', self.on_top_keypress)
 
-        #edit = EditField(50, 'DlouhyTest12')
-        #self.top.add(edit)
-
         t = open('tuikit/widget.py').read()
-        textedit = TextEdit(100, 40, t)
-        self.top.add(textedit)
-        textedit.x = 2
-        self.textedit = textedit
+        textedit = TextEdit(t)
+        self.top.add(textedit, halign='fill', valign='fill')
 
     def on_top_keypress(self, ev):
         if ev.keyname == 'escape':
--- a/demo_input.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/demo_input.py	Fri Jan 18 22:36:50 2013 +0100
@@ -12,9 +12,8 @@
         Application.__init__(self)
 
         self.text = ''
-        self.textedit = TextEdit(100, 40, self.text)
-        self.top.add(self.textedit)
-        self.textedit.x = 2
+        self.textedit = TextEdit(self.text)
+        self.top.add(self.textedit, halign='fill', valign='fill')
 
         editbox = self.textedit.editbox
         editbox.add_handler('keypress', self.on_any_input)
--- a/demo_layout.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/demo_layout.py	Fri Jan 18 22:36:50 2013 +0100
@@ -10,12 +10,11 @@
 class MyApplication(Application):
     def __init__(self):
         Application.__init__(self)
+
+        self.top = VerticalLayout(homogeneous=False)
         self.top.add_handler('keypress', self.on_top_keypress)
-
         #self.top.borders = (1,1,1,1)
 
-        self.top.layout = VerticalLayout(homogeneous=False)
-
         self._row_num = 0
         self.buildrow()
         self.buildrow(expand=True)
@@ -26,9 +25,8 @@
         self.buildrow(homogeneous=True, fill=True, spacing=2)
 
     def buildrow(self, homogeneous=False, spacing=0, expand=False, fill=False):
-        hbox = Container()
-        hbox.sizereq.h = 2
-        hbox.layout = HorizontalLayout(homogeneous=homogeneous, spacing=spacing)
+        hbox = HorizontalLayout(homogeneous=homogeneous, spacing=spacing)
+        hbox.resize(h=2)
         self._row_num += 1
         hbox.name = 'hbox' + str(self._row_num)
         self.top.add(hbox)
--- a/demo_treeview.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/demo_treeview.py	Fri Jan 18 22:36:50 2013 +0100
@@ -9,7 +9,7 @@
 
 class MyApplication(Application):
     def __init__(self):
-        Application.__init__(self)
+        Application.__init__(self, top_layout=VerticalLayout)
         self.top.add_handler('keypress', self.on_top_keypress)
 
         model = TreeModel()
@@ -29,8 +29,6 @@
         scroll.add(view)
         self.top.add(scroll, expand=True, fill=True)
 
-        self.top.layout = VerticalLayout()
-
     def on_top_keypress(self, ev):
         if ev.keyname == 'escape':
             self.terminate()
--- a/demo_window.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/demo_window.py	Fri Jan 18 22:36:50 2013 +0100
@@ -8,28 +8,33 @@
 from tuikit.application import Application
 from tuikit.window import Window
 from tuikit.button import Button
+from tuikit import AnchorLayout
+from tuikit.common import Borders
 
 
 class MyApplication(Application):
     def __init__(self):
         Application.__init__(self)
+        self.top = AnchorLayout()
+        self.top.name = 'top'
         self.top.add_handler('keypress', self.on_top_keypress)
 
         #edit = EditField(50, 'DlouhyTest12')
         #self.top.add(edit)
 
         win = Window()
-        self.top.add(win)
+        win.title = 'demo_window'
+        win.resize(80, 25)
+        self.top.add(win, halign='left', valign='top')
 
         button = Button('click!')
-        win.add(button)
-        button.x = 10
-        button.y = 7
+#        win.add(button, x=10, y=6)
 
         button.add_handler('click', self.on_button_click)
         self.button = button
 
-        subwin = Window(8,8)
+        subwin = Window()
+        subwin.name = 'subwin'
         win.add(subwin)
 
 
@@ -48,9 +53,9 @@
     locale.setlocale(locale.LC_ALL, '')
     os.environ['ESCDELAY'] = '25' # do not wait 1 second after pressing Escape key
     app = MyApplication()
-    #app.start()
+    app.start()
 
-    cProfile.run('app.start()', 'demo_window.appstats')
-    p = pstats.Stats('demo_window.appstats')
-    p.sort_stats('time', 'cumulative').print_stats(20)
+    #cProfile.run('app.start()', 'demo_window.appstats')
+    #p = pstats.Stats('demo_window.appstats')
+    #p.sort_stats('time', 'cumulative').print_stats(20)
 
--- a/docs/index.rst	Thu Jan 10 00:03:34 2013 +0100
+++ b/docs/index.rst	Fri Jan 18 22:36:50 2013 +0100
@@ -18,19 +18,3 @@
    colors
    driver
 
-.. inheritance-diagram:: tuikit.application
-                         tuikit.events
-                         tuikit.widget
-                         tuikit.container
-                         tuikit.window
-                         tuikit.button
-                         tuikit.label
-                         tuikit.scrollbar
-                         tuikit.textedit
-                         tuikit.editfield
-                         tuikit.editbox
-                         tuikit.combobox
-                         tuikit.menu
-                         tuikit.menubar
-                         tuikit.treeview
-
--- a/docs/widget.rst	Thu Jan 10 00:03:34 2013 +0100
+++ b/docs/widget.rst	Fri Jan 18 22:36:50 2013 +0100
@@ -16,11 +16,11 @@
 
    .. attribute:: x
                   y
-      
+
       Position inside parent widget. Modified by layout manager.
-   
+
    .. attribute:: width
                   height
-      
+
       Actual size. Modified by layout manager.
 
--- a/sdlterm/src/sdlterm.cc	Thu Jan 10 00:03:34 2013 +0100
+++ b/sdlterm/src/sdlterm.cc	Fri Jan 18 22:36:50 2013 +0100
@@ -371,9 +371,6 @@
 
 bool Terminal::wait_event(Event &event, Uint32 timeout)
 {
-    static SDL_Event sdl_event;
-    bool event_ready = false;
-
     // use timer to simulate SDL_WaitEventTimeout, which is not available in SDL 1.2
     SDL_TimerID timer_id = NULL;
     if (timeout)
@@ -381,6 +378,9 @@
         timer_id = SDL_AddTimer(timeout, _wait_event_callback, NULL);
     }
 
+    // loop until we have something to return
+    SDL_Event sdl_event;
+    bool event_ready = false;
     while (SDL_WaitEvent(&sdl_event) && !event_ready)
     {
         switch (sdl_event.type)
--- a/tuikit/__init__.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/tuikit/__init__.py	Fri Jan 18 22:36:50 2013 +0100
@@ -9,7 +9,7 @@
 from tuikit.editbox import EditBox
 from tuikit.editfield import EditField
 from tuikit.label import Label
-from tuikit.layout import VerticalLayout, HorizontalLayout, GridLayout
+from tuikit.layout import VerticalLayout, HorizontalLayout, GridLayout, AnchorLayout
 from tuikit.menu import Menu
 from tuikit.menubar import MenuBar
 from tuikit.pager import Pager
--- a/tuikit/application.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/tuikit/application.py	Fri Jan 18 22:36:50 2013 +0100
@@ -3,7 +3,7 @@
 import logging
 import time
 
-from tuikit.container import Container
+from tuikit.layout import AnchorLayout
 
 
 class Timer:
@@ -51,66 +51,64 @@
                 timeout[0] = adjusted_delay
 
 
-class TopWindow(Container):
-
-    '''Top window of an application. Covers entire screen.'''
-
-    def __init__(self):
-        '''Create top window.'''
-        Container.__init__(self)
-        self.top = self
-        self.timer = Timer()
-
-    def draw(self, driver, x=0, y=0):
-        """Draw the top window and its children.
-
-        This is entry point for full screen redraw.
-
-        """
-        driver.erase()
-        Container.draw(self, driver, x, y)
-
-
 class Application:
 
     '''Application class. Defines main loop.'''
 
-    def __init__(self, driver = 'sdl'):
+    def __init__(self, top_layout=AnchorLayout, driver='curses'):
         '''Create application.'''
-
-        self.top = TopWindow()
-        '''Top window.'''
+        self._setup_logging()
+        
+        # Top widget
+        self._top = None
+        self._timer = Timer()
+        self.top = top_layout()
 
         self.quit = False
 
+        #: Driver class instance (render + input), e.g. DriverCurses.
         self.driver = self.get_driver_instance(driver)
-        '''Driver class instance (render + input), e.g. DriverCurses.'''
 
+    def _setup_logging(self):
         self.log = logging.getLogger('tuikit')
         self.log.setLevel(logging.DEBUG)
         handler = logging.FileHandler('./tuikit.log')
         formatter = logging.Formatter('%(asctime)s %(levelname)-5s %(message)s', '%y-%m-%d %H:%M:%S')
         handler.setFormatter(formatter)
         self.log.addHandler(handler)
-        self.log.info('=== start ===')
+
+    @property
+    def top(self):
+        return self._top
+
+    @top.setter
+    def top(self, value):
+        self._top = value
+        self._top.top = value
+        self._top.timer = self._timer
+        self._top.add_handler('draw', self._on_top_draw)
+
+    def _on_top_draw(self, ev):
+        ev.driver.erase()
 
     def start(self):
         '''Start application. Runs main loop.'''
-        self.driver.start(self.mainloop)
+        self.log.info('=== start ===')
+        self.driver.start(self.main_loop)
 
     def terminate(self):
         '''Terminate application.'''
         self.quit = True
 
-    def mainloop(self):
+    def main_loop(self):
         '''The main loop.'''
-        self.applytheme()
-        self.top.size = self.driver.size  # link top widget size to screen size
-        self.top.emit('resize')
-        timer = self.top.timer
+        self.apply_theme()
+        self._top._size = self.driver.size  # link top widget size to screen size
+        self._top.emit('resize')
+        timer = self._timer
 
         while True:
-            self.top.draw(self.driver)
+            self._top.draw(self.driver, 0, 0)
             self.driver.commit()
 
             timeout = timer.nearest_timeout()
@@ -121,13 +119,14 @@
                 if event[0] == 'quit':
                     self.quit = True
                 else:
-                    self.top.emit(event[0], *event[1:])
+                    self._top.emit(event[0], *event[1:])
 
             if self.quit:
                 break
+        self.log.info('=== quit ===')
 
 
-    def applytheme(self):
+    def apply_theme(self):
         #TODO: allow custom colors:
         #    e.g. "blue (#2020FF) on black (#101010), underline"
         #    color in brackets is used when driver supports custom colors
--- a/tuikit/button.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/tuikit/button.py	Fri Jan 18 22:36:50 2013 +0100
@@ -10,18 +10,18 @@
 
     def __init__(self, label=''):
         '''Create button with given label, size according to label.'''
-        w = len(label) + 4
-        h = 1
-        Widget.__init__(self, w, h)
+        Widget.__init__(self)
 
         #: Button label.
-        self.label = label
+        self._label = ''
         #: Text or graphics to be added before label
         self.prefix = '['
         #: Text or graphics to be added after label
         self.suffix = ']'
         #: How should label be aligned if button has excess space - center | left | right
         self.align = 'center'
+        #: Padding between prefix/suffix and label
+        self.padding = 1
 
         self.allow_focus = True
 
@@ -29,12 +29,20 @@
         self.bghi = 'button-active'
         self.highlight = False
 
-        # size
-        self.sizereq.w = w
-        self.sizereq.h = h
-
         self.add_events('click', Event)
 
+        self.label = label
+
+    @property
+    def label(self):
+        """Button label."""
+        return self._label
+
+    @label.setter
+    def label(self, value):
+        self._label = value
+        w = len(value) + len(self.prefix) + len(self.suffix) + 2 * self.padding
+        self._default_size.update(w, 1)
 
     def on_draw(self, ev):
         pad = self.width - len(self.label) - len(self.prefix) - len(self.suffix)
--- a/tuikit/combobox.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/tuikit/combobox.py	Fri Jan 18 22:36:50 2013 +0100
@@ -1,38 +1,40 @@
 # -*- coding: utf-8 -*-
 
-from tuikit.container import Container
+from tuikit.layout import HorizontalLayout
 from tuikit.editfield import EditField
 from tuikit.button import Button
 from tuikit.menu import Menu
+import logging
 
 
-class ComboBox(Container):
-    def __init__(self, width=15, value='', items=[]):
-        Container.__init__(self, width, 1)
-
+class ComboBox(HorizontalLayout):
+    def __init__(self, value='', items=[]):
+        HorizontalLayout.__init__(self)
+        self._default_size.update(15, 1)
         self.allow_focus = True
-
         self.colorprefix = 'combo:'
 
-        self._edit = EditField(width - 3, value)
-        self.add(self._edit)
+        self._edit = EditField(value)
+        self._edit.resize(self.width - 3, 1)
+        self.add(self._edit, expand=True, fill=True)
 
         self._btn = Button('v')
         self._btn.prefix = ''
         self._btn.suffix = ''
-        self._btn.x = width - 3
-        self._btn.width = 3
+        self._btn.resize(3, 1)
         self._btn.add_handler('click', self._on_btn_click)
-        self.add(self._btn)
+        self.add(self._btn, expand=False)
 
         self._menu = Menu(items)
         self._menu.hide()
-        self._menu.allow_layout = False
-
-    def _set_top(self, value):
-        Container._set_top(self, value)
-        self.top.add(self._menu)
+        self.add_floater(self._menu)
 
     def _on_btn_click(self, ev):
         self._menu.show()
 
+    def on_resize(self, ev):
+        HorizontalLayout.on_resize(self, ev)
+        logging.getLogger('tuikit').info('w = %s', self.width)
+        self._menu.move(self.x, self.y + 1)
+        self._menu.resize(w=self.width)
+
--- a/tuikit/common.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/tuikit/common.py	Fri Jan 18 22:36:50 2013 +0100
@@ -3,6 +3,37 @@
 from tuikit.events import Event, Emitter
 
 
+class Select:
+    def __init__(self, selected=None):
+        if not hasattr(self, '_options'):
+            raise TypeError('Cannot instantiate Select class.')
+        self._selected = selected or self._options[0]
+
+    def update(self, selected):
+        if selected not in self._options:
+            raise ValueError('Select: %r not in options %r' % (selected, self._options))
+        self._selected = selected
+
+    def select_next(self):
+        i = self._options.index(self._selected)
+        try:
+            self._selected = self._options[i+1]
+        except IndexError:
+            self._selected = self._options[0]
+
+    @property
+    def selected(self):
+        return self._selected
+
+    def __repr__(self):
+        return self.__class__.__name__ + '(%r)' % self._selected
+
+
+def make_select(*args):
+    name = ''.join([x.capitalize() for x in args]) + 'Select'
+    return type(name, (Select,), {'_options': args})
+
+
 class Coords:
 
     '''2D coordinates.'''
@@ -28,6 +59,19 @@
     def __repr__(self):
         return 'Coords(x={0.x},y={0.y})'.format(self)
 
+    def update(self, x=None, y=None):
+        if isinstance(x, Coords) and y is None:
+            self.x, self.y = x
+        else:
+            if isinstance(x, int):
+                self.x = x
+            elif x is not None:
+                raise ValueError('Coords.update(): first parameter must be int or Coords')
+            if isinstance(y, int):
+                self.y = y
+            elif y is not None:
+                raise ValueError('Coords.update(): second parameter must be int')
+
 
 class Size(Emitter):
 
@@ -78,6 +122,22 @@
     def __repr__(self):
         return 'Size(w={0._w},h={0._h})'.format(self)
 
+    def update(self, w=None, h=None):
+        old_w, old_h = self._w, self._h
+        if isinstance(w, Size) and h is None:
+            self._w, self._h = w
+        else:
+            if isinstance(w, int):
+                self._w = w
+            elif w is not None:
+                raise ValueError('Size.update(): first parameter must be int or Size')
+            if isinstance(h, int):
+                self._h = h
+            elif h is not None:
+                raise ValueError('Size.update(): second parameter must be int')
+        if self._w != old_w or self._h != old_h:
+            self.emit('change')
+
 
 class Rect:
 
@@ -103,11 +163,13 @@
 
     '''
 
-    def __init__(self, l=0, t=0, r=0, b=0):
+    def __init__(self, l=0, t=0, r=0, b=0, full=None):
         self.l = l # left
         self.t = t # top
         self.r = r # right
         self.b = b # bottom
+        if full is not None:
+            self.l = self.t = self.r = self.b = full
 
     def __getitem__(self, key):
         try:
@@ -120,6 +182,16 @@
     def __repr__(self):
         return 'Borders(l={0.l},t={0.t},r={0.r},b={0.b})'.format(self)
 
+    def update(self, *args, **kwargs):
+        if len(args) == 4:
+            self.l, self.t, self.r, self.b = args
+        elif len(args) == 1 and isinstance(args[0], Borders):
+            self.l, self.t, self.r, self.b = args[0]
+        elif len(args):
+            raise ValueError('Borders.update() takes exactly 4 positional arguments.')
+        for arg in kwargs:
+            setattr(self, arg, kwargs[arg])
+
 
 class ClipStack:
 
@@ -176,6 +248,11 @@
     # http://en.wikipedia.org/wiki/List_of_Unicode_characters#Geometric_shapes
     UP_ARROW = '▲' #curses.ACS_UARROW
     DOWN_ARROW = '▼' #curses.ACS_DARROW
+    LEFT_ARROW = '◀'
+    RIGHT_ARROW = '▶'
+    CIRCLE = '●'
+    DIAMOND = '◆'
+    MIDDLE_DOT = '·'
 
     # http://en.wikipedia.org/wiki/Box-drawing_characters
     LIGHT_SHADE = '░' #curses.ACS_BOARD
--- a/tuikit/container.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/tuikit/container.py	Fri Jan 18 22:36:50 2013 +0100
@@ -2,18 +2,20 @@
 
 from tuikit.widget import Widget
 from tuikit.common import Borders, Coords
+import logging
 
 
 class Container(Widget):
 
     '''Container widget. Base for any widget which can contain other widgets.'''
 
-    def __init__(self, width = 10, height = 10):
+    def __init__(self):
         '''Create container of requested size.'''
-        Widget.__init__(self, width, height)
+        Widget.__init__(self)
 
         #: List of child widgets.
         self.children = []
+        self._hint_class = {}
 
         #: Offset for child widgets
         self.offset = Coords()
@@ -36,16 +38,29 @@
 
         self.trap_focus = False  # if True, tab cycles inside container
 
-
     def add(self, widget, **kwargs):
         '''Add widget into this container.'''
         self.children.append(widget)
         widget.parent = self
         widget.top = self.top
-        widget.hints.update(kwargs)
+        widget.hints.update({k:v() for k,v in self._hint_class.items()})
+        for key in kwargs:
+            try:
+                widget.hints[key].update(kwargs[key])
+            except AttributeError:
+                widget.hints[key] = widget.hints[key].__class__(kwargs[key])
         if self.focuschild is None and widget.can_focus():
             self.focuschild = widget
 
+    def add_floater(self, widget, **kwargs):
+        widget.floater = True
+        widget._size.update(widget._sizereq)
+        self.add(widget, **kwargs)
+
+    def register_hints(self, *hints):
+        for hint_name, hint_class in zip(hints[::2], hints[1::2]):
+            self._hint_class[hint_name] = hint_class
+
     @property
     def layout(self):
         return self._layout
@@ -57,6 +72,9 @@
         self._layout.container = self
 
 
+    def move_child(self, child, x, y):
+        pass
+
     def _set_top(self, value):
         self._top = value
         for child in self.children:
@@ -112,23 +130,37 @@
 
         Widget.draw(self, driver, x, y)
 
-        for child in [ch for ch in self.children if not ch.allow_layout]:
-            child.draw(driver, x + child.x, y + child.y)
-
         l, t, r, b = self.borders
         driver.clipstack.push(x+l, y+t, self.width-l-r, self.height-t-b)
 
-        for child in [ch for ch in self.children if ch.allow_layout]:
+        for child in [ch for ch in self.children if not ch.floater]:
             child.draw(driver,
                 x + self.offset.x + child.x,
                 y + self.offset.y + child.y)
 
         driver.clipstack.pop()
 
+        # draw floaters - no clipping, no colorprefix
+        if self.parent is None:
+            self.draw_floaters(driver, x, y)
+
         if self.colorprefix:
             driver.popcolorprefix()
         driver.clipstack.pop()
 
+    def draw_floaters(self, driver, x, y):
+        logging.getLogger('tuikit').info('draw_floaters %s %r %r', self, x ,y )
+        # draw floaters in this container
+        for child in [ch for ch in self.children if ch.floater]:
+            child.draw(driver,
+                x + self.offset.x + child.x,
+                y + self.offset.y + child.y)
+        # delve into child containers, draw their floaters
+        for child in [ch for ch in self.children if not ch.floater and isinstance(ch, Container)]:
+            child.draw_floaters(driver,
+                x + self.offset.x + child.x,
+                y + self.offset.y + child.y)
+
     def on_resize(self, ev):
         for child in self.children:
             child.emit('resize')
@@ -144,9 +176,9 @@
     def on_mousedown(self, ev):
         handled = False
         for child in reversed(self.children):
-            if child.enclose(ev.wx, ev.wy):
-                childev = ev.childevent(child)
-                child.emit('mousedown', childev)
+            if child.enclose(ev.wx - self.offset.x, ev.wy - self.offset.y):
+                child_ev = ev.make_child_event(self, child)
+                child.emit('mousedown', child_ev)
                 self.mousechild = child
                 handled = True
                 break
@@ -154,23 +186,23 @@
 
     def on_mouseup(self, ev):
         if self.mousechild:
-            childev = ev.childevent(self.mousechild)
-            self.mousechild.emit('mouseup', childev)
+            child_ev = ev.make_child_event(self, self.mousechild)
+            self.mousechild.emit('mouseup', child_ev)
             self.mousechild = None
             return True
 
     def on_mousemove(self, ev):
         if self.mousechild:
-            childev = ev.childevent(self.mousechild)
-            self.mousechild.emit('mousemove', childev)
+            child_ev = ev.make_child_event(self, self.mousechild)
+            self.mousechild.emit('mousemove', child_ev)
             return True
 
     def on_mousewheel(self, ev):
         handled = False
         for child in reversed(self.children):
-            if child.enclose(ev.wx, ev.wy):
-                childev = ev.childevent(child)
-                child.emit('mousewheel', childev)
+            if child.enclose(ev.wx - self.offset.x, ev.wy - self.offset.y):
+                child_ev = ev.make_child_event(self, child)
+                child.emit('mousewheel', child_ev)
                 handled = True
                 break
         return handled
--- a/tuikit/driver_sdl.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/tuikit/driver_sdl.py	Fri Jan 18 22:36:50 2013 +0100
@@ -131,7 +131,7 @@
         if len(self.colorstack):
             attr = self.colorstack[-1]
         else:
-            attr = 0
+            attr = 7
         self.sdlterm.set_attr(attr)
 
     def _color_by_name(self, name):
--- a/tuikit/editbox.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/tuikit/editbox.py	Fri Jan 18 22:36:50 2013 +0100
@@ -5,9 +5,9 @@
 
 
 class EditBox(Widget):
-    def __init__(self, width=20, height=20, text=''):
-        Widget.__init__(self, width, height)
-
+    def __init__(self, text=''):
+        Widget.__init__(self)
+        self._default_size.update(20, 20)
         self.allow_focus = True
 
         self.xofs = 0
--- a/tuikit/editfield.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/tuikit/editfield.py	Fri Jan 18 22:36:50 2013 +0100
@@ -6,8 +6,9 @@
 
 
 class EditField(Widget):
-    def __init__(self, width=10, value=''):
-        Widget.__init__(self, width, 1)
+    def __init__(self, value=''):
+        Widget.__init__(self)
+        self._default_size.update(10, 1)
 
         self.allow_focus = True
 
--- a/tuikit/events.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/tuikit/events.py	Fri Jan 18 22:36:50 2013 +0100
@@ -20,6 +20,9 @@
     def __getitem__(self, key):
         return self.__dict__[key]
 
+    def __repr__(self):
+        return 'Event()'
+
 
 class DrawEvent(Event):
     def __init__(self, driver, x, y):
@@ -66,7 +69,7 @@
         #: Mouse button: left=1, middle=2, right=3, wheelup=4 wheeldown=5
         self.button = button
 
-    def childevent(self, child):
+    def make_child_event(self, container, child):
         ev = MouseEvent(self.x, self.y, self.button)
         # original local coordinates are new parent coordinates
         ev.px = self.wx
@@ -97,7 +100,7 @@
         It also serves as initializer, other methods of Emitter
         will not work if add_events was not called.
 
-        *events -- Arguments must be given in pairs.
+        events -- Arguments must be given in pairs.
 
         Each pair consists of event_name, event_class:
             event_name -- a string used in add_handler(), emit() etc.
@@ -185,10 +188,6 @@
         is passed to handlers.
 
         """
-        logging.getLogger('tuikit').debug('Emit "%s" on %s %s',
-            event_name,
-            self.__class__.__name__,
-            getattr(self, 'name', None) or id(self))
         # create event from specified event class, or use first argument
         if len(args) == 1 and isinstance(args[0], Event):
             event = args[0]
@@ -197,6 +196,11 @@
         # set originator to instance on which emit() was called
         event.originator = self
         event.event_name = event_name
+        # log
+        logging.getLogger('tuikit').debug('Emit %r %r on %s %s',
+            event_name, event,
+            self.__class__.__name__,
+            getattr(self, 'name', None) or id(self))
         # call handlers from first to last, stop if satisfied
         for handler in self._event_handlers[event_name]:
             handled = handler(event)
--- a/tuikit/label.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/tuikit/label.py	Fri Jan 18 22:36:50 2013 +0100
@@ -5,8 +5,8 @@
 
 class Label(Widget):
     def __init__(self, label=''):
-        Widget.__init__(self, len(label), 1)
-
+        Widget.__init__(self)
+        self._default_size.update(len(label), 1)
         self.label = label
         self.color = 'normal'
 
--- a/tuikit/layout.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/tuikit/layout.py	Fri Jan 18 22:36:50 2013 +0100
@@ -8,50 +8,119 @@
 '''
 
 import math
+import logging
 
-from tuikit.common import Rect
+from tuikit.common import Coords, Size, Rect, Borders, make_select
+from tuikit.container import Container
 
 
-class Layout:
-    def __init__(self):
-        self._container = None
-
-    @property
-    def container(self):
-        return self._container
+class Layout(Container):
+    def _get_children(self):
+        return [child for child in self.children
+            if not child.floater and not child.hidden]
 
-    @container.setter
-    def container(self, value):
-        self._container = value
-        self._container.add_handler('resize', self._on_container_resize)
-
-    def _on_container_resize(self, ev):
-        self.resize()
-
-    def _getchildren(self):
-        return [child for child in self.container.children
-            if child.allow_layout and not child.hidden]
-
-    def _getregion(self):
+    def _get_region(self):
         c = self.container
         bl, bt, br, bb = c.borders
         return Rect(bl, bt, c.width - bl - br, c.height - bt - bb)
 
 
+class AnchorLayout(Layout):
+
+    """Attach widgets to borders of container.
+
+    Also allows absolute positioning when possible (not for center, fill).
+
+    """
+
+    def __init__(self):
+        Layout.__init__(self)
+        self.register_hints(
+            'halign', make_select('left', 'right', 'fill', 'center'),
+            'valign', make_select('top', 'bottom', 'fill', 'center'),
+            'margin', Borders,
+            )
+
+    def on_resize(self, ev):
+        for child in self._get_children():
+            reqw = max(child.sizereq.w, child.sizemin.w)
+            reqh = max(child.sizereq.h, child.sizemin.h)
+            ha = child.hints['halign'].selected
+            va = child.hints['valign'].selected
+            margin = child.hints['margin']
+            if ha == 'left':
+                x = margin.l
+                w = reqw
+            if ha == 'right':
+                x = self.width - margin.r - reqw
+                w = reqw
+            if ha == 'fill':
+                x = margin.l
+                w = self.width - margin.l - margin.r
+            if ha == 'center':
+                x = (self.width - reqw) // 2
+                w = reqw
+            if va == 'top':
+                y = margin.t
+                h = reqh
+            if va == 'bottom':
+                y = self.height - margin.b - reqh
+                h = reqh
+            if va == 'fill':
+                y = margin.t
+                h = self.height - margin.t - margin.b
+            if va == 'center':
+                y = (self.height - reqh) // 2
+                h = reqh
+            child._pos.update(x=x, y=y)
+            child._size.update(w=w, h=h)
+
+    def move_child(self, child, x, y):
+        if not child in self.children:
+            raise ValueError('AnchorLayout.move(): Cannot move foreign child.')
+        margin = child.hints['margin']
+        ha = child.hints['halign'].selected
+        va = child.hints['valign'].selected
+        ofsx = x - child.x
+        ofsy = y - child.y
+        if ha == 'left':
+            margin.l += ofsx
+            newx = margin.l
+        elif ha == 'right':
+            margin.r -= ofsx
+            newx = self.width - margin.r - child.sizereq.w
+        else:
+            # do not move when halign is center,fill
+            newx = child.x
+        if va == 'top':
+            margin.t += ofsy
+            newy = margin.t
+        elif va == 'bottom':
+            margin.b -= ofsy
+            newy = self.height - margin.b - child.sizereq.h
+        else:
+            # do not move when valign is center,fill
+            newy = child.y
+        child._pos.update(x=newx, y=newy)
+
+
 class LinearLayout(Layout):
     def __init__(self, homogeneous=False, spacing=0):
         Layout.__init__(self)
         self.homogeneous = homogeneous
         self.spacing = spacing
+        self.register_hints(
+            'expand', bool,
+            'fill', bool,
+            )
 
     def _resize(self, ax1, ax2):
-        children = self._getchildren()
-        c = self.container
-        b1 = c.borders[ax1]
-        b2 = c.borders[ax2]
+        children = self._get_children()
+        b1 = self.borders[ax1]
+        b2 = self.borders[ax2]
         # available space
-        space1 = c.size[ax1] - b1 - c.borders[ax1+2]
-        space2 = c.size[ax2] - b2 - c.borders[ax2+2]
+        space1 = self._size[ax1] - b1 - self.borders[ax1+2]
+        space2 = self._size[ax2] - b2 - self.borders[ax2+2]
 
         # all space minus spacing
         space_to_divide = space1 - (len(children) - 1) * self.spacing
@@ -59,7 +128,7 @@
             # reduce by space acquired by children
             space_to_divide -= sum([child.sizereq[ax1] for child in children])
             # number of children with expanded hint
-            expanded_num = len([ch for ch in children if ch.hint('expand')])
+            expanded_num = len([ch for ch in children if ch.hints['expand']])
         else:
             # all children are implicitly expanded
             expanded_num = len(children)
@@ -70,40 +139,42 @@
 
         offset = 0.
         for child in children:
-            child.position[ax1] = b1 + int(offset)
-            child.position[ax2] = b2
-            child.size[ax1] = child.sizereq[ax1]
-            child.size[ax2] = space2
+            pos = Coords()
+            pos[ax1] = b1 + int(offset)
+            pos[ax2] = b2
+            size = Size()
+            size[ax1] = max(child.sizereq[ax1], child.sizemin[ax1])
+            size[ax2] = space2
 
-            if child.hint('expand') or self.homogeneous:
+            if child.hints['expand'] or self.homogeneous:
                 maxsize = int(round(space_child + math.modf(offset)[0], 2))
                 offset += space_child + self.spacing
                 if not self.homogeneous:
                     maxsize += child.sizereq[ax1]
                     offset += child.sizereq[ax1]
 
-                if child.hint('fill'):
-                    child.size[ax1] = maxsize
+                if child.hints['fill']:
+                    size[ax1] = maxsize
                 else:
-                    child.position[ax1] += int((maxsize - child.size[ax1])/2)
+                    pos[ax1] += int((maxsize - size[ax1])/2)
             else:
-                offset += child.size[ax1]
+                offset += size[ax1]
 
-            child.emit('resize')
-        c.redraw()
+            child._pos.update(pos)
+            child._size.update(size)
 
 
 class VerticalLayout(LinearLayout):
-    def resize(self):
-        ax1 = 1 # primary dimension - y
-        ax2 = 0 # secondary dimension - x
+    def on_resize(self, ev):
+        ax1 = 1 # primary dimension = y
+        ax2 = 0 # secondary dimension = x
         self._resize(ax1, ax2)
 
 
 class HorizontalLayout(LinearLayout):
-    def resize(self):
-        ax1 = 0 # primary dimension - x
-        ax2 = 1 # secondary dimension - y
+    def on_resize(self, ev):
+        ax1 = 0 # primary dimension = x
+        ax2 = 1 # secondary dimension = y
         self._resize(ax1, ax2)
 
 
--- a/tuikit/menu.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/tuikit/menu.py	Fri Jan 18 22:36:50 2013 +0100
@@ -2,13 +2,15 @@
 
 from tuikit.widget import Widget
 from tuikit.events import GenericEvent
+import logging
 
 
 class Menu(Widget):
     def __init__(self, items=[]):
         Widget.__init__(self)
-        self.width = max([len(x[0]) for x in items if x is not None]) + 4
-        self.height = len(items) + 2
+        w = max([len(x[0]) for x in items if x is not None]) + 4
+        h = len(items) + 2
+        self._default_size.update(w, h)
 
         self.allow_focus = True
 
@@ -21,6 +23,7 @@
         self.add_events('activate', GenericEvent)
 
     def on_draw(self, ev):
+        logging.getLogger('tuikit').info('menu draw %s %s %s', ev, ev.x, ev.y)
         ev.driver.pushcolor(self.bg)
         ev.driver.frame(ev.x, ev.y, self.width, self.height)
         i = 1
--- a/tuikit/scrollbar.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/tuikit/scrollbar.py	Fri Jan 18 22:36:50 2013 +0100
@@ -2,67 +2,133 @@
 
 from tuikit.widget import Widget
 from tuikit.events import Event
+from tuikit.common import UnicodeGraphics
 
 
-class VScrollbar(Widget):
-    def __init__(self, height=10):
-        Widget.__init__(self, 1, height)
+class Scrollbar(Widget):
 
-        self._max = height - 3
-        self._pos = 0
-        self._thumbpos = 0
+    """Abstract base class for scrollbars."""
+
+    def __init__(self):
+        Widget.__init__(self)
 
-        # interval for continuous scroll when user holds mouse on scrollbar arrow
-        self.scroll_delay = 0.150
-        self.scroll_interval = 0.030
+        # Scrolling range is 0 .. _scroll_max
+        self._scroll_max = self._get_length() - 3
+        # Current position of scrollbar in scrolling range.
+        self._scroll_pos = 0
+        # Current position of thumb on scrollbar - used for draw.
+        self._thumb_pos = 0
+        # Auxilliary variable, True when user holds mouse on thumb.
+        self._dragging = False
+        # Auxilliary variable, 'up' or 'down' depending on last move direction.
+        self._move = None
 
-        self.dragging = False
-        self.move = None
+        #: delay before continuous scrolling when user holds mouse on scrollbar arrow
+        self.scroll_delay = 0.150
+        #: interval for continuous scrolling
+        self.scroll_interval = 0.030
+        #: scrollbar styling
+        unigraph = UnicodeGraphics()
+        self.track_char = unigraph.MIDDLE_DOT
+        self.thumb_char = unigraph.CIRCLE
 
+        # change event is emitted when user moves scrollbar (even programmatically)
         self.add_events('change', Event)
 
     @property
-    def max(self):
+    def scroll_max(self):
         """Maximum for scrolling position."""
-        return self._max
+        return self._scroll_max
 
-    @max.setter
-    def max(self, value):
-        self._max = value
-        self._update_thumbpos()
+    @scroll_max.setter
+    def scroll_max(self, value):
+        self._scroll_max = value
+        self._update_thumb_pos()
 
     @property
-    def pos(self):
+    def scroll_pos(self):
         """Scrolling position.
 
         Integer number between 0 and 'max'.
 
         """
-        return self._pos
+        return self._scroll_pos
 
-    @pos.setter
-    def pos(self, value):
-        if self._pos != value:
-            self._pos = value
-            self._update_thumbpos()
+    @scroll_pos.setter
+    def scroll_pos(self, value):
+        if self._scroll_pos != value:
+            self._scroll_pos = value
+            self._update_thumb_pos()
             self.emit('change')
 
-    def _update_thumbpos(self):
-        self._thumbpos = 0
-        if self._max and self._pos <= self._max:
-            self._thumbpos = int(round(self._pos / self._max * (self.height - 3)))
+    def move_up(self):
+        """Move scrolling position up/left."""
+        if self._scroll_pos > 0:
+            self.scroll_pos = self._scroll_pos - 1
+        self._move = 'up'
+
+    def move_down(self):
+        """Move scrolling position down/right."""
+        if self._scroll_pos < self._scroll_max:
+            self.scroll_pos = self._scroll_pos + 1
+        self._move = 'down'
+
+    def drag(self, mouse_pos):
+        """Scroll using mouse drag on thumb.
+
+        Args:
+            mouse_pos: new position of mouse, in range 0 .. self._get_length()
+
+        """
+        new_pos = int(round((mouse_pos - 1) / (self._get_length() - 3) * self._scroll_max))
+        if new_pos < 0:
+            new_pos = 0
+        if new_pos > self._scroll_max:
+            new_pos = self._scroll_max
+        if self._scroll_pos != new_pos:
+            self.scroll_pos = new_pos
+
+    def _get_length(self):
+        """Return length of scrollbar.
+
+        This will be widget height for vertical scrollbar,
+        width for horizontal scrollbar.
+
+        """
+        raise NotImplementedError()
+
+    def _update_thumb_pos(self):
+        """Update value of internal variable _thumb_pos."""
+        self._thumb_pos = 0
+        if self._scroll_max and self._scroll_pos <= self._scroll_max:
+            self._thumb_pos = int(round(self._scroll_pos / self._scroll_max * (self._get_length() - 3)))
         self.redraw()
 
+    def _continuous_scroll(self):
+        if self._move == 'up':
+            self.move_up()
+        if self._move == 'down':
+            self.move_down()
+        self.add_timeout(self.scroll_interval, self._continuous_scroll)
+
+
+class VScrollbar(Scrollbar):
+    def __init__(self):
+        Scrollbar.__init__(self)
+        self._default_size.update(1, 20)
+
     def on_draw(self, ev):
+        ev.driver.pushcolor('normal')
         ev.driver.putch(ev.x, ev.y, ev.driver.unigraph.UP_ARROW)
-        for i in range(ev.y + 1, ev.y + self.height - 1):
-            ev.driver.putch(ev.x, i, ev.driver.unigraph.LIGHT_SHADE)
-        ev.driver.putch(ev.x, ev.y + 1 + self._thumbpos, ev.driver.unigraph.BLOCK)
+        for i in range(1, self.height - 1):
+            ev.driver.putch(ev.x, ev.y + i, self.track_char)
+        ev.driver.putch(ev.x, ev.y + 1 + self._thumb_pos, self.thumb_char)
         ev.driver.putch(ev.x, ev.y + self.height - 1, ev.driver.unigraph.DOWN_ARROW)
+        ev.driver.popcolor()
 
     def on_mousedown(self, ev):
-        self.dragging = False
-        self.move = None
+        self._dragging = False
+        self._move = None
         # arrow buttons
         if ev.wy == 0 or ev.wy == self.height - 1:
             if ev.wy == 0:
@@ -72,47 +138,72 @@
             self.add_timeout(self.scroll_delay, self._continuous_scroll)
             return
         # thumb bar
-        if ev.wy == 1 + self._thumbpos:
-            self.dragging = True
-            return
-
-    def on_mouseup(self, ev):
-        if self.dragging:
-            self.drag(ev.wy)
-            self.dragging = False
-            return
-        if self.move:
-            self.remove_timeout(self._continuous_scroll)
-            self.move = None
+        if ev.wy == 1 + self._thumb_pos:
+            self._dragging = True
             return
 
     def on_mousemove(self, ev):
-        if self.dragging:
+        if self._dragging:
             self.drag(ev.wy)
 
-    def move_up(self):
-        if self._pos > 0:
-            self.pos = self._pos - 1
-        self.move = 'up'
+    def on_mouseup(self, ev):
+        if self._dragging:
+            self.drag(ev.wy)
+            self._dragging = False
+            return
+        if self._move:
+            self.remove_timeout(self._continuous_scroll)
+            self._move = None
+            return
+
+    def _get_length(self):
+        return self.height
+
 
-    def move_down(self):
-        if self._pos < self._max:
-            self.pos = self._pos + 1
-        self.move = 'down'
+class HScrollbar(Scrollbar):
+    def __init__(self):
+        Scrollbar.__init__(self)
+        self._default_size.update(20, 1)
+
+    def on_draw(self, ev):
+        ev.driver.pushcolor('normal')
+        ev.driver.putch(ev.x, ev.y, ev.driver.unigraph.LEFT_ARROW)
+        for i in range(1, self.width - 1):
+            ev.driver.putch(ev.x + i, ev.y, self.track_char)
+        ev.driver.putch(ev.x + 1 + self._thumb_pos, ev.y, self.thumb_char)
+        ev.driver.putch(ev.x + self.width - 1, ev.y, ev.driver.unigraph.RIGHT_ARROW)
+        ev.driver.popcolor()
 
-    def drag(self, wy):
-        newpos = int(round((wy - 1) / (self.height - 3) * self._max))
-        if newpos < 0:
-            newpos = 0
-        if newpos > self._max:
-            newpos = self._max
-        if self._pos != newpos:
-            self.pos = newpos
+    def on_mousedown(self, ev):
+        self._dragging = False
+        self._move = None
+        # arrow buttons
+        if ev.wx == 0 or ev.wx == self.width - 1:
+            if ev.wx == 0:
+                self.move_up()
+            else:
+                self.move_down()
+            self.add_timeout(self.scroll_delay, self._continuous_scroll)
+            return
+        # thumb bar
+        if ev.wx == 1 + self._thumb_pos:
+            self._dragging = True
+            return
 
-    def _continuous_scroll(self):
-        if self.move == 'up':
-            self.move_up()
-        if self.move == 'down':
-            self.move_down()
-        self.add_timeout(self.scroll_interval, self._continuous_scroll)
+    def on_mousemove(self, ev):
+        if self._dragging:
+            self.drag(ev.wx)
 
+    def on_mouseup(self, ev):
+        if self._dragging:
+            self.drag(ev.wx)
+            self._dragging = False
+            return
+        if self._move:
+            self.remove_timeout(self._continuous_scroll)
+            self._move = None
+            return
+
+    def _get_length(self):
+        return self.width
+
--- a/tuikit/scrollview.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/tuikit/scrollview.py	Fri Jan 18 22:36:50 2013 +0100
@@ -12,12 +12,13 @@
 
     """
 
-    def __init__(self, width=20, height=20):
-        Container.__init__(self, width, height)
+    def __init__(self):
+        Container.__init__(self)
+        self._default_size.update(20, 20)
 
-        self.vscroll = VScrollbar(height)
+        self.vscroll = VScrollbar()
         self.vscroll.add_handler('change', self._on_vscroll_change)
-        self.vscroll.allow_layout = False
+        self.vscroll.evade_layout = 2
         self.add(self.vscroll)
 
     def add(self, widget, **kwargs):
@@ -27,8 +28,8 @@
             widget.add_handler('spotmove', self._on_child_spotmove)
 
     def on_resize(self, ev):
-        self.vscroll.x = self.size.w - 1
-        self.vscroll.height = self.height
+        self.vscroll.move(x = self.size.w - 1)
+        self.vscroll.resize(h = self.height)
         self._update_vscroll_max()
 
     def _on_vscroll_change(self, ev):
--- a/tuikit/textedit.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/tuikit/textedit.py	Fri Jan 18 22:36:50 2013 +0100
@@ -1,26 +1,28 @@
 # -*- coding: utf-8 -*-
 
-from tuikit.container import Container
+from tuikit.layout import AnchorLayout
 from tuikit.editbox import EditBox
-from tuikit.scrollbar import VScrollbar
+from tuikit.scrollbar import VScrollbar, HScrollbar
+from tuikit.common import Borders
 
 
-class TextEdit(Container):
-    def __init__(self, width=20, height=20, text=''):
-        Container.__init__(self, width, height)
+class TextEdit(AnchorLayout):
+    def __init__(self, text=''):
+        AnchorLayout.__init__(self)
+        self._default_size.update(20, 20)
 
-        self.editbox = EditBox(width-2, height-2, text)
-        self.add(self.editbox)
-        self.editbox.x = 1
-        self.editbox.y = 1
+        self.editbox = EditBox(text)
         self.editbox.add_handler('scroll', self.on_editbox_scroll)
         self.editbox.add_handler('areasize', self.on_editbox_areasize)
+        self.add(self.editbox, halign='fill', valign='fill', margin=Borders(full=1))
 
-        self.vscroll = VScrollbar(height - 2)
-        self.add(self.vscroll)
-        self.vscroll.x = width - 1
-        self.vscroll.y = 1
+        self.vscroll = VScrollbar()
         self.vscroll.add_handler('change', self.on_vscroll_change)
+        self.add(self.vscroll, halign='right', valign='fill', margin=Borders(t=1, b=1))
+
+        self.hscroll = HScrollbar()
+        self.hscroll.add_handler('change', self.on_hscroll_change)
+        self.add(self.hscroll, halign='fill', valign='bottom', margin=Borders(l=1, r=1))
 
         self.on_editbox_areasize(None)
 
@@ -34,15 +36,18 @@
         ev.driver.frame(ev.x, ev.y, self.width, self.height)
 
     def on_editbox_scroll(self, ev):
-        self.vscroll.pos = self.editbox.yofs
+        self.vscroll.scroll_pos = self.editbox.yofs
 
     def on_editbox_areasize(self, ev):
         smax = len(self.editbox.lines) - self.editbox.height
         if smax < 0:
             smax = 0
-        self.vscroll.max = smax
+        self.vscroll.scroll_max = smax
 
     def on_vscroll_change(self, ev):
-        self.editbox.set_yofs(self.vscroll.pos)
+        self.editbox.set_yofs(self.vscroll.scroll_pos)
         self.editbox.redraw()
 
+    def on_hscroll_change(self, ev):
+        self.editbox.xofs = self.hscroll.scroll_pos
+        self.editbox.redraw()
--- a/tuikit/treeview.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/tuikit/treeview.py	Fri Jan 18 22:36:50 2013 +0100
@@ -175,8 +175,10 @@
 class TreeView(Widget):
     """Tree view displays data from tree model."""
 
-    def __init__(self, model=None, width=20, height=20):
-        Widget.__init__(self, width, height)
+    def __init__(self, model=None):
+        Widget.__init__(self)
+        self._default_size.update(20, 20)
+
         self.allow_focus = True
 
         # model
--- a/tuikit/widget.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/tuikit/widget.py	Fri Jan 18 22:36:50 2013 +0100
@@ -3,12 +3,14 @@
 from tuikit.events import Emitter, Event, DrawEvent, FocusEvent, KeyboardEvent, MouseEvent
 from tuikit.common import Coords, Size
 
+import logging
+
 
 class Widget(Emitter):
 
     '''Base class for all widgets.'''
 
-    def __init__(self, width = 10, height = 10):
+    def __init__(self):
         #: Widget name is used for logging etc. Not visible anywhere.
         self.name = None
 
@@ -18,28 +20,30 @@
         #: Top widget (same for every widget in one application).
         self._top = None
 
-        # Position inside parent widget. Modified by layout manager.
-        self.position = Coords()
+        #: Floating widget
+        self.floater = False
 
-        # Actual size. Modified by layout manager.
-        self.size = Size(width, height)
-
+        ### placing and size
+        # Position inside parent widget. Modified by layout manager.
+        self._pos = Coords()
+        # Actual size. Modified only by layout manager.
+        self._size = Size(10, 10)
+        self._size.add_handler('change', lambda ev: self.emit('resize'))
+        #: Default (natural) size of Widget.
+        self._default_size = Size(1, 1)
+        self._default_size.add_handler('change', lambda ev: self._sizereq.update(self._default_size))
+        #: Size request. Equals to default size, unless changed by user.
+        #: Size request will be fulfilled by layout manager when possible.
+        self._sizereq = Size(1, 1)
+        self._sizereq.add_handler('change', lambda ev: self.emit('sizereq'))
         #: Minimal size of widget. Under normal circumstances
         #: widget will never be sized smaller than this.
         #: Tuple (w, h). Both must be integers >= 1.
         self.sizemin = Size(1, 1)
-
         #: Maximum size of widget. Widget will never be sized bigger than this.
         #: Tuple (w, h). Integers >= 1 or None (meaning no maximum size or infinite).
         self.sizemax = Size(None, None)
 
-        #: Size request. This is default size of the widget. Will be fulfilled if possible.
-        #: Size(w, h). Integers >= 1 or None (meaning use minumal size).
-        self._sizereq = Size(10, 10)
-        self._sizereq.add_handler('change', lambda ev: self.emit('sizereq'))
-
-        #: When false, the widget is not considered in layout.
-        self.allow_layout = True
 
         #: Allow keyboard focus for this widget.
         self.allow_focus = False
@@ -75,41 +79,39 @@
             'focus', FocusEvent,
             'unfocus', FocusEvent)
 
+    ### placing and size
 
     @property
     def x(self):
-        return self.position.x
-
-    @x.setter
-    def x(self, value):
-        self.position.x = value
+        return self._pos.x
 
     @property
     def y(self):
-        return self.position.y
+        return self._pos.y
 
-    @y.setter
-    def y(self, value):
-        self.position.y = value
-
+    def move(self, x, y):
+        if self.floater:
+            self._pos.update(x, y)
+        if self.parent:
+            self.parent.move_child(self, x, y)
 
     @property
     def width(self):
-        return self.size.w
-
-    @width.setter
-    def width(self, value):
-        self.size.w = value
-        self.emit('resize')
+        return self._size.w
 
     @property
     def height(self):
-        return self.size.h
+        return self._size.h
+
+    def resize(self, w=None, h=None):
+        """Set size request.
 
-    @height.setter
-    def height(self, value):
-        self.size.h = value
-        self.emit('resize')
+        It's up to parent container if request will be fulfilled.
+
+        """
+        self._sizereq.update(w, h)
+        #if self.parent:
+         #   self.parent.emit('resize')
 
     @property
     def sizereq(self):
@@ -121,6 +123,15 @@
         """
         return self._sizereq
 
+    def on_sizereq(self, ev):
+        # floater is not managet by layout,
+        # always set its size to sizereq
+        if self.floater:
+            logging.getLogger('tuikit').info('xy')
+            self._size.update(self._sizereq)
+
+    ### misc
+
     @property
     def spot(self):
         """Spot of visibility.
@@ -220,10 +231,6 @@
 
     ###
 
-    def hint(self, key):
-        if key in self.hints:
-            return self.hints[key]
-
     def enclose(self, x, y):
         if self.hidden:
             return False
--- a/tuikit/window.py	Thu Jan 10 00:03:34 2013 +0100
+++ b/tuikit/window.py	Fri Jan 18 22:36:50 2013 +0100
@@ -2,45 +2,48 @@
 
 from tuikit.container import Container
 from tuikit.button import Button
-
+from tuikit.layout import AnchorLayout
+from tuikit.common import Borders
 
-class Window(Container):
+import logging
+
+class Window(AnchorLayout):
 
     '''Window widget.
 
     It represents part of screen with border, close button and contents.
     Window can be moved, resized or closed by user.'''
 
-    def __init__(self, width=40, height=10):
+    def __init__(self, inner_layout=AnchorLayout):
         '''Create window of requested size.'''
-        Container.__init__(self, width, height)
+        AnchorLayout.__init__(self)
+        self.sizemin.update(8, 3)
 
         #: Window title.
         self.title = ''
 
-        self._closebutton = True
-
         #: Allow user to resize window.
         self.resizable = True
-
+        self._resizing = False
         #: Allow user to move window.
         self.movable = True
-
-        self.resizing = False
-        self.moving = False
+        self._moving = False
 
         self.closebtn = Button('x')
-        self.closebtn.allow_layout = False
-        self.closebtn.x = self.width - 5
-        self.closebtn.width = 3
+        self.closebtn.sizereq.w = 3
         self.closebtn.add_handler('click', self.on_closebtn_click)
         self.closebtn.bg = 'controls'
         self.closebtn.bghi = 'controls-active'
-        self.add(self.closebtn)
+        Container.add(self, self.closebtn, halign='right', margin=Borders(r=1))
+        self._closebutton = True
 
-        self.borders = (1, 1, 1, 1)
         self.colorprefix = 'window:'
 
+        self._inner = inner_layout()
+        Container.add(self, self._inner, halign='fill', valign='fill', margin=Borders(1,1,1,1))
+
+    def add(self, widget, **kwargs):
+        self._inner.add(widget, **kwargs)
 
     @property
     def closebutton(self):
@@ -58,7 +61,7 @@
         ev.driver.frame(ev.x, ev.y, self.width, self.height)
 
         if self.resizable:
-            if self.resizing:
+            if self._resizing:
                 ev.driver.pushcolor('controls-active')
             else:
                 ev.driver.pushcolor('controls')
@@ -66,7 +69,17 @@
             ev.driver.popcolor()
 
         if self.title:
-            ev.driver.puts(ev.x + (self.width - len(self.title))//2, ev.y, self.title)
+            title = ' ' + self.title + ' '
+            left = max(1, (self.width - len(title))//2)
+            right = self.width - len(title) - left
+            maxlen = self.width - 5
+            if left > 1 and right < 4:
+                # move space from left to right to make space for close button
+                left -= 1
+            if len(title) > maxlen:
+                title = title[:maxlen]
+                title = title[:-1] + ' '
+            ev.driver.puts(ev.x + left, ev.y, title)
 
         ev.driver.fill(ev.x+1, ev.y+1, self.width-2, self.height-2)
         ev.driver.popcolor()
@@ -75,24 +88,23 @@
     def after_mousedown(self, ev):
         self.dragstart = (ev.wx, ev.wy)
         if self.resizable and ev.wx >= self.width - 1 and ev.wy >= self.height - 1:
-            self.resizing = True
+            self._resizing = True
         elif self.movable:
-            self.moving = True
+            self._moving = True
         self.origsize = (self.width, self.height)
 
         self.redraw(True)
 
 
     def after_mouseup(self, ev):
-        if self.resizing:
-            self.width = self.origsize[0] + ev.wx - self.dragstart[0]
-            self.height = self.origsize[1] + ev.wy - self.dragstart[1]
-            self.resizing = False
-            self.emit('resize')
-        elif self.moving:
-            self.x = ev.px - self.dragstart[0]
-            self.y = ev.py - self.dragstart[1]
-            self.moving = False
+        if self._resizing:
+            self.resize(self.origsize[0] + ev.wx - self.dragstart[0],
+                        self.origsize[1] + ev.wy - self.dragstart[1])
+            self._resizing = False
+        elif self._moving:
+            self.move(ev.px - self.dragstart[0],
+                      ev.py - self.dragstart[1])
+            self._moving = False
 
         self.redraw(True)
 
@@ -105,21 +117,16 @@
         #if x > self.parent.width-self.width:
         #    return
 
-        if self.resizing:
-            self.width = self.origsize[0] + ev.wx - self.dragstart[0]
-            self.height = self.origsize[1] + ev.wy - self.dragstart[1]
-            self.emit('resize')
-        elif self.moving:
-            self.x = ev.px - self.dragstart[0]
-            self.y = ev.py - self.dragstart[1]
+        if self._resizing:
+            self.resize(self.origsize[0] + ev.wx - self.dragstart[0],
+                        self.origsize[1] + ev.wy - self.dragstart[1])
+        elif self._moving:
+            self.move(ev.px - self.dragstart[0],
+                      ev.py - self.dragstart[1])
 
         self.redraw(True)
 
 
-    def on_resize(self, ev):
-        self.closebtn.x = self.width - 5
-
-
     def on_closebtn_click(self, ev):
         self.hide()