[billreminder] sync with upstream hamster graphics!



commit df7a33d719ac6d2917dfe435583992df72afac2b
Author: Toms Bauģis <toms baugis gmail com>
Date:   Tue Dec 7 21:24:45 2010 +0000

    sync with upstream hamster graphics!

 src/gui/maindialog.py          |    1 +
 src/gui/widgets/charting.py    |  842 ++++-------------
 src/gui/widgets/chartwidget.py |    9 +-
 src/gui/widgets/graphics.py    | 2067 ++++++++++++++++++++++++++++++++++------
 src/gui/widgets/pytweener.py   |  952 +++++++------------
 5 files changed, 2292 insertions(+), 1579 deletions(-)
---
diff --git a/src/gui/maindialog.py b/src/gui/maindialog.py
index 1d71aa9..0202fd1 100644
--- a/src/gui/maindialog.py
+++ b/src/gui/maindialog.py
@@ -86,6 +86,7 @@ class MainDialog:
 
         # Chart
         self.chart = ChartWidget()
+        self.chart.set_border_width(10)
         self.ui.get_object("chart_box").add(self.chart)
 
         # Restore position and size of window
diff --git a/src/gui/widgets/charting.py b/src/gui/widgets/charting.py
index cd8f14c..85e07f5 100644
--- a/src/gui/widgets/charting.py
+++ b/src/gui/widgets/charting.py
@@ -1,6 +1,6 @@
 # - coding: utf-8 -
 
-# Copyright (C) 2008 Toms Bauģis <toms.baugis at gmail.com>
+# Copyright (C) 2008-2010 Toms Bauģis <toms.baugis at gmail.com>
 
 # This file is part of Project Hamster.
 
@@ -17,709 +17,308 @@
 # You should have received a copy of the GNU General Public License
 # along with Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
 
-
-"""Small charting library that enables you to draw bar and
-horizontal bar charts. This library is not intended for scientific graphs.
-More like some visual clues to the user.
-
-The whole thing is a bit of minefield, but it can bring pretty decent results
-if you don't ask for much.
-
-For graph options see the Chart class and Chart.plot function
-
-Author: toms baugis gmail com
-Feel free to contribute - more info at Project Hamster web page:
-http://projecthamster.wordpress.com/
-
-"""
-
 import gtk, gobject
-import cairo, pango
-import copy
-import math
-from sys import maxint
+import pango
 import datetime as dt
 import time
-import colorsys
-import logging
-
 import graphics
+import locale
 
+def escape_pango(text):
+    if not text:
+        return text
 
-def size_list(set, target_set):
-    """turns set lenghts into target set - trim it, stretches it, but
-       keeps values for cases when lengths match
-    """
-    set = set[:min(len(set), len(target_set))] #shrink to target
-    set += target_set[len(set):] #grow to target
-
-    #nest
-    for i in range(len(set)):
-        if isinstance(set[i], list):
-            set[i] = size_list(set[i], target_set[i])
-    return set
-
-def get_limits(set, stack_subfactors = True):
-    # stack_subfactors indicates whether we should sum up nested lists
-    max_value, min_value = -maxint, maxint
-    for col in set:
-        if type(col) in [int, float]:
-            max_value = max(col, max_value)
-            min_value = min(col, min_value)
-        elif stack_subfactors:
-            max_value = max(sum(col), max_value)
-            min_value = min(sum(col), min_value)
-        else:
-            for row in col:
-                max_value = max(row, max_value)
-                min_value = max(row, min_value)
-
-    return min_value, max_value
-    
-
-class Bar(object):
-    def __init__(self, value, size = 0):
-        self.value = value
-        self.size = size
-    
-    def __repr__(self):
-        return str((self.value, self.size))
-        
-
-class Chart(graphics.Area):
-    """Chart constructor. Optional arguments:
-        self.max_bar_width     = pixels. Maximal width of bar. If not specified,
-                                 bars will stretch to fill whole area
-        self.legend_width      = pixels. Legend width will keep you graph
-                                 from floating around.
-        self.animate           = Should transitions be animated.
-                                 Defaults to TRUE
-        self.framerate         = Frame rate for animation. Defaults to 60
-
-        self.background        = Tripplet-tuple of background color in RGB
-        self.chart_background  = Tripplet-tuple of chart background color in RGB
-        self.bar_base_color    = Tripplet-tuple of bar color in RGB
-
-        self.show_scale        = Should we show scale values. See grid_stride!
-        self.grid_stride       = Step of grid. If expressed in normalized range
-                                 (0..1), will be treated as percentage.
-                                 Otherwise will be striding through maximal value.
-                                 Defaults to 0. Which is "don't draw"
-
-        self.values_on_bars    = Should values for each bar displayed on top of
-                                 it.
-        self.value_format      = Format string for values. Defaults to "%s"
-
-        self.show_stack_labels = If the labels of stack bar chart should be
-                                 displayed. Defaults to False
-        self.labels_at_end     = If stack bars are displayed, this allows to
-                                 show them at right end of graph.
-    """
-    __gsignals__ = {
-        "bar-clicked": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
-    }
-    def __init__(self, **args):
-        graphics.Area.__init__(self)
-
-        # options
-        self.max_bar_width     = args.get("max_bar_width", 500)
-        self.legend_width      = args.get("legend_width", 0)
-        self.animation           = args.get("animate", True)
-
-        self.background        = args.get("background", None)
-        self.chart_background  = args.get("chart_background", None)
-        self.bar_base_color    = args.get("bar_base_color", None)
-
-        self.grid_stride       = args.get("grid_stride", None)
-        self.values_on_bars    = args.get("values_on_bars", False)
-        self.value_format      = args.get("value_format", "%s")
-        self.show_scale        = args.get("show_scale", False)
-
-        self.show_stack_labels = args.get("show_stack_labels", False)
-        self.labels_at_end     = args.get("labels_at_end", False)
-        self.framerate         = args.get("framerate", 60)
-
-        self.interactive       = args.get("interactive", False) # if the bars are clickable
-
-        # other stuff
-        self.bars = []
-        self.keys = []
-        self.data = None
-        self.stack_keys = []
-        
-        self.key_colors = {} # key:color dictionary. if key's missing will grab basecolor
-        self.stack_key_colors = {} # key:color dictionary. if key's missing will grab basecolor
-        
-
-        # use these to mark area where the "real" drawing is going on
-        self.graph_x, self.graph_y = 0, 0
-        self.graph_width, self.graph_height = None, None
-        
-        self.mouse_bar = None
-        if self.interactive:
-            self.connect("mouse-over", self.on_mouse_over)
-            self.connect("button-release", self.on_clicked)
-            
-        self.bars_selected = []
-        
-    
-    def on_mouse_over(self, area, region):
-        if region:
-            self.mouse_bar = int(region[0])
-        else:
-            self.mouse_bar = None
-            
-        self.redraw_canvas()
-        
-    def on_clicked(self, area, bar):
-        self.emit("bar-clicked", self.mouse_bar)
-    
-    def select_bar(self, index):
-        pass
-
-    def get_bar_color(self, index):
-        # returns color darkened by it's index
-        # the approach reduces contrast by each step
-        base_color = self.bar_base_color or (220, 220, 220)
-        
-        base_hls = colorsys.rgb_to_hls(*base_color)
-        
-        step = (base_hls[1] - 30) / 10 #will go from base down to 20 and max 22 steps
-        
-        return colorsys.hls_to_rgb(base_hls[0],
-                                   base_hls[1] - step * index,
-                                   base_hls[2])
-        
-
-    def draw_bar(self, x, y, w, h, color = None):
-        """ draws a simple bar"""
-        base_color = color or self.bar_base_color or (220, 220, 220)
-        self.fill_area(x, y, w, h, base_color)
-
-
-    def plot(self, keys, data, stack_keys = None):
-        """Draw chart with given data"""
-        self.keys, self.data, self.stack_keys = keys, data, stack_keys
-
-        self.show()
-
-        if not data: #if there is no data, just draw blank
-            self.redraw_canvas()
-            return
-
-
-        min, self.max_value = get_limits(data)
-
-        self._update_targets()
+    text = text.replace ("&", "&amp;")
+    text = text.replace("<", "&lt;")
+    text = text.replace(">", "&gt;")
+    return text
 
-        if not self.animation:
-            self.tweener.finish()
 
-        self.redraw_canvas()
+class Bar(graphics.Sprite):
+    def __init__(self, key, value, normalized, label_color):
+        graphics.Sprite.__init__(self, cache_as_bitmap=True)
+        self.key, self.value, self.normalized = key, value, normalized
 
+        self.height = 0
+        self.width = 20
+        self.interactive = True
+        self.fill = None
 
-    def on_expose(self):
-        # fill whole area 
-        if self.background:
-            self.fill_area(0, 0, self.width, self.height, self.background)
-        
+        self.label = graphics.Label(value, size=8, color=label_color)
+        self.label_background = graphics.Rectangle(self.label.width + 4, self.label.height + 4, 4, visible=False)
+        self.add_child(self.label_background)
+        self.add_child(self.label)
+        self.connect("on-render", self.on_render)
 
-    def _update_targets(self):
-        # calculates new factors and then updates existing set
-        max_value = float(self.max_value) or 1 # avoid division by zero
-        
-        self.bars = size_list(self.bars, self.data)
+    def on_render(self, sprite):
+        # invisible rectangle for the mouse, covering whole area
+        self.graphics.rectangle(0, 0, self.width, self.height)
+        self.graphics.fill("#000", 0)
 
-        #need function to go recursive
-        def retarget(bars, new_values):
-            for i in range(len(new_values)):
-                if isinstance(new_values[i], list):
-                    bars[i] = retarget(bars[i], new_values[i])
-                else:
-                    if isinstance(bars[i], Bar) == False:
-                        bars[i] = Bar(new_values[i], 0)
-                    else:
-                        bars[i].value = new_values[i]
-                        for tween in self.tweener.getTweensAffectingObject(bars[i]):
-                            self.tweener.removeTween(tween)
+        size = round(self.width * self.normalized)
 
-                    self.tweener.addTween(bars[i], size = bars[i].value / float(max_value))
-            return bars
-    
-        retarget(self.bars, self.data)
+        self.graphics.rectangle(0, 0, size, self.height, 3)
+        self.graphics.rectangle(0, 0, min(size, 3), self.height)
+        self.graphics.fill(self.fill)
 
+        self.label.y = (self.height - self.label.height) / 2
 
-    def longest_label(self, labels):
-        """returns width of the longest label"""
-        max_extent = 0
-        for label in labels:
-            self.layout.set_text(label)
-            label_w, label_h = self.layout.get_pixel_size()
-            max_extent = max(label_w + 5, max_extent)
-        
-        return max_extent
-    
-    def draw(self):
-        logging.error("OMG OMG, not implemented!!!")
+        horiz_offset = min(10, self.label.y * 2)
 
+        if self.label.width < size - horiz_offset * 2:
+            #if it fits in the bar
+            self.label.x = size - self.label.width - horiz_offset
+        else:
+            self.label.x = size + 3
 
-class BarChart(Chart):
-    def on_expose(self):
-        Chart.on_expose(self)
+        self.label_background.x = self.label.x - 2
+        self.label_background.y = self.label.y - 2
 
-        if not self.data:
-            return
 
-        context = self.context        
-        context.set_line_width(1)
+class Chart(graphics.Scene):
+    __gsignals__ = {
+        "bar-clicked": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
+    }
 
+    def __init__(self, max_bar_width = 20, legend_width = 70, value_format = "%.2f", interactive = True):
+        graphics.Scene.__init__(self)
 
-        # determine graph dimensions
-        if self.show_stack_labels:
-            legend_width = self.legend_width or self.longest_label(self.keys)
-        elif self.show_scale:
-            if self.grid_stride < 1:
-                grid_stride = int(self.max_value * self.grid_stride)
-            else:
-                grid_stride = int(self.grid_stride)
-            
-            scale_labels = [self.value_format % i
-                  for i in range(grid_stride, int(self.max_value), grid_stride)]
-            self.legend_width = legend_width = self.legend_width or self.longest_label(scale_labels)
-        else:
-            legend_width = self.legend_width
+        self.selected_keys = [] # keys of selected bars
 
-        if self.stack_keys and self.labels_at_end:
-            self.graph_x = 0
-            self.graph_width = self.width - legend_width
-        else:
-            self.graph_x = legend_width + 8 # give some space to scale labels
-            self.graph_width = self.width - self.graph_x - 10
+        self.bars = []
+        self.labels = []
+        self.data = None
 
-        self.graph_y = 0
-        self.graph_height = self.height - 15
+        self.key_colors = {}
 
-        if self.chart_background:
-            self.fill_area(self.graph_x, self.graph_y,
-                           self.graph_width, self.graph_height,
-                           self.chart_background)
+        self.max_width = max_bar_width
+        self.legend_width = legend_width
+        self.value_format = value_format
+        self.graph_interactive = interactive
 
-        self.context.stroke()
+        self.plot_area = graphics.Sprite(interactive = False)
+        self.add_child(self.plot_area)
 
-        # bars and keys
-        max_bar_size = self.graph_height
-        #make sure bars don't hit the ceiling
-        if self.animate or self.before_drag_animate:
-            max_bar_size = self.graph_height - 10
+        self.bar_color, self.label_color = None, None
 
+        self.connect("on-enter-frame", self.on_enter_frame)
 
-        prev_label_end = None
-        self.layout.set_width(-1)
+        if self.graph_interactive:
+            self.connect("on-mouse-over", self.on_mouse_over)
+            self.connect("on-mouse-out", self.on_mouse_out)
+            self.connect("on-click", self.on_click)
 
-        exes = {}
-        x = 0
-        bar_width = min(self.graph_width / float(len(self.keys)), self.max_bar_width)
-        for i, key in enumerate(self.keys):
-            exes[key] = (x + self.graph_x, round(bar_width - 1))
-            
-            x = x + round(bar_width)
-            bar_width = min(self.max_bar_width,
-                            (self.graph_width - x) / float(max(1, len(self.keys) - i - 1)))
+    def find_colors(self):
+        bg_color = self.get_style().bg[gtk.STATE_NORMAL].to_string()
+        self.bar_color = self.colors.contrast(bg_color, 30)
 
+        # now for the text - we want reduced contrast for relaxed visuals
+        fg_color = self.get_style().fg[gtk.STATE_NORMAL].to_string()
+        self.label_color = self.colors.contrast(fg_color,  80)
 
-        for key, bar, data in zip(self.keys, self.bars, self.data):
-            self.set_color(graphics.Colors.aluminium[5]);
-            self.layout.set_text(key)
-            label_w, label_h = self.layout.get_pixel_size()
 
-            intended_x = exes[key][0] + (exes[key][1] - label_w) / 2
-            
-            if not prev_label_end or intended_x > prev_label_end:
-                self.context.move_to(intended_x, self.graph_height + 4)
-                context.show_layout(self.layout)
-            
-                prev_label_end = intended_x + label_w + 3
-                
-
-            bar_start = 0
-            base_color = self.bar_base_color or (220, 220, 220)
-            
-            if self.stack_keys:
-                remaining_fractions, remaining_pixels = 1, max_bar_size
-                
-                for j, stack_bar in enumerate(bar):
-                    if stack_bar.size > 0:
-                        bar_size = round(remaining_pixels * (stack_bar.size / remaining_fractions))
-                        remaining_fractions -= stack_bar.size
-                        remaining_pixels -= bar_size
-
-                        bar_start += bar_size
-                        
-                        last_color = self.stack_key_colors.get(self.stack_keys[j]) or self.get_bar_color(j)
-                        self.draw_bar(exes[key][0],
-                                      self.graph_height - bar_start,
-                                      exes[key][1],
-                                      bar_size,
-                                      last_color)
-            else:
-                bar_size = round(max_bar_size * bar.size)
-                bar_start = bar_size
+    def on_mouse_over(self, scene, bar):
+        if bar.key not in self.selected_keys:
+            bar.fill = self.get_style().base[gtk.STATE_PRELIGHT].to_string()
 
-                last_color = self.key_colors.get(key) or base_color
-                self.draw_bar(exes[key][0],
-                              self.graph_y + self.graph_height - bar_size,
-                              exes[key][1],
-                              bar_size,
-                              last_color)
+    def on_mouse_out(self, scene, bar):
+        if bar.key not in self.selected_keys:
+            bar.fill = self.bar_color
 
+    def on_click(self, scene, event, clicked_bar):
+        if not clicked_bar: return
+        self.emit("bar-clicked", clicked_bar.key)
 
-            if self.values_on_bars:  # it is either stack labels or values at the end for now
-                if self.stack_keys:
-                    total_value = sum(data[i])
-                else:
-                    total_value = data[i]
-                
-                self.layout.set_width(-1)
-                self.layout.set_text(self.value_format % total_value)
-                label_w, label_h = self.layout.get_pixel_size()
-    
-
-                if bar_start > label_h + 2:
-                    label_y = self.graph_y + self.graph_height - bar_start + 5
-                else:
-                    label_y = self.graph_y + self.graph_height - bar_start - label_h + 5
-                
-                context.move_to(self.exes[key][0] + (self.exes[key][1] - label_w) / 2.0,
-                                label_y)
-
-                # we are in the bar so make sure that the font color is distinguishable
-                if colorsys.rgb_to_hls(*graphics.Colors.rgb(last_color))[1] < 150:
-                    self.set_color(graphics.Colors.almost_white)
-                else:
-                    self.set_color(graphics.Colors.aluminium[5])        
+    def plot(self, keys, data):
+        self.data = data
 
-                context.show_layout(self.layout)
+        bars = dict([(bar.key, bar.normalized) for bar in self.bars])
 
+        max_val = float(max(data or [0]))
 
-        #white grid and scale values
-        self.layout.set_width(-1)
-        if self.grid_stride and self.max_value:
-            # if grid stride is less than 1 then we consider it to be percentage
-            if self.grid_stride < 1:
-                grid_stride = int(self.max_value * self.grid_stride)
-            else:
-                grid_stride = int(self.grid_stride)
-            
-            context.set_line_width(1)
-            for i in range(grid_stride, int(self.max_value), grid_stride):
-                y = round(max_bar_size * (i / self.max_value)) + 0.5
-
-                if self.show_scale:
-                    self.layout.set_text(self.value_format % i)
-                    label_w, label_h = self.layout.get_pixel_size()
-                    context.move_to(legend_width - label_w - 8,
-                                    y - label_h / 2)
-                    self.set_color(graphics.Colors.aluminium[4])
-                    context.show_layout(self.layout)
-
-                self.set_color("#ffffff")
-                self.context.move_to(legend_width, y)
-                self.context.line_to(self.width, y)
-
-
-        #stack keys
-        if self.show_stack_labels:
-            #put series keys
-            self.set_color(graphics.Colors.aluminium[5]);
-            
-            y = self.graph_height
-            label_y = None
-
-            # if labels are at end, then we need show them for the last bar! 
-            if self.labels_at_end:
-                factors = self.bars[-1]
+        new_bars, new_labels = [], []
+        for key, value in zip(keys, data):
+            if max_val:
+                normalized = value / max_val
             else:
-                factors = self.bars[0]
-            
-            if isinstance(factors, Bar):
-                factors = [factors]
-
-            self.layout.set_ellipsize(pango.ELLIPSIZE_END)
-            self.layout.set_width(self.graph_x * pango.SCALE)
-            if self.labels_at_end:
-                self.layout.set_alignment(pango.ALIGN_LEFT)
-            else:
-                self.layout.set_alignment(pango.ALIGN_RIGHT)
-    
-            for j in range(len(factors)):
-                factor = factors[j].size
-                bar_size = factor * max_bar_size
-                
-                if round(bar_size) > 0 and self.stack_keys:
-                    label = "%s" % self.stack_keys[j]
-
-                    
-                    self.layout.set_text(label)
-                    label_w, label_h = self.layout.get_pixel_size()
-                    
-                    y -= bar_size
-                    intended_position = round(y + (bar_size - label_h) / 2)
-                    
-                    if label_y:
-                        label_y = min(intended_position, label_y - label_h)
-                    else:
-                        label_y = intended_position
-                    
-                    if self.labels_at_end:
-                        label_x = self.graph_x + self.graph_width 
-                        line_x1 = self.graph_x + self.graph_width - 1
-                        line_x2 = self.graph_x + self.graph_width - 6
-                    else:
-                        label_x = -8
-                        line_x1 = self.graph_x - 6
-                        line_x2 = self.graph_x
+                normalized = 0
+            bar = Bar(key, locale.format(self.value_format, value), normalized, self.label_color)
+            bar.interactive = self.graph_interactive
 
+            if key in bars:
+                bar.normalized = bars[key]
+                self.tweener.add_tween(bar, normalized=normalized)
+            new_bars.append(bar)
 
-                    context.move_to(label_x, label_y)
-                    context.show_layout(self.layout)
+            label = graphics.Label(escape_pango(key), size = 8, alignment = pango.ALIGN_RIGHT)
+            new_labels.append(label)
 
-                    if label_y != intended_position:
-                        context.move_to(line_x1, label_y + label_h / 2)
-                        context.line_to(line_x2, round(y + bar_size / 2))
 
-        context.stroke()
+        self.plot_area.remove_child(*self.bars)
+        self.remove_child(*self.labels)
 
+        self.bars, self.labels = new_bars, new_labels
+        self.add_child(*self.labels)
+        self.plot_area.add_child(*self.bars)
 
-class HorizontalBarChart(Chart):
-    def on_expose(self):
-        Chart.on_expose(self)
+        self.show()
+        self.redraw()
 
-        if not self.data:
-            return
 
-        context = self.context
-        rowcount, keys = len(self.keys), self.keys
-        
-        # push graph to the right, so it doesn't overlap
-        legend_width = self.legend_width or self.longest_label(keys)
-        
-        self.graph_x = legend_width
-        self.graph_x += 8 #add another 8 pixes of padding
-        
-        self.graph_width = self.width - self.graph_x
-        self.graph_y, self.graph_height = 0, self.height
+    def on_enter_frame(self, scene, context):
+        # adjust sizes and positions on redraw
 
+        legend_width = self.legend_width
+        if legend_width < 1: # allow fractions
+            legend_width = int(self.width * legend_width)
 
-        if self.chart_background:
-            self.fill_area(self.graph_x, self.graph_y, self.graph_width, self.graph_height, self.chart_background)
+        self.find_colors()
 
-    
-        if not self.data:  # go home if we have nothing
-            return
+        self.plot_area.y = 0
+        self.plot_area.height = self.height - self.plot_area.y
+        self.plot_area.x = legend_width + 8
+        self.plot_area.width = self.width - self.plot_area.x
 
-        positions = {}
         y = 0
-        bar_width = min(self.graph_height / float(len(self.keys)), self.max_bar_width)
-        for i, key in enumerate(self.keys):
-            positions[key] = (y + self.graph_y, round(bar_width - 1))
-            
-            y = y + round(bar_width)
-            bar_width = min(self.max_bar_width,
-                            (self.graph_height - y) / float(max(1, len(self.keys) - i - 1)))
-
-
-        max_bar_size = self.graph_width - 15
-
-        self.layout.set_alignment(pango.ALIGN_RIGHT)
-        self.layout.set_ellipsize(pango.ELLIPSIZE_END)
-        
-
-        
-        context.set_line_width(1)
-
-        # bars and labels
-        self.layout.set_width(legend_width * pango.SCALE)
-        
-
-        for i, label in enumerate(keys):
-            if self.interactive:
-                self.register_mouse_region(0,
-                                           positions[label][0],
-                                           self.width,
-                                           positions[label][0] + positions[label][1],
-                                           str(i))
-
-            self.layout.set_width(legend_width * pango.SCALE)
-            self.layout.set_text(label)
-            label_w, label_h = self.layout.get_pixel_size()
-            
-            self.set_color(graphics.Colors.aluminium[5])        
-            context.move_to(0, positions[label][0] + (positions[label][1] - label_h) / 2)
-            context.show_layout(self.layout)
-
-            base_color = self.bar_base_color or (220, 220, 220)
-
-            last_color = (255,255,255)
-
-            if self.stack_keys:
-                bar_start = 0
-
-                remaining_fractions, remaining_pixels = 1, max_bar_size
-                
-                for j, stack_bar in enumerate(self.bars[i]):
-                    if stack_bar.size > 0:
-                        bar_size = round(remaining_pixels * (stack_bar.size / remaining_fractions))
-                        remaining_fractions -= stack_bar.size
-                        remaining_pixels -= bar_size
-
-                        last_color = self.stack_key_colors.get(self.stack_keys[j]) or self.get_bar_color(j)
-                        self.draw_bar(self.graph_x + bar_start,
-                                      positions[label][0],
-                                      bar_size,
-                                      positions[label][1],
-                                      last_color)
-                        bar_start += bar_size
-            else:
-                bar_size = round(max_bar_size * self.bars[i].size)
-                bar_start = bar_size
-
-                if i in self.bars_selected:
-                    last_color = self.get_style().bg[gtk.STATE_SELECTED].to_string()
-                elif i == self.mouse_bar:
-                    last_color = self.get_style().bg[gtk.STATE_PRELIGHT].to_string()
+        for i, (label, bar) in enumerate(zip(self.labels, self.bars)):
+            bar_width = min(round((self.plot_area.height - y) / (len(self.bars) - i)), self.max_width)
+            bar.y = y
+            bar.height = bar_width
+            bar.width = self.plot_area.width
+
+            if bar.key in self.selected_keys:
+                bar.fill = self.key_colors.get(bar.key, self.get_style().bg[gtk.STATE_SELECTED].to_string())
+
+                if bar.normalized == 0:
+                    bar.label.color = self.get_style().fg[gtk.STATE_SELECTED].to_string()
+                    bar.label_background.fill = self.get_style().bg[gtk.STATE_SELECTED].to_string()
+                    bar.label_background.visible = True
                 else:
-                    last_color = self.key_colors.get(self.keys[i]) or base_color
+                    bar.label_background.visible = False
+                    if bar.label.x < round(bar.width * bar.normalized):
+                        bar.label.color = self.get_style().fg[gtk.STATE_SELECTED].to_string()
+                    else:
+                        bar.label.color = self.label_color
 
-                self.draw_bar(self.graph_x,
-                              positions[label][0],
-                              bar_size,
-                              positions[label][1],
-                              last_color)
+            if not bar.fill:
+                bar.fill = bar.fill or self.key_colors.get(bar.key, self.bar_color)
 
-            # values on bars
-            if self.stack_keys:
-                total_value = sum(self.data[i])
-            else:
-                total_value = self.data[i]
-            
-            self.layout.set_width(-1)
-            self.layout.set_text(self.value_format % total_value)
-            label_w, label_h = self.layout.get_pixel_size()
+                bar.label.color = self.label_color
+                bar.label_background.fill = None
 
-            vertical_padding = max((positions[label][1] - label_h) / 2.0, 1)
-            if  bar_start - vertical_padding < label_w:
-                label_x = self.graph_x + bar_start + vertical_padding
-                self.set_color(graphics.Colors.aluminium[5])        
-            else:
-                if i in self.bars_selected:
-                    self.set_color(self.get_style().fg[gtk.STATE_SELECTED].to_string())
-                else:
-                    # we are in the bar so make sure that the font color is distinguishable
-                    if colorsys.rgb_to_hls(*graphics.Colors.rgb(last_color))[1] < 150:
-                        self.set_color(graphics.Colors.almost_white)
-                    else:
-                        self.set_color(graphics.Colors.aluminium[5])        
-                    
-                label_x = self.graph_x + bar_start - label_w - vertical_padding
-            
-            context.move_to(label_x, positions[label][0] + (positions[label][1] - label_h) / 2.0)
-            context.show_layout(self.layout)
+            label.y = y + (bar_width - label.height) / 2 + self.plot_area.y
 
-        context.stroke()
+            label.width = legend_width
+            if not label.color:
+                label.color = self.label_color
+
+            y += bar_width + 1
 
 
 
 
-class HorizontalDayChart(Chart):
+class HorizontalDayChart(graphics.Scene):
     """Pretty much a horizontal bar chart, except for values it expects tuple
     of start and end time, and the whole thing hangs in air"""
-    def __init__(self, *args, **kwargs):
-        Chart.__init__(self, *args, **kwargs)
+    def __init__(self, max_bar_width, legend_width):
+        graphics.Scene.__init__(self)
+        self.max_bar_width = max_bar_width
+        self.legend_width = legend_width
         self.start_time, self.end_time = None, None
-    
+        self.connect("on-enter-frame", self.on_enter_frame)
+
     def plot_day(self, keys, data, start_time = None, end_time = None):
         self.keys, self.data = keys, data
         self.start_time, self.end_time = start_time, end_time
         self.show()
-        self.redraw_canvas()
-    
-    def on_expose(self):
-        context = self.context
-        
-        Chart.on_expose(self)
+        self.redraw()
+
+    def on_enter_frame(self, scene, context):
+        g = graphics.Graphics(context)
+
         rowcount, keys = len(self.keys), self.keys
-        
+
         start_hour = 0
         if self.start_time:
             start_hour = self.start_time
-        end_hour = 24 * 60        
+        end_hour = 24 * 60
         if self.end_time:
             end_hour = self.end_time
-        
-        
+
+
         # push graph to the right, so it doesn't overlap
         legend_width = self.legend_width or self.longest_label(keys)
 
         self.graph_x = legend_width
         self.graph_x += 8 #add another 8 pixes of padding
-        
+
         self.graph_width = self.width - self.graph_x
-        
+
+        # TODO - should handle the layout business in graphics
+        self.layout = context.create_layout()
+        default_font = pango.FontDescription(self.get_style().font_desc.to_string())
+        default_font.set_size(8 * pango.SCALE)
+        self.layout.set_font_description(default_font)
+
+
         #on the botttom leave some space for label
         self.layout.set_text("1234567890:")
         label_w, label_h = self.layout.get_pixel_size()
-        
-        self.graph_y, self.graph_height = 0, self.height - label_h - 4
 
-
-        if self.chart_background:
-            self.fill_area(self.graph_x, self.graph_y, self.graph_width, self.graph_height, self.chart_background)
+        self.graph_y, self.graph_height = 0, self.height - label_h - 4
 
         if not self.data:  #if we have nothing, let's go home
             return
 
-        
+
         positions = {}
         y = 0
         bar_width = min(self.graph_height / float(len(self.keys)), self.max_bar_width)
         for i, key in enumerate(self.keys):
             positions[key] = (y + self.graph_y, round(bar_width - 1))
-            
+
             y = y + round(bar_width)
             bar_width = min(self.max_bar_width,
                             (self.graph_height - y) / float(max(1, len(self.keys) - i - 1)))
 
 
-        
+
         max_bar_size = self.graph_width - 15
 
+
+        # now for the text - we want reduced contrast for relaxed visuals
+        fg_color = self.get_style().fg[gtk.STATE_NORMAL].to_string()
+        label_color = self.colors.contrast(fg_color,  80)
+
         self.layout.set_alignment(pango.ALIGN_RIGHT)
         self.layout.set_ellipsize(pango.ELLIPSIZE_END)
-        
+
         # bars and labels
         self.layout.set_width(legend_width * pango.SCALE)
 
         factor = max_bar_size / float(end_hour - start_hour)
 
+        # determine bar color
+        bg_color = self.get_style().bg[gtk.STATE_NORMAL].to_string()
+        base_color = self.colors.contrast(bg_color,  30)
+
         for i, label in enumerate(keys):
-            self.set_color(graphics.Colors.aluminium[5])        
-            
+            g.set_color(label_color)
+
             self.layout.set_text(label)
             label_w, label_h = self.layout.get_pixel_size()
 
             context.move_to(0, positions[label][0] + (positions[label][1] - label_h) / 2)
             context.show_layout(self.layout)
 
-            base_color = self.bar_base_color or [220, 220, 220]
-
             if isinstance(self.data[i], list) == False:
                 self.data[i] = [self.data[i]]
-            
+
             for row in self.data[i]:
                 bar_x = round((row[0]- start_hour) * factor)
                 bar_size = round((row[1] - start_hour) * factor - bar_x)
-                
-                self.draw_bar(round(self.graph_x + bar_x),
+
+                g.fill_area(round(self.graph_x + bar_x),
                               positions[label][0],
                               bar_size,
                               positions[label][1],
@@ -732,9 +331,13 @@ class HorizontalDayChart(Chart):
 
         pace = ((end_hour - start_hour) / 3) / 60 * 60
         last_position = positions[keys[-1]]
+
+
+        grid_color = self.get_style().bg[gtk.STATE_NORMAL].to_string()
+
         for i in range(start_hour + 60, end_hour, pace):
             x = round((i - start_hour) * factor)
-            
+
             minutes = i % (24 * 60)
 
             self.layout.set_markup(dt.time(minutes / 60, minutes % 60).strftime("%H<small><sup>%M</sup></small>"))
@@ -742,69 +345,14 @@ class HorizontalDayChart(Chart):
 
             context.move_to(self.graph_x + x - label_w / 2,
                             last_position[0] + last_position[1] + 4)
-            self.set_color(graphics.Colors.aluminium[4])
+            g.set_color(label_color)
             context.show_layout(self.layout)
 
-            
-            self.set_color((255, 255, 255))
-            self.context.move_to(round(self.graph_x + x) + 0.5, self.graph_y)
-            self.context.line_to(round(self.graph_x + x) + 0.5,
-                                 last_position[0] + last_position[1])
 
-                
-        context.stroke()
+            g.set_color(grid_color)
+            g.move_to(round(self.graph_x + x) + 0.5, self.graph_y)
+            g.line_to(round(self.graph_x + x) + 0.5,
+                                 last_position[0] + last_position[1])
 
 
-""" sample usage """
-class BasicWindow:
-    def __init__(self):
-        window = gtk.Window(gtk.WINDOW_TOPLEVEL)
-        window.set_title("Hamster Charting")
-        window.set_size_request(400, 300)
-        window.connect("delete_event", lambda *args: gtk.main_quit())
-    
-        self.stacked = BarChart(background = "#fafafa",
-                                bar_base_color = (220, 220, 220),
-                                legend_width = 20,
-                                show_stack_labels = True)
-        
-        box = gtk.VBox()
-        box.pack_start(self.stacked)
-
-
-        self.series = ["One", "Two", "Three", "Four", "Five", "Six", "Seven"]
-        self.stacks = ["x", "y", "z", "a", "b", "c", "d"]
-        self.stack_colors = dict([(stack, None) for stack in self.stacks])
-
-        import random
-        self.data = [[random.randint(0, 10) for j in range(len(self.stacks))] for i in range(len(self.series))]
-        
-        color_buttons = gtk.HBox()
-        color_buttons.set_spacing(4)
-
-        for stack in self.stacks:
-            button = gtk.ColorButton()
-            button.connect("color-set", self.on_color_set, stack)
-            color_buttons.pack_start(button)
-
-        box.pack_start(color_buttons, False)
-        
-        window.add(box)
-        window.show_all()
-
-        self.plot()
-        
-        
-    def plot(self):
-        self.stacked.stack_key_colors = self.stack_colors
-        self.stacked.plot(self.series, self.data, self.stacks)
-
-
-    def on_color_set(self, button, stack_idx):
-        self.stack_colors[stack_idx] = button.get_color().to_string()
-        self.plot()
-
-
-if __name__ == "__main__":
-   example = BasicWindow()
-   gtk.main()
+        context.stroke()
diff --git a/src/gui/widgets/chartwidget.py b/src/gui/widgets/chartwidget.py
index 98e70e0..014a28f 100644
--- a/src/gui/widgets/chartwidget.py
+++ b/src/gui/widgets/chartwidget.py
@@ -4,21 +4,18 @@ import pygtk
 pygtk.require('2.0')
 import gtk
 
-from charting import HorizontalBarChart
+from charting import Chart
 
 class ChartWidget(gtk.EventBox):
-   background = (0.975, 0.975, 0.975)
    x_offset = 90 # align all graphs to the left edge
 
    def __init__(self):
        gtk.EventBox.__init__(self)
 
-       self.chart = HorizontalBarChart(
-           background = self.background,
-           bar_base_color = (238,221,221),
+       self.chart = Chart(
            legend_width = self.x_offset,
            max_bar_width = 35,
-           values_on_bars = True
+           interactive = False
        )
 
        self.add(self.chart)
diff --git a/src/gui/widgets/graphics.py b/src/gui/widgets/graphics.py
index 6897e04..2fe5c10 100644
--- a/src/gui/widgets/graphics.py
+++ b/src/gui/widgets/graphics.py
@@ -1,344 +1,1823 @@
 # - coding: utf-8 -
 
-# Copyright (C) 2008-2009 Toms Bauģis <toms.baugis at gmail.com>
+# Copyright (C) 2008-2010 Toms Bauģis <toms.baugis at gmail.com>
+# Dual licensed under the MIT or GPL Version 2 licenses.
+# See http://github.com/tbaugis/hamster_experiments/blob/master/README.textile
 
-# This file is part of Project Hamster.
-
-# Project Hamster is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# Project Hamster is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-
-# You should have received a copy of the GNU General Public License
-# along with Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
 import math
-import time, datetime as dt
+import datetime as dt
 import gtk, gobject
 
 import pango, cairo
+import re
+
+try:
+    import pytweener
+except: # we can also live without tweener. Scene.animate will not work
+    pytweener = None
+
+import colorsys
+from collections import deque
+
+if cairo.version in ('1.8.2', '1.8.4'):
+    # in these two cairo versions the matrix multiplication was flipped
+    # http://bugs.freedesktop.org/show_bug.cgi?id=19221
+    def cairo_matrix_multiply(matrix1, matrix2):
+        return matrix2 * matrix1
+else:
+    def cairo_matrix_multiply(matrix1, matrix2):
+        return matrix1 * matrix2
 
-import pytweener
-from pytweener import Easing
 
 class Colors(object):
-    aluminium = [(238, 238, 236), (211, 215, 207), (186, 189, 182),
-                 (136, 138, 133), (85, 87, 83), (46, 52, 54)]
-    almost_white = (250, 250, 250)
+    hex_color_normal = re.compile("#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})")
+    hex_color_short = re.compile("#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])")
+    hex_color_long = re.compile("#([a-fA-F0-9]{4})([a-fA-F0-9]{4})([a-fA-F0-9]{4})")
+
+    def parse(self, color):
+        assert color is not None
 
-    @staticmethod
-    def color(color):
         #parse color into rgb values
-        if isinstance(color, str) or isinstance(color, unicode):
-            color = gtk.gdk.Color(color)
+        if isinstance(color, basestring):
+            match = self.hex_color_long.match(color)
+            if match:
+                color = [int(color, 16) / 65535.0 for color in match.groups()]
+            else:
+                match = self.hex_color_normal.match(color)
+                if match:
+                    color = [int(color, 16) / 255.0 for color in match.groups()]
+                else:
+                    match = self.hex_color_short.match(color)
+                    color = [int(color + color, 16) / 255.0 for color in match.groups()]
+
+        elif isinstance(color, gtk.gdk.Color):
+            color = [color.red / 65535.0,
+                     color.green / 65535.0,
+                     color.blue / 65535.0]
 
-        if isinstance(color, gtk.gdk.Color):
-            color = [color.red / 65535.0, color.green / 65535.0, color.blue / 65535.0]
         else:
             # otherwise we assume we have color components in 0..255 range
             if color[0] > 1 or color[1] > 1 or color[2] > 1:
                 color = [c / 255.0 for c in color]
 
         return color
-    
+
+    def rgb(self, color):
+        return [c * 255 for c in self.parse(color)]
+
+    def gdk(self, color):
+        c = self.parse(color)
+        return gtk.gdk.Color(int(c[0] * 65535.0), int(c[1] * 65535.0), int(c[2] * 65535.0))
+
+    def is_light(self, color):
+        # tells you if color is dark or light, so you can up or down the
+        # scale for improved contrast
+        return colorsys.rgb_to_hls(*self.rgb(color))[1] > 150
+
+    def darker(self, color, step):
+        # returns color darker by step (where step is in range 0..255)
+        hls = colorsys.rgb_to_hls(*self.rgb(color))
+        return colorsys.hls_to_rgb(hls[0], hls[1] - step, hls[2])
+
+    def contrast(self, color, step):
+        """if color is dark, will return a lighter one, otherwise darker"""
+        hls = colorsys.rgb_to_hls(*self.rgb(color))
+        if self.is_light(color):
+            return colorsys.hls_to_rgb(hls[0], hls[1] - step, hls[2])
+        else:
+            return colorsys.hls_to_rgb(hls[0], hls[1] + step, hls[2])
+        # returns color darker by step (where step is in range 0..255)
+
+Colors = Colors() # this is a static class, so an instance will do
+
+
+class Graphics(object):
+    """If context is given upon contruction, will perform drawing
+       operations on context instantly. Otherwise queues up the drawing
+       instructions and performs them in passed-in order when _draw is called
+       with context.
+
+       Most of instructions are mapped to cairo functions by the same name.
+       Where there are differences, documenation is provided.
+
+       See http://cairographics.org/documentation/pycairo/2/reference/context.html
+       for detailed description of the cairo drawing functions.
+    """
+    def __init__(self, context = None):
+        self.context = context
+        self.colors = Colors    # pointer to the color utilities instance
+        self.extents = None     # bounds of the object, only if interactive
+        self.paths = None       # paths for mouse hit checks
+        self._last_matrix = None
+        self.__new_instructions = [] # instruction set until it is converted into path-based instructions
+        self.__instruction_cache = []
+        self.cache_surface = None
+        self._cache_layout = None
+
+    def clear(self):
+        """clear all instructions"""
+        self.__new_instructions = []
+        self.__instruction_cache = []
+        self.paths = []
+
+    @staticmethod
+    def _stroke(context): context.stroke()
+    def stroke(self, color = None, alpha = 1):
+        if color or alpha < 1:self.set_color(color, alpha)
+        self._add_instruction(self._stroke,)
+
+    @staticmethod
+    def _fill(context): context.fill()
+    def fill(self, color = None, alpha = 1):
+        if color or alpha < 1:self.set_color(color, alpha)
+        self._add_instruction(self._fill,)
+
+    @staticmethod
+    def _mask(context, pattern): context.mask(pattern)
+    def mask(self, pattern):
+        self._add_instruction(self._mask, pattern)
+
+    @staticmethod
+    def _stroke_preserve(context): context.stroke_preserve()
+    def stroke_preserve(self, color = None, alpha = 1):
+        if color or alpha < 1:self.set_color(color, alpha)
+        self._add_instruction(self._stroke_preserve,)
+
+    @staticmethod
+    def _fill_preserve(context): context.fill_preserve()
+    def fill_preserve(self, color = None, alpha = 1):
+        if color or alpha < 1:self.set_color(color, alpha)
+        self._add_instruction(self._fill_preserve,)
+
+    @staticmethod
+    def _new_path(context): context.new_path()
+    def new_path(self):
+        self._add_instruction(self._new_path,)
+
+    @staticmethod
+    def _paint(context): context.paint()
+    def paint(self):
+        self._add_instruction(self._paint,)
+
+    @staticmethod
+    def _set_font_face(context, face): context.set_font_face(face)
+    def set_font_face(self, face):
+        self._add_instruction(self._set_font_face, face)
+
+    @staticmethod
+    def _set_font_size(context, size): context.set_font_size(size)
+    def set_font_size(self, size):
+        self._add_instruction(self._set_font_size, size)
+
+    @staticmethod
+    def _set_source(context, image):
+        context.set_source(image)
+    def set_source(self, image, x = 0, y = 0):
+        self._add_instruction(self._set_source, image)
+
+    @staticmethod
+    def _set_source_surface(context, surface, x, y):
+        context.set_source_surface(surface, x, y)
+    def set_source_surface(self, surface, x = 0, y = 0):
+        self._add_instruction(self._set_source_surface, surface, x, y)
+
+    @staticmethod
+    def _set_source_pixbuf(context, pixbuf, x, y):
+        context.set_source_pixbuf(pixbuf, x, y)
+    def set_source_pixbuf(self, pixbuf, x = 0, y = 0):
+        self._add_instruction(self._set_source_pixbuf, pixbuf, x, y)
+
+    @staticmethod
+    def _save_context(context): context.save()
+    def save_context(self):
+        self._add_instruction(self._save_context)
+
+    @staticmethod
+    def _restore_context(context): context.restore()
+    def restore_context(self):
+        self._add_instruction(self._restore_context)
+
+
+    @staticmethod
+    def _clip(context): context.clip()
+    def clip(self):
+        self._add_instruction(self._clip)
+
+    @staticmethod
+    def _translate(context, x, y): context.translate(x, y)
+    def translate(self, x, y):
+        self._add_instruction(self._translate, x, y)
+
+    @staticmethod
+    def _rotate(context, radians): context.rotate(radians)
+    def rotate(self, radians):
+        self._add_instruction(self._rotate, radians)
+
+    @staticmethod
+    def _move_to(context, x, y): context.move_to(x, y)
+    def move_to(self, x, y):
+        self._add_instruction(self._move_to, x, y)
+
+    @staticmethod
+    def _line_to(context, x, y): context.line_to(x, y)
+    def line_to(self, x, y = None):
+        if y is not None:
+            self._add_instruction(self._line_to, x, y)
+        elif isinstance(x, list) and y is None:
+            for x2, y2 in x:
+                self._add_instruction(self._line_to, x2, y2)
+
+
+    @staticmethod
+    def _rel_line_to(context, x, y): context.rel_line_to(x, y)
+    def rel_line_to(self, x, y = None):
+        if x and y:
+            self._add_instruction(self._rel_line_to, x, y)
+        elif isinstance(x, list) and y is None:
+            for x2, y2 in x:
+                self._add_instruction(self._rel_line_to, x2, y2)
+
+
+    @staticmethod
+    def _curve_to(context, x, y, x2, y2, x3, y3):
+        context.curve_to(x, y, x2, y2, x3, y3)
+    def curve_to(self, x, y, x2, y2, x3, y3):
+        """draw a curve. (x2, y2) is the middle point of the curve"""
+        self._add_instruction(self._curve_to, x, y, x2, y2, x3, y3)
+
+    @staticmethod
+    def _close_path(context): context.close_path()
+    def close_path(self):
+        self._add_instruction(self._close_path,)
+
+    @staticmethod
+    def _set_line_width(context, width):
+        context.set_line_width(width)
+    @staticmethod
+    def _set_dash(context, dash, dash_offset = 0):
+        context.set_dash(dash, dash_offset)
+
+    def set_line_style(self, width = None, dash = None, dash_offset = 0):
+        """change width and dash of a line"""
+        if width is not None:
+            self._add_instruction(self._set_line_width, width)
+
+        if dash is not None:
+            self._add_instruction(self._set_dash, dash, dash_offset)
+
+    def _set_color(self, context, r, g, b, a):
+        if a < 1:
+            context.set_source_rgba(r, g, b, a)
+        else:
+            context.set_source_rgb(r, g, b)
+
+    def set_color(self, color, alpha = 1):
+        """set active color. You can use hex colors like "#aaa", or you can use
+        normalized RGB tripplets (where every value is in range 0..1), or
+        you can do the same thing in range 0..65535.
+        also consider skipping this operation and specify the color on stroke and
+        fill.
+        """
+        color = self.colors.parse(color) # parse whatever we have there into a normalized triplet
+        if len(color) == 4 and alpha is None:
+            alpha = color[3]
+        r, g, b = color[:3]
+        self._add_instruction(self._set_color, r, g, b, alpha)
+
+    @staticmethod
+    def _arc(context, x, y, radius, start_angle, end_angle):
+        context.arc(x, y, radius, start_angle, end_angle)
+    def arc(self, x, y, radius, start_angle, end_angle):
+        """draw arc going counter-clockwise from start_angle to end_angle"""
+        self._add_instruction(self._arc, x, y, radius, start_angle, end_angle)
+
+    def circle(self, x, y, radius):
+        """draw circle"""
+        self._add_instruction(self._arc, x, y, radius, 0, math.pi * 2)
+
+    def ellipse(self, x, y, width, height, edges = None):
+        """draw 'perfect' ellipse, opposed to squashed circle. works also for
+           equilateral polygons"""
+        # the automatic edge case is somewhat arbitrary
+        steps = edges or max((32, width, height)) / 2
+
+        angle = 0
+        step = math.pi * 2 / steps
+        points = []
+        while angle < math.pi * 2:
+            points.append((width / 2.0 * math.cos(angle),
+                           height / 2.0 * math.sin(angle)))
+            angle += step
+
+        min_x = min((point[0] for point in points))
+        min_y = min((point[1] for point in points))
+
+        self.move_to(points[0][0] - min_x + x, points[0][1] - min_y + y)
+        for p_x, p_y in points:
+            self.line_to(p_x - min_x + x, p_y - min_y + y)
+        self.line_to(points[0][0] - min_x + x, points[0][1] - min_y + y)
+
+
+    @staticmethod
+    def _arc_negative(context, x, y, radius, start_angle, end_angle):
+        context.arc_negative(x, y, radius, start_angle, end_angle)
+    def arc_negative(self, x, y, radius, start_angle, end_angle):
+        """draw arc going clockwise from start_angle to end_angle"""
+        self._add_instruction(self._arc_negative, x, y, radius, start_angle, end_angle)
+
     @staticmethod
-    def rgb(color):
-        return [c * 255 for c in Colors.color(color)]
+    def _rounded_rectangle(context, x, y, x2, y2, corner_radius):
+        half_corner = corner_radius / 2
+
+        context.move_to(x + corner_radius, y)
+        context.line_to(x2 - corner_radius, y)
+        context.curve_to(x2 - half_corner, y, x2, y + half_corner, x2, y + corner_radius)
+        context.line_to(x2, y2 - corner_radius)
+        context.curve_to(x2, y2 - half_corner, x2 - half_corner, y2, x2 - corner_radius, y2)
+        context.line_to(x + corner_radius, y2)
+        context.curve_to(x + half_corner, y2, x, y2 - half_corner, x, y2 - corner_radius)
+        context.line_to(x, y + corner_radius)
+        context.curve_to(x, y + half_corner, x + half_corner, y, x + corner_radius, y)
+
+    @staticmethod
+    def _rectangle(context, x, y, w, h): context.rectangle(x, y, w, h)
+    def rectangle(self, x, y, width, height, corner_radius = 0):
+        "draw a rectangle. if corner_radius is specified, will draw rounded corners"
+        if corner_radius <= 0:
+            self._add_instruction(self._rectangle, x, y, width, height)
+            return
+
+        # make sure that w + h are larger than 2 * corner_radius
+        corner_radius = min(corner_radius, min(width, height) / 2)
+        x2, y2 = x + width, y + height
+        self._add_instruction(self._rounded_rectangle, x, y, x2, y2, corner_radius)
+
+    def fill_area(self, x, y, width, height, color, opacity = 1):
+        """fill rectangular area with specified color"""
+        self.rectangle(x, y, width, height)
+        self.fill(color, opacity)
+
+
+    def fill_stroke(self, fill = None, stroke = None, line_width = None):
+        """fill and stroke the drawn area in one go"""
+        if line_width: self.set_line_style(line_width)
+
+        if fill and stroke:
+            self.fill_preserve(fill)
+        elif fill:
+            self.fill(fill)
+
+        if stroke:
+            self.stroke(stroke)
+
+
+    @staticmethod
+    def _show_layout(context, layout, text, font_desc, alignment, width, wrap, ellipsize):
+        layout.set_font_description(font_desc)
+        layout.set_markup(text)
+        layout.set_width(width or -1)
+        layout.set_alignment(alignment)
+
+        if width > 0:
+            if wrap is not None:
+                layout.set_wrap(wrap)
+            else:
+                layout.set_ellipsize(ellipsize or pango.ELLIPSIZE_END)
+
+        context.show_layout(layout)
+
+    def create_layout(self, size = None):
+        """utility function to create layout with the default font. Size and
+        alignment parameters are shortcuts to according functions of the
+        pango.Layout"""
+        if not self.context:
+            # TODO - this is rather sloppy as far as exception goes
+            #        should explain better
+            raise "Can not create layout without existing context!"
+
+        layout = self.context.create_layout()
+        font_desc = pango.FontDescription(gtk.Style().font_desc.to_string())
+        if size: font_desc.set_size(size * pango.SCALE)
+
+        layout.set_font_description(font_desc)
+        return layout
+
+
+    def show_label(self, text, size = None, color = None):
+        """display text with system's default font"""
+        font_desc = pango.FontDescription(gtk.Style().font_desc.to_string())
+        if color: self.set_color(color)
+        if size: font_desc.set_size(size * pango.SCALE)
+        self.show_layout(text, font_desc)
+
+
+    @staticmethod
+    def _show_text(context, text): context.show_text(text)
+    def show_text(self, text):
+        self._add_instruction(self._show_text, text)
+
+    @staticmethod
+    def _text_path(context, text): context.text_path(text)
+    def text_path(self, text):
+        """this function is most likely to change"""
+        self._add_instruction(self._text_path, text)
+
+    def show_layout(self, text, font_desc, alignment = pango.ALIGN_LEFT, width = -1, wrap = None, ellipsize = None):
+        """display text. font_desc is string of pango font description
+           often handier than calling this function directly, is to create
+           a class:Label object
+        """
+        layout = self._cache_layout = self._cache_layout or gtk.gdk.CairoContext(cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, 0, 0))).create_layout()
+        self._add_instruction(self._show_layout, layout, text, font_desc, alignment, width, wrap, ellipsize)
+
+    def _add_instruction(self, function, *params):
+        if self.context:
+            function(self.context, *params)
+        else:
+            self.paths = None
+            self.__new_instructions.append((function, params))
+
+
+    def _draw(self, context, opacity):
+        """draw accumulated instructions in context"""
+
+        # if we have been moved around, we should update bounds
+        fresh_draw = self.__new_instructions and len(self.__new_instructions) > 0
+        if fresh_draw: #new stuff!
+            self.paths = []
+            self.__instruction_cache = self.__new_instructions
+            self.__new_instructions = []
+        else:
+            if not self.__instruction_cache:
+                return
+
+        for instruction, args in self.__instruction_cache:
+            if fresh_draw and instruction in (self._new_path, self._stroke, self._fill, self._clip):
+                self.paths.append(context.copy_path())
+
+            if opacity < 1 and instruction == self._set_color:
+                self._set_color(context, args[0], args[1], args[2], args[3] * opacity)
+            elif opacity < 1 and instruction == self._paint:
+                context.paint_with_alpha(opacity)
+            else:
+                instruction(context, *args)
+
+
+
+    def _draw_as_bitmap(self, context, opacity):
+        """
+            instead of caching paths, this function caches the whole drawn thing
+            use cache_as_bitmap on sprite to enable this mode
+        """
+        matrix = context.get_matrix()
+        matrix_changed = matrix != self._last_matrix
+        new_instructions = len(self.__new_instructions) > 0
+
+        if new_instructions or matrix_changed:
+            if new_instructions:
+                self.__instruction_cache = list(self.__new_instructions)
+                self.__new_instructions = deque()
+
+            self.paths = deque()
+            self.extents = None
+
+            if not self.__instruction_cache:
+                # no instructions - nothing to do
+                return
+
+            # instructions that end path
+            path_end_instructions = (self._new_path, self._clip, self._stroke, self._fill, self._stroke_preserve, self._fill_preserve)
+
+            # measure the path extents so we know the size of cache surface
+            # also to save some time use the context to paint for the first time
+            extents = gtk.gdk.Rectangle()
+            for instruction, args in self.__instruction_cache:
+                if instruction in path_end_instructions:
+                    self.paths.append(context.copy_path())
+                    exts = context.path_extents()
+                    exts = gtk.gdk.Rectangle(int(exts[0]), int(exts[1]),
+                                             int(exts[2]-exts[0]), int(exts[3]-exts[1]))
+                    if extents.width and extents.height:
+                        extents = extents.union(exts)
+                    else:
+                        extents = exts
+
+
+                if instruction in (self._set_source_pixbuf, self._set_source_surface):
+                    # draw a rectangle around the pathless instructions so that the extents are correct
+                    pixbuf = args[0]
+                    x = args[1] if len(args) > 1 else 0
+                    y = args[2] if len(args) > 2 else 0
+                    self._rectangle(context, x, y, pixbuf.get_width(), pixbuf.get_height())
+                    self._clip()
+
+                if instruction == self._paint and opacity < 1:
+                    context.paint_with_alpha(opacity)
+                elif instruction == self._set_color and opacity < 1:
+                    self._set_color(context, args[0], args[1], args[2], args[3] * opacity)
+                else:
+                    instruction(context, *args)
+
+
+            # avoid re-caching if we have just moved
+            just_transforms = new_instructions == False and \
+                              matrix and self._last_matrix \
+                              and all([matrix[i] == self._last_matrix[i] for i in range(4)])
+
+            # TODO - this does not look awfully safe
+            extents.x += matrix[4]
+            extents.y += matrix[5]
+            self.extents = extents
+
+            if not just_transforms:
+                # now draw the instructions on the caching surface
+                w = int(extents.width) + 1
+                h = int(extents.height) + 1
+                self.cache_surface = context.get_target().create_similar(cairo.CONTENT_COLOR_ALPHA, w, h)
+                ctx = gtk.gdk.CairoContext(cairo.Context(self.cache_surface))
+                ctx.translate(-extents.x, -extents.y)
+
+                ctx.transform(matrix)
+                for instruction, args in self.__instruction_cache:
+                    instruction(ctx, *args)
+
+            self._last_matrix = matrix
+        else:
+            context.save()
+            context.identity_matrix()
+            context.translate(self.extents.x, self.extents.y)
+            context.set_source_surface(self.cache_surface)
+            if opacity < 1:
+                context.paint_with_alpha(opacity)
+            else:
+                context.paint()
+            context.restore()
+
+
+
+
+
+class Sprite(gtk.Object):
+    """The Sprite class is a basic display list building block: a display list
+       node that can display graphics and can also contain children.
+       Once you have created the sprite, use Scene's add_child to add it to
+       scene
+    """
+
+    __gsignals__ = {
+        "on-mouse-over": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+        "on-mouse-out": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+        "on-mouse-down": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+        "on-mouse-up": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+        "on-click": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+        "on-drag-start": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+        "on-drag": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+        "on-drag-finish": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+        "on-render": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ())
+    }
+
+    transformation_flags = set(('x', 'y', 'rotation', 'scale_x', 'scale_y',
+                                'pivot_x', 'pivot_y', '_prev_parent_matrix'))
+    dirty_flags = set(('opacity', 'visible', 'z_order'))
+    graphics_unrelated_flags = set(('drag_x', 'drag_y', '_matrix', 'sprites',
+                                    '_stroke_context',
+                                    'mouse_cursor', '_scene', '_sprite_dirty'))
+
+
+    def __init__(self, x = 0, y = 0,
+                 opacity = 1, visible = True,
+                 rotation = 0, pivot_x = 0, pivot_y = 0,
+                 scale_x = 1, scale_y = 1,
+                 interactive = False, draggable = False,
+                 z_order = 0, mouse_cursor = None,
+                 cache_as_bitmap = False, snap_to_pixel = True):
+        gtk.Object.__init__(self)
+
+        self._scene = None
+
+        #: list of children sprites. Use :func:`add_child` to add sprites
+        self.sprites = []
+
+        #: instance of :ref:`graphics` for this sprite
+        self.graphics = Graphics()
+
+        #: boolean denoting whether the sprite responds to mouse events
+        self.interactive = interactive
+
+        #: boolean marking if sprite can be automatically dragged
+        self.draggable = draggable
+
+        #: relative x coordinate of the sprites' rotation point
+        self.pivot_x = pivot_x
+
+        #: relative y coordinates of the sprites' rotation point
+        self.pivot_y = pivot_y
+
+        #: sprite opacity
+        self.opacity = opacity
+
+        #: boolean visibility flag
+        self.visible = visible
+
+        #: pointer to parent :class:`Sprite` or :class:`Scene`
+        self.parent = None
+
+        #: sprite coordinates
+        self.x, self.y = x, y
+
+        #: rotation of the sprite in radians (use :func:`math.degrees` to convert to degrees if necessary)
+        self.rotation = rotation
+
+        #: scale X
+        self.scale_x = scale_x
+
+        #: scale Y
+        self.scale_y = scale_y
+
+        #: drawing order between siblings. The one with the highest z_order will be on top.
+        self.z_order = z_order
+
+        #: mouse-over cursor of the sprite. See :meth:`Scene.mouse_cursor`
+        #: for possible values
+        self.mouse_cursor = mouse_cursor
+
+        #: x position of the cursor within mouse upon drag. change this value
+        #: in on-drag-start to adjust drag point
+        self.drag_x = 0
+
+        #: y position of the cursor within mouse upon drag. change this value
+        #: in on-drag-start to adjust drag point
+        self.drag_y = 0
+
+        #: Whether the sprite should be cached as a bitmap. Default: true
+        #: Generally good when you have many static sprites
+        self.cache_as_bitmap = cache_as_bitmap
+
+        #: Should the sprite coordinates always rounded to full pixel. Default: true
+        #: Mostly this is good for performance but in some cases that can lead
+        #: to rounding errors in positioning.
+        self.snap_to_pixel = snap_to_pixel
+
+        self.__dict__["_sprite_dirty"] = True # flag that indicates that the graphics object of the sprite should be rendered
+        self.__dict__["_sprite_moved"] = True # flag that indicates that the graphics object of the sprite should be rendered
+
+        self._matrix = None
+        self._prev_parent_matrix = None
+
+        self._extents = None
+        self._prev_extents = None
+
+
+    def __setattr__(self, name, val):
+        if self.__dict__.get(name, "hamster_graphics_no_value_really") == val:
+            return
+
+        self.__dict__[name] = val
+
+        if name == 'visible' or (hasattr(self, "visible") and self.visible):
+            self.__dict__['_extents'] = None
+
+        if name == 'parent':
+            self._prev_parent_matrix = None
+            return
+
+        if name == '_prev_parent_matrix':
+            for sprite in self.sprites:
+                sprite._prev_parent_matrix = None
+            return
+
+        if name in self.graphics_unrelated_flags:
+            return
+
+        if name in self.transformation_flags:
+            self.__dict__['_matrix'] = None
+
+            for sprite in self.sprites:
+                sprite._prev_parent_matrix = None
+
+
+        elif name in ("visible", "z_order"):
+            for sprite in self.sprites:
+                sprite._prev_parent_matrix = None
+
+
+
+        if name == 'opacity' and self.__dict__.get("cache_as_bitmap") and hasattr(self, "graphics"):
+            # invalidating cache for the bitmap version as that paints opacity in the image
+            self.graphics._last_matrix = None
+        elif name == 'z_order' and self.__dict__.get('parent'):
+            self.parent._sort()
+
+
+
+        if name not in (self.transformation_flags ^ self.dirty_flags):
+            self.__dict__["_sprite_dirty"] = True
+            self.redraw()
+
+
+
+    def _sort(self):
+        """sort sprites by z_order"""
+        self.sprites = sorted(self.sprites, key=lambda sprite:sprite.z_order)
+
+    def add_child(self, *sprites):
+        """Add child sprite. Child will be nested within parent"""
+        for sprite in sprites:
+            if sprite == self:
+                raise Exception("trying to add sprite to itself")
+            if sprite.parent:
+                sprite.x, sprite.y = self.from_scene_coords(*sprite.to_scene_coords())
+                sprite.parent.remove_child(sprite)
+
+            self.sprites.append(sprite)
+            sprite.parent = self
+        self._sort()
+
+
+    def remove_child(self, *sprites):
+        for sprite in sprites:
+            self.sprites.remove(sprite)
+            sprite._scene = None
+            sprite.parent = None
+
+    def bring_to_front(self):
+        """adjusts sprite's z-order so that the sprite is on top of it's
+        siblings"""
+        if not self.parent:
+            return
+        self.z_order = self.parent.sprites[-1].z_order + 1
+
+    def send_to_back(self):
+        """adjusts sprite's z-order so that the sprite is behind it's
+        siblings"""
+        if not self.parent:
+            return
+        self.z_order = self.parent.sprites[0].z_order - 1
+
+
+    def get_extents(self):
+        """measure the extents of the sprite's graphics. if context is provided
+           will use that to draw the paths"""
+        if self._extents:
+            return self._extents
+
+        if not self.graphics.paths:
+            self.graphics._draw(cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, 0, 0)), 1)
+
+        if not self.graphics.paths:
+            return None
+
+        context = gtk.gdk.CairoContext(cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, 0, 0)))
+        context.transform(self.get_matrix())
+
+        for path in self.graphics.paths:
+            context.append_path(path)
+        context.identity_matrix()
+
+        ext = context.path_extents()
+        ext = gtk.gdk.Rectangle(int(ext[0]), int(ext[1]),
+                                int(ext[2] - ext[0]), int(ext[3] - ext[1]))
+
+        if not ext.width and not ext.height:
+            ext = None
+
+        self.__dict__['_extents'] = ext
+        self.__dict__['_stroke_context'] = context
+
+        return ext
+
+
+    def check_hit(self, x, y):
+        """check if the given coordinates are inside the sprite's fill or stroke
+           path"""
+
+        extents = self.get_extents()
+
+        if not extents:
+            return False
+
+        if extents.x <= x <= extents.x + extents.width and extents.y <= y <= extents.y + extents.height:
+            return self._stroke_context.in_fill(x, y)
+        else:
+            return False
+
+    def get_scene(self):
+        """returns class:`Scene` the sprite belongs to"""
+        if not self._scene:
+            if hasattr(self, 'parent') and self.parent:
+                if isinstance(self.parent, Sprite) == False:
+                    scene = self.parent
+                else:
+                    scene = self.parent.get_scene()
+
+                self._scene = scene
+
+        return self._scene
+
+    def redraw(self):
+        """queue redraw of the sprite. this function is called automatically
+           whenever a sprite attribute changes. sprite changes that happen
+           during scene redraw are ignored in order to avoid echoes.
+           Call scene.redraw() explicitly if you need to redraw in these cases.
+        """
+        scene = self.get_scene()
+        if scene and scene._redraw_in_progress == False and self.parent:
+            self.parent.redraw()
+
+    def animate(self, duration = None, easing = None, on_complete = None, on_update = None, **kwargs):
+        """Request paretn Scene to Interpolate attributes using the internal tweener.
+           Specify sprite's attributes that need changing.
+           `duration` defaults to 0.4 seconds and `easing` to cubic in-out
+           (for others see pytweener.Easing class).
+
+           Example::
+             # tween some_sprite to coordinates (50,100) using default duration and easing
+             self.animate(x = 50, y = 100)
+        """
+        scene = self.get_scene()
+        if scene:
+            scene.animate(self, duration, easing, on_complete, on_update, **kwargs)
+        else:
+            for key, val in kwargs.items():
+                setattr(self, key, val)
+
+    def get_local_matrix(self):
+        if not self._matrix:
+            self._matrix = cairo.Matrix()
+
+            if self.snap_to_pixel:
+                self._matrix.translate(int(self.x) + int(self.pivot_x), int(self.y) + int(self.pivot_y))
+            else:
+                self._matrix.translate(self.x + self.pivot_x, self.y + self.pivot_y)
+
+            if self.rotation:
+                self._matrix.rotate(self.rotation)
+
+
+            if self.snap_to_pixel:
+                self._matrix.translate(int(-self.pivot_x), int(-self.pivot_y))
+            else:
+                self._matrix.translate(-self.pivot_x, -self.pivot_y)
+
+
+            if self.scale_x != 1 or self.scale_y != 1:
+                self._matrix.scale(self.scale_x, self.scale_y)
+
+        return cairo.Matrix() * self._matrix
+
+
+    def get_matrix(self):
+        """return sprite's current transformation matrix"""
+        if self.parent:
+            return cairo_matrix_multiply(self.get_local_matrix(),
+                                         (self._prev_parent_matrix or self.parent.get_matrix()))
+        else:
+            return self.get_local_matrix()
+
+
+    def from_scene_coords(self, x=0, y=0):
+        """Converts x, y given in the scene coordinates to sprite's local ones
+        coordinates"""
+        matrix = self.get_matrix()
+        matrix.invert()
+        return matrix.transform_point(x, y)
+
+    def to_scene_coords(self, x=0, y=0):
+        """Converts x, y from sprite's local coordinates to scene coordinates"""
+        return self.get_matrix().transform_point(x, y)
+
+    def _draw(self, context, opacity = 1, parent_matrix = None):
+        if self.visible is False:
+            return
+
+        if (self._sprite_dirty): # send signal to redo the drawing when sprite is dirty
+            self.__dict__['_extents'] = None
+            self.emit("on-render")
+            self.__dict__["_sprite_dirty"] = False
+
+
+        parent_matrix = parent_matrix or cairo.Matrix()
+
+        # cache parent matrix
+        self._prev_parent_matrix = parent_matrix
+
+        matrix = self.get_local_matrix()
+
+        context.save()
+        context.transform(matrix)
+
+
+        if self.cache_as_bitmap:
+            self.graphics._draw_as_bitmap(context, self.opacity * opacity)
+        else:
+            self.graphics._draw(context, self.opacity * opacity)
+
+        self.__dict__['_prev_extents'] = self._extents or self.get_extents()
+
+        for sprite in self.sprites:
+            sprite._draw(context, self.opacity * opacity, cairo_matrix_multiply(matrix, parent_matrix))
+
+
+        context.restore()
+        context.new_path() #forget about us
+
+
+class BitmapSprite(Sprite):
+    """Caches given image data in a surface similar to targets, which ensures
+       that drawing it will be quick and low on CPU.
+       Image data can be either :class:`cairo.ImageSurface` or :class:`gtk.gdk.Pixbuf`
+    """
+    def __init__(self, image_data = None, cache_mode = None, **kwargs):
+        Sprite.__init__(self, **kwargs)
+
+        self.width, self.height = None, None
+        self.cache_mode = cache_mode or cairo.CONTENT_COLOR_ALPHA
+        #: image data
+        self.image_data = image_data
+
+        self._surface = None
+
+        self.graphics_unrelated_flags = self.graphics_unrelated_flags ^ set(('_surface',))
+
+    def __setattr__(self, name, val):
+        Sprite.__setattr__(self, name, val)
+        if name == 'image_data':
+            self.__dict__['_surface'] = None
+            if self.image_data:
+                self.__dict__['width'] = self.image_data.get_width()
+                self.__dict__['height'] = self.image_data.get_height()
+
+    def _draw(self, context, opacity = 1, parent_matrix = None):
+        if self.image_data is None or self.width is None or self.height is None:
+            return
+
+        if not self._surface:
+            # caching image on surface similar to the target
+            surface = context.get_target().create_similar(self.cache_mode,
+                                                          self.width,
+                                                          self.height)
+
+
+            local_context = gtk.gdk.CairoContext(cairo.Context(surface))
+            if isinstance(self.image_data, gtk.gdk.Pixbuf):
+                local_context.set_source_pixbuf(self.image_data, 0, 0)
+            else:
+                local_context.set_source_surface(self.image_data)
+            local_context.paint()
+
+            # add instructions with the resulting surface
+            self.graphics.clear()
+            self.graphics.rectangle(0, 0, self.width, self.height)
+            self.graphics.clip()
+            self.graphics.set_source_surface(surface)
+            self.graphics.paint()
+            self._surface = surface
+
+
+        Sprite._draw(self,  context, opacity, parent_matrix)
+
+
+class Image(BitmapSprite):
+    """Displays image by path. Currently supports only PNG images."""
+    def __init__(self, path, **kwargs):
+        BitmapSprite.__init__(self, **kwargs)
+
+        #: path to the image
+        self.path = path
+
+    def __setattr__(self, name, val):
+        BitmapSprite.__setattr__(self, name, val)
+        if name == 'path': # load when the value is set to avoid penalty on render
+            self.image_data = cairo.ImageSurface.create_from_png(self.path)
+
+
+
+class Icon(BitmapSprite):
+    """Displays icon by name and size in the theme"""
+    def __init__(self, name, size=24, **kwargs):
+        BitmapSprite.__init__(self, **kwargs)
+        self.theme = gtk.icon_theme_get_default()
+
+        #: icon name from theme
+        self.name = name
+
+        #: icon size in pixels
+        self.size = size
+
+    def __setattr__(self, name, val):
+        BitmapSprite.__setattr__(self, name, val)
+        if name in ('name', 'size'): # no other reason to discard cache than just on path change
+            if self.__dict__.get('name') and self.__dict__.get('size'):
+                self.image_data = self.theme.load_icon(self.name, self.size, 0)
+            else:
+                self.image_data = None
+
+
+class Label(Sprite):
+    __gsignals__ = {
+        "on-change": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+    }
+    def __init__(self, text = "", size = 10, color = None,
+                 alignment = pango.ALIGN_LEFT, font_face = None,
+                 max_width = None, wrap = None, ellipsize = None,
+                 outline_color = None, outline_width = 5,
+                 **kwargs):
+        Sprite.__init__(self, **kwargs)
+        self.width, self.height = None, None
+
+
+        self._test_context = gtk.gdk.CairoContext(cairo.Context(cairo.ImageSurface(cairo.FORMAT_A8, 0, 0)))
+        self._test_layout = self._test_context.create_layout()
+
+
+        #: pango.FontDescription, default is the system's font
+        self.font_desc = pango.FontDescription(gtk.Style().font_desc.to_string())
+        self.font_desc.set_size(size * pango.SCALE)
+
+        #: color of label either as hex string or an (r,g,b) tuple
+        self.color = color
+
+        #: color for text outline (currently works only with a custom font face)
+        self.outline_color = outline_color
+
+        #: text outline thickness (currently works only with a custom font face)
+        self.outline_width = outline_width
+
+        self._bounds_width = None
+
+        #: wrapping method. Can be set to pango. [WRAP_WORD, WRAP_CHAR,
+        #: WRAP_WORD_CHAR]
+        self.wrap = wrap
+
+        #: Ellipsize mode. Can be set to pango. [ELLIPSIZE_NONE,
+        #: ELLIPSIZE_START, ELLIPSIZE_MIDDLE, ELLIPSIZE_END]
+        self.ellipsize = ellipsize
+
+        #: alignment. one of pango.[ALIGN_LEFT, ALIGN_RIGHT, ALIGN_CENTER]
+        self.alignment = alignment
+
+        #: label's `FontFace <http://www.cairographics.org/documentation/pycairo/2/reference/text.html#cairo.FontFace>`_
+        self.font_face = font_face
+
+        #: font size
+        self.size = size
+
+
+        #: maximum  width of the label in pixels. if specified, the label
+        #: will be wrapped or ellipsized depending on the wrap and ellpisize settings
+        self.max_width = max_width
+
+        self._ascent = None # used to determine Y position for when we have a font face
+
+        self.__surface = None
+
+        #: label text
+        self.text = text
+
+        self._letter_sizes = {}
+        self._measures = {}
+
+        self.connect("on-render", self.on_render)
+
+        self.graphics_unrelated_flags = self.graphics_unrelated_flags ^ set(("_letter_sizes", "__surface", "_ascent", "_bounds_width", "_measures"))
+
+
+    def __setattr__(self, name, val):
+        if self.__dict__.get(name, "hamster_graphics_no_value_really") != val:
+            if name == "width" and val and self.__dict__.get('_bounds_width') and val * pango.SCALE == self.__dict__['_bounds_width']:
+                return
+
+            Sprite.__setattr__(self, name, val)
+
+
+            if name == "width":
+                # setting width means consumer wants to contrain the label
+                if val is None or val == -1:
+                    self.__dict__['_bounds_width'] = None
+                else:
+                    self.__dict__['_bounds_width'] = val * pango.SCALE
+
+            if name in ("width", "text", "size", "font_desc", "wrap", "ellipsize", "max_width"):
+                self._measures = {}
+                # avoid chicken and egg
+                if hasattr(self, "text") and hasattr(self, "size") and hasattr(self, "font_face"):
+                    self.__dict__['width'], self.__dict__['height'], self.__dict__['_ascent'] = self.measure(self.text)
+
+            if name in("font_desc", "size"):
+                self._letter_sizes = {}
+
+            if name == 'text':
+                self.emit('on-change')
+
+
+    def _wrap(self, text):
+        """wrapping text ourselves when we can't use pango"""
+        if not text:
+            return [], 0
+
+        context = self._test_context
+        context.set_font_face(self.font_face)
+        context.set_font_size(self.size)
+
+
+        if (not self._bounds_width and not self.max_width) or self.wrap is None:
+            return [(text, context.text_extents(text)[4])], context.font_extents()[2]
+
+
+        width = self.max_width or self.width
+
+        letters = {}
+        # measure individual letters
+        if self.wrap in (pango.WRAP_CHAR, pango.WRAP_WORD_CHAR):
+            letters = set(unicode(text))
+            sizes = [self._letter_sizes.setdefault(letter, context.text_extents(letter)[4]) for letter in letters]
+            letters = dict(zip(letters, sizes))
+
+
+        line = ""
+        lines = []
+        running_width = 0
+
+        if self.wrap in (pango.WRAP_WORD, pango.WRAP_WORD_CHAR):
+            # if we wrap by word then we split the whole thing in words
+            # and stick together while they fit. in case if the word does not
+            # fit at all, we break it in pieces
+            while text:
+                fragment, fragment_length = "", 0
+
+                word = re.search("\s", text)
+                if word:
+                    fragment = text[:word.start()+1]
+                else:
+                    fragment = text
+
+                fragment_length = context.text_extents(fragment)[4]
+
+
+                if (fragment_length > width) and self.wrap == pango.WRAP_WORD_CHAR:
+                    # too big to fit in any way
+                    # split in pieces so that we fit in current row as much
+                    # as we can and trust the task of putting things in next row
+                    # to the next run
+                    while fragment and running_width + fragment_length > width:
+                        fragment_length -= letters[fragment[-1]]
+                        fragment = fragment[:-1]
+
+                    lines.append((line + fragment, running_width + fragment_length))
+                    running_width = 0
+                    fragment_length = 0
+                    line = ""
+
+
+
+                else:
+                    # otherwise the usual squishing
+                    if running_width + fragment_length <= width:
+                        line += fragment
+                    else:
+                        lines.append((line, running_width))
+                        running_width = 0
+                        line = fragment
+
+
+
+                running_width += fragment_length
+                text = text[len(fragment):]
+
+        elif self.wrap == pango.WRAP_CHAR:
+            # brute force glueing while we have space
+            for fragment in text:
+                fragment_length = letters[fragment]
+
+                if running_width + fragment_length <= width:
+                    line += fragment
+                else:
+                    lines.append((line, running_width))
+                    running_width = 0
+                    line = fragment
+
+                running_width += fragment_length
+
+        if line:
+            lines.append((line, running_width))
+
+        return lines, context.font_extents()[2]
+
+
+
+
+    def measure(self, text):
+        """measures given text with label's font and size.
+        returns width, height and ascent. Ascent's null in case if the label
+        does not have font face specified (and is thusly using pango)"""
+
+        if text in self._measures:
+            return self._measures[text]
+
+        width, height, ascent = None, None, None
+
+        context = self._test_context
+        if self.font_face:
+            context.set_font_face(self.font_face)
+            context.set_font_size(self.size)
+            font_ascent, font_descent, font_height = context.font_extents()[:3]
+
+            if self._bounds_width or self.max_width:
+                lines, line_height = self._wrap(text)
+
+                if self._bounds_width:
+                    width = self._bounds_width / pango.SCALE
+                else:
+                    max_width = 0
+                    for line, line_width in lines:
+                        max_width = max(max_width, line_width)
+                    width = max_width
+
+                height = len(lines) * line_height
+                ascent = font_ascent
+            else:
+                width = context.text_extents(text)[4]
+                ascent, height = font_ascent, font_ascent + font_descent
+
+        else:
+            layout = self._test_layout
+            layout.set_font_description(self.font_desc)
+            layout.set_markup(text)
+            layout.set_width((self._bounds_width or -1))
+            layout.set_ellipsize(pango.ELLIPSIZE_NONE)
+
+            if self.wrap is not None:
+                layout.set_wrap(self.wrap)
+            else:
+                layout.set_ellipsize(self.ellipsize or pango.ELLIPSIZE_END)
+
+            width, height = layout.get_pixel_size()
+
+        self._measures[text] = width, height, ascent
+        return self._measures[text]
+
+
+    def on_render(self, sprite):
+        if not self.text:
+            self.graphics.clear()
+            return
+
+        self.graphics.set_color(self.color)
+
+        rect_width = self.width
+
+        if self.font_face:
+            self.graphics.set_font_size(self.size)
+            self.graphics.set_font_face(self.font_face)
+            if self._bounds_width or self.max_width:
+                lines, line_height = self._wrap(self.text)
+
+                x, y = 0.5, int(self._ascent) + 0.5
+                for line, line_width in lines:
+                    if self.alignment == pango.ALIGN_RIGHT:
+                        x = self.width - line_width
+                    elif self.alignment == pango.ALIGN_CENTER:
+                        x = (self.width - line_width) / 2
+
+                    if self.outline_color:
+                        self.graphics.save_context()
+                        self.graphics.move_to(x, y)
+                        self.graphics.text_path(line)
+                        self.graphics.set_line_style(width=self.outline_width)
+                        self.graphics.fill_stroke(self.outline_color, self.outline_color)
+                        self.graphics.restore_context()
+
+                    self.graphics.move_to(x, y)
+                    self.graphics.set_color(self.color)
+                    self.graphics.show_text(line)
+
+                    y += line_height
+
+            else:
+                if self.outline_color:
+                    self.graphics.save_context()
+                    self.graphics.move_to(0, self._ascent)
+                    self.graphics.text_path(self.text)
+                    self.graphics.set_line_style(width=self.outline_width)
+                    self.graphics.fill_stroke(self.outline_color, self.outline_color)
+                    self.graphics.restore_context()
+
+                self.graphics.move_to(0, self._ascent)
+                self.graphics.show_text(self.text)
+
+        else:
+            self.graphics.show_layout(self.text, self.font_desc,
+                                      self.alignment,
+                                      self._bounds_width,
+                                      self.wrap,
+                                      self.ellipsize)
+
+            if self._bounds_width:
+                rect_width = self._bounds_width / pango.SCALE
+
+        self.graphics.rectangle(0, 0, rect_width, self.height)
+        self.graphics.clip()
+
+
+
+class Rectangle(Sprite):
+    def __init__(self, w, h, corner_radius = 0, fill = None, stroke = None, line_width = 1, **kwargs):
+        Sprite.__init__(self, **kwargs)
+
+        #: width
+        self.width = w
+
+        #: height
+        self.height = h
+
+        #: fill color
+        self.fill = fill
+
+        #: stroke color
+        self.stroke = stroke
+
+        #: stroke line width
+        self.line_width = line_width
+
+        #: corner radius. Set bigger than 0 for rounded corners
+        self.corner_radius = corner_radius
+        self.connect("on-render", self.on_render)
+
+    def on_render(self, sprite):
+        self.graphics.set_line_style(width = self.line_width)
+        self.graphics.rectangle(0, 0, self.width, self.height, self.corner_radius)
+        self.graphics.fill_stroke(self.fill, self.stroke, self.line_width)
+
+
+class Polygon(Sprite):
+    def __init__(self, points, fill = None, stroke = None, line_width = 1, **kwargs):
+        Sprite.__init__(self, **kwargs)
+
+        #: list of (x,y) tuples that the line should go through. Polygon
+        #: will automatically close path.
+        self.points = points
+
+        #: fill color
+        self.fill = fill
+
+        #: stroke color
+        self.stroke = stroke
+
+        #: stroke line width
+        self.line_width = line_width
+
+        self.connect("on-render", self.on_render)
+
+    def on_render(self, sprite):
+        if not self.points: return
+
+        self.graphics.move_to(*self.points[0])
+        self.graphics.line_to(self.points)
+        self.graphics.close_path()
+
+        self.graphics.fill_stroke(self.fill, self.stroke, self.line_width)
+
+
+class Circle(Sprite):
+    def __init__(self, width, height, fill = None, stroke = None, line_width = 1, **kwargs):
+        Sprite.__init__(self, **kwargs)
+
+        #: circle width
+        self.width = width
+
+        #: circle height
+        self.height = height
+
+        #: fill color
+        self.fill = fill
+
+        #: stroke color
+        self.stroke = stroke
+
+        #: stroke line width
+        self.line_width = line_width
+
+        self.connect("on-render", self.on_render)
+
+    def on_render(self, sprite):
+        if self.width == self.height:
+            radius = self.width / 2.0
+            self.graphics.circle(radius, radius, radius)
+        else:
+            self.graphics.ellipse(0, 0, self.width, self.height)
+
+        self.graphics.fill_stroke(self.fill, self.stroke, self.line_width)
+
+
+class Scene(gtk.DrawingArea):
+    """ Drawing area for displaying sprites.
+        Add sprites to the Scene by calling :func:`add_child`.
+        Scene is descendant of `gtk.DrawingArea <http://www.pygtk.org/docs/pygtk/class-gtkdrawingarea.html>`_
+        and thus inherits all it's methods and everything.
+    """
 
-class Area(gtk.DrawingArea):
-    """Abstraction on top of DrawingArea to work specifically with cairo"""
     __gsignals__ = {
         "expose-event": "override",
         "configure_event": "override",
-        "mouse-over": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
-        "button-release": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
+        "on-enter-frame": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
+        "on-finish-frame": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
+
+        "on-click": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
+        "on-drag": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
+        "on-drag-start": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
+        "on-drag-finish": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
+
+        "on-mouse-move": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+        "on-mouse-down": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+        "on-mouse-up": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+        "on-mouse-over": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+        "on-mouse-out": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+
+        "on-scroll": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
     }
 
-    def __init__(self):
+    def __init__(self, interactive = True, framerate = 60,
+                       background_color = None, scale = False, keep_aspect = True):
         gtk.DrawingArea.__init__(self)
-        self.set_events(gtk.gdk.EXPOSURE_MASK
-                        | gtk.gdk.LEAVE_NOTIFY_MASK
-                        | gtk.gdk.BUTTON_PRESS_MASK
-                        | gtk.gdk.BUTTON_RELEASE_MASK
-                        | gtk.gdk.POINTER_MOTION_MASK
-                        | gtk.gdk.POINTER_MOTION_HINT_MASK)
-        self.connect("button_release_event", self.__on_button_release)
-        self.connect("motion_notify_event", self.__on_mouse_move)
-        self.connect("leave_notify_event", self.__on_mouse_out)
-
-        self.font_size = 8
-        self.mouse_regions = [] #regions of drawing that respond to hovering/clicking
-
-        self.context, self.layout = None, None
-        self.width, self.height = None, None
-        self.__prev_mouse_regions = None
-        
-        self.tweener = pytweener.Tweener(0.4, pytweener.Easing.Cubic.easeInOut)
-        self.framerate = 80
-        self.last_frame_time = None
-        self.__animating = False
-
-    def on_expose(self):
-        """ on_expose event is where you hook in all your drawing
-            canvas has been initialized for you """
-        raise NotImplementedError
-
-    def redraw_canvas(self):
-        """Redraw canvas. Triggers also to do all animations"""
-        if not self.__animating: #if we are moving, then there is a timeout somewhere already
-            self.__animating = True
-            self.last_frame_time = dt.datetime.now()
-            gobject.timeout_add(1000 / self.framerate, self.__interpolate)
-            
-    """ animation bits """
-    def __interpolate(self):
-        self.__animating = self.tweener.hasTweens()
-
-        if not self.window: #will wait until window comes
-            return self.__animating
-        
-        
-        time_since_start = (dt.datetime.now() - self.last_frame_time).microseconds / 1000000.0
-        self.tweener.update(time_since_start)
-
-        self.queue_draw()
-
-        self.last_frame_time = dt.datetime.now()
-        return self.__animating
-
-
-    def animate(self, object, params = {}, duration = None, easing = None, callback = None):
-        if duration: params["tweenTime"] = duration  # if none will fallback to tweener default
-        if easing: params["tweenType"] = easing    # if none will fallback to tweener default
-        if callback: params["onCompleteFunction"] = callback
-        self.tweener.addTween(object, **params)
-        self.redraw_canvas()
-    
-
-    """ drawing on canvas bits """
-    def draw_rect(self, x, y, w, h, corner_radius = 0):
-        if corner_radius <=0:
-            self.context.rectangle(x, y, w, h)
+        if interactive:
+            self.set_events(gtk.gdk.POINTER_MOTION_MASK
+                            | gtk.gdk.LEAVE_NOTIFY_MASK | gtk.gdk.ENTER_NOTIFY_MASK
+                            | gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK
+                            | gtk.gdk.SCROLL_MASK
+                            | gtk.gdk.KEY_PRESS_MASK)
+            self.connect("motion_notify_event", self.__on_mouse_move)
+            self.connect("enter_notify_event", self.__on_mouse_enter)
+            self.connect("leave_notify_event", self.__on_mouse_leave)
+            self.connect("button_press_event", self.__on_button_press)
+            self.connect("button_release_event", self.__on_button_release)
+            self.connect("scroll-event", self.__on_scroll)
+
+        #: list of sprites in scene. use :func:`add_child` to add sprites
+        self.sprites = []
+
+        #: framerate of animation. This will limit how often call for
+        #: redraw will be performed (that is - not more often than the framerate). It will
+        #: also influence the smoothness of tweeners.
+        self.framerate = framerate
+
+        #: Scene width. Will be `None` until first expose (that is until first
+        #: on-enter-frame signal below).
+        self.width = None
+
+        #: Scene height. Will be `None` until first expose (that is until first
+        #: on-enter-frame signal below).
+        self.height = None
+
+        #: instance of :class:`pytweener.Tweener` that is used by
+        #: :func:`animate` function, but can be also accessed directly for advanced control.
+        self.tweener = False
+        if pytweener:
+            self.tweener = pytweener.Tweener(0.4, pytweener.Easing.Cubic.ease_in_out)
+
+        #: instance of :class:`Colors` class for color parsing
+        self.colors = Colors
+
+        #: read only info about current framerate (frames per second)
+        self.fps = 0 # inner frames per second counter
+
+        #: Last known x position of the mouse (set on expose event)
+        self.mouse_x = None
+
+        #: Last known y position of the mouse (set on expose event)
+        self.mouse_y = None
+
+        #: Background color of the scene. Use either a string with hex color or an RGB triplet.
+        self.background_color = background_color
+
+        #: Mouse cursor appearance.
+        #: Replace with your own cursor or set to False to have no cursor.
+        #: None will revert back the default behavior
+        self.mouse_cursor = None
+
+        blank_pixmap = gtk.gdk.Pixmap(None, 1, 1, 1)
+        self._blank_cursor = gtk.gdk.Cursor(blank_pixmap, blank_pixmap, gtk.gdk.Color(), gtk.gdk.Color(), 0, 0)
+
+
+        #: Miminum distance in pixels for a drag to occur
+        self.drag_distance = 1
+
+        self._last_frame_time = None
+        self._mouse_sprite = None
+        self._drag_sprite = None
+        self._mouse_down_sprite = None
+        self.__drag_started = False
+        self.__drag_start_x, self.__drag_start_y = None, None
+
+        self._mouse_in = False
+        self.__last_cursor = None
+
+        self.__drawing_queued = False
+        self._redraw_in_progress = False
+
+        #: When specified, upon window resize the content will be scaled
+        #: relative to original window size. Defaults to False.
+        self.scale = scale
+
+        #: Should the stage maintain aspect ratio upon scale if
+        #: :attr:`Scene.scale` is enabled. Defaults to true.
+        self.keep_aspect = keep_aspect
+
+        self._original_width, self._original_height = None,  None
+
+
+
+    def add_child(self, *sprites):
+        """Add one or several :class:`Sprite` objects to the scene"""
+        for sprite in sprites:
+            if sprite == self:
+                raise Exception("trying to add sprite to itself")
+            if sprite.parent:
+                sprite.x, sprite.y = sprite.to_scene_coords(0, 0)
+                sprite.parent.remove_child(sprite)
+            self.sprites.append(sprite)
+            sprite.parent = self
+        self._sort()
+
+    def _sort(self):
+        """sort sprites by z_order"""
+        self.sprites = sorted(self.sprites, key=lambda sprite:sprite.z_order)
+
+
+    def remove_child(self, *sprites):
+        """Remove one or several :class:`Sprite` sprites from scene """
+        for sprite in sprites:
+            self.sprites.remove(sprite)
+            sprite._scene = None
+            sprite.parent = None
+
+    # these two mimic sprite functions so parent check can be avoided
+    def from_scene_coords(self, x, y): return x, y
+    def to_scene_coords(self, x, y): return x, y
+    def get_matrix(self): return cairo.Matrix()
+
+    def clear(self):
+        """Remove all sprites from scene"""
+        self.remove_child(*self.sprites)
+
+    def animate(self, sprite, duration = None, easing = None, on_complete = None, on_update = None, **kwargs):
+        """Interpolate attributes of the given object using the internal tweener
+           and redrawing scene after every tweener update.
+           Specify the sprite and sprite's attributes that need changing.
+           `duration` defaults to 0.4 seconds and `easing` to cubic in-out
+           (for others see pytweener.Easing class).
+
+           Redraw is requested right after creating the animation.
+           Example::
+
+             # tween some_sprite to coordinates (50,100) using default duration and easing
+             scene.animate(some_sprite, x = 50, y = 100)
+        """
+        if not self.tweener: # here we complain
+            raise Exception("pytweener was not found. Include it to enable animations")
+
+        tween = self.tweener.add_tween(sprite,
+                                       duration=duration,
+                                       easing=easing,
+                                       on_complete=on_complete,
+                                       on_update=on_update,
+                                       **kwargs)
+        self.redraw()
+        return tween
+
+
+    def redraw(self):
+        """Queue redraw. The redraw will be performed not more often than
+           the `framerate` allows"""
+        if self.__drawing_queued == False: #if we are moving, then there is a timeout somewhere already
+            self.__drawing_queued = True
+            self._last_frame_time = dt.datetime.now()
+            gobject.timeout_add(1000 / self.framerate, self.__redraw_loop)
+
+    def __redraw_loop(self):
+        """loop until there is nothing more to tween"""
+        self.queue_draw() # this will trigger do_expose_event when the current events have been flushed
+
+        self.__drawing_queued = self.tweener and self.tweener.has_tweens()
+        return self.__drawing_queued
+
+
+    def do_expose_event(self, event):
+        context = self.window.cairo_create()
+
+        # clip to the visible part
+        context.rectangle(event.area.x, event.area.y,
+                          event.area.width, event.area.height)
+        if self.background_color:
+            color = self.colors.parse(self.background_color)
+            context.set_source_rgb(*color)
+            context.fill_preserve()
+        context.clip()
+
+        if self.scale:
+            aspect_x = self.width / self._original_width
+            aspect_y = self.height / self._original_height
+            if self.keep_aspect:
+                aspect_x = aspect_y = min(aspect_x, aspect_y)
+            context.scale(aspect_x, aspect_y)
+
+        self.mouse_x, self.mouse_y, mods = self.get_window().get_pointer()
+
+        self._redraw_in_progress = True
+
+        # update tweens
+        now = dt.datetime.now()
+        delta = (now - (self._last_frame_time or dt.datetime.now())).microseconds / 1000000.0
+        self._last_frame_time = now
+        if self.tweener:
+            self.tweener.update(delta)
+
+        self.fps = 1 / delta
+
+
+        # start drawing
+        self.emit("on-enter-frame", context)
+        for sprite in self.sprites:
+            sprite._draw(context)
+
+        self.__check_mouse(self.mouse_x, self.mouse_y)
+        self.emit("on-finish-frame", context)
+        self._redraw_in_progress = False
+
+
+    def do_configure_event(self, event):
+        if self._original_width is None:
+            self._original_width = float(event.width)
+            self._original_height = float(event.height)
+
+        self.width, self.height = event.width, event.height
+
+
+    def all_visible_sprites(self):
+        """Returns flat list of the sprite tree for simplified iteration"""
+        def all_recursive(sprites):
+            for sprite in sprites:
+                if sprite.visible:
+                    yield sprite
+                    if sprite.sprites:
+                        for child in all_recursive(sprite.sprites):
+                            yield child
+
+        return all_recursive(self.sprites)
+
+
+    def get_sprite_at_position(self, x, y):
+        """Returns the topmost visible interactive sprite for given coordinates"""
+        over = None
+
+        for sprite in self.all_visible_sprites():
+            if (sprite.interactive or sprite.draggable) and sprite.check_hit(x, y):
+                over = sprite
+
+        return over
+
+
+    def __check_mouse(self, x, y):
+        if x is None or self._mouse_in == False:
             return
 
-        # make sure that w + h are larger than 2 * corner_radius
-        corner_radius = min(corner_radius, min(w, h) / 2)
+        cursor = gtk.gdk.ARROW # default
 
-        x2, y2 = x + w, y + h
+        if self.mouse_cursor is not None:
+            cursor = self.mouse_cursor
 
-        half_corner = corner_radius / 2
-        
-        self.context.move_to(x + corner_radius, y);
-        self.context.line_to(x2 - corner_radius, y);
-        # top-right
-        self.context.curve_to(x2 - half_corner, y,
-                              x2, y + half_corner,
-                              x2, y + corner_radius)
-
-        self.context.line_to(x2, y2 - corner_radius);
-        # bottom-right
-        self.context.curve_to(x2, y2 - half_corner,
-                              x2 - half_corner, y+h,
-                              x2 - corner_radius,y+h)
-
-        self.context.line_to(x + corner_radius, y2);
-        # bottom-left
-        self.context.curve_to(x + half_corner, y2,
-                              x, y2 - half_corner,
-                              x,y2 - corner_radius)
-
-        self.context.line_to(x, y + corner_radius);
-        # top-left
-        self.context.curve_to(x, y + half_corner,
-                              x + half_corner, y,
-                              x + corner_radius,y)
-
-
-    def rectangle(self, x, y, w, h, color = None, opacity = 0):
-        if color:
-            self.set_color(color, opacity)
-        self.context.rectangle(x, y, w, h)
-    
-    def fill_area(self, x, y, w, h, color, opacity = 0):
-        self.rectangle(x, y, w, h, color, opacity)
-        self.context.fill()
-
-    def set_text(self, text):
-        # sets text and returns width and height of the layout
-        self.layout.set_text(text)
-        return self.layout.get_pixel_size()
-        
-    def set_color(self, color, opacity = None):
-        color = Colors.color(color) #parse whatever we have there into a normalized triplet
-
-        if opacity:
-            self.context.set_source_rgba(color[0], color[1], color[2], opacity)
-        elif len(color) == 3:
-            self.context.set_source_rgb(*color)
+        if self._drag_sprite:
+            cursor = self._drag_sprite.mouse_cursor or self.mouse_cursor or gtk.gdk.FLEUR
         else:
-            self.context.set_source_rgba(*color)
+            #check if we have a mouse over
+            over = self.get_sprite_at_position(x, y)
+            if self._mouse_sprite and self._mouse_sprite != over:
+                self._mouse_sprite.emit("on-mouse-out")
+                self.emit("on-mouse-out", self._mouse_sprite)
+                self.redraw()
 
+            if over:
+                if over.mouse_cursor is not None:
+                    cursor = over.mouse_cursor
 
-    def register_mouse_region(self, x1, y1, x2, y2, region_name):
-        self.mouse_regions.append((x1, y1, x2, y2, region_name))
+                elif self.mouse_cursor is None:
+                    # resort to defaults
+                    if over.draggable:
+                        cursor = gtk.gdk.FLEUR
+                    else:
+                        cursor = gtk.gdk.HAND2
 
-    """ exposure events """
-    def do_configure_event(self, event):
-        (self.width, self.height) = self.window.get_size()
+                if over != self._mouse_sprite:
+                    over.emit("on-mouse-over")
+                    self.emit("on-mouse-over", over)
+                    self.redraw()
 
-    def do_expose_event(self, event):
-        self.width, self.height = self.window.get_size()
-        self.context = self.window.cairo_create()
+            self._mouse_sprite = over
 
-        self.context.rectangle(event.area.x, event.area.y,
-                               event.area.width, event.area.height)
-        self.context.clip()
+        if cursor == False:
+            cursor = self._blank_cursor
 
-        self.layout = self.context.create_layout()
-        default_font = pango.FontDescription(gtk.Style().font_desc.to_string())
-        default_font.set_size(self.font_size * pango.SCALE)
-        self.layout.set_font_description(default_font)
-        alloc = self.get_allocation()  #x, y, width, height
-        self.width, self.height = alloc.width, alloc.height
-        
-        self.mouse_regions = [] #reset since these can move in each redraw
-        self.on_expose()
+        if not self.__last_cursor or cursor != self.__last_cursor:
+            if isinstance(cursor, gtk.gdk.Cursor):
+                self.window.set_cursor(cursor)
+            else:
+                self.window.set_cursor(gtk.gdk.Cursor(cursor))
+
+            self.__last_cursor = cursor
 
 
     """ mouse events """
     def __on_mouse_move(self, area, event):
-        if not self.mouse_regions:
-            return
+        state = event.state
+
+
+        if self._mouse_down_sprite and self._mouse_down_sprite.draggable \
+           and gtk.gdk.BUTTON1_MASK & event.state:
+            # dragging around
+            if not self.__drag_started:
+                drag_started = (self.__drag_start_x is not None and \
+                               (self.__drag_start_x - event.x) ** 2 + \
+                               (self.__drag_start_y - event.y) ** 2 > self.drag_distance ** 2)
+
+                if drag_started:
+                    self._drag_sprite = self._mouse_down_sprite
+
+                    self._drag_sprite.drag_x, self._drag_sprite.drag_y = self._drag_sprite.x, self._drag_sprite.y
+
+                    self._drag_sprite.emit("on-drag-start", event)
+                    self.emit("on-drag-start", self._drag_sprite, event)
+                    self.redraw()
+
+                    self.__drag_started = True
+
+            if self.__drag_started:
+                diff_x, diff_y = event.x - self.__drag_start_x, event.y - self.__drag_start_y
+                if isinstance(self._drag_sprite.parent, Sprite):
+                    matrix = self._drag_sprite.parent.get_matrix()
+                    matrix.invert()
+                    diff_x, diff_y = matrix.transform_distance(diff_x, diff_y)
+
+                self._drag_sprite.x, self._drag_sprite.y = self._drag_sprite.drag_x + diff_x, self._drag_sprite.drag_y + diff_y
+
+                self._drag_sprite.emit("on-drag", event)
+                self.emit("on-drag", self._drag_sprite, event)
+                self.redraw()
 
-        if event.is_hint:
-            x, y, state = event.window.get_pointer()
-        else:
-            x = event.x
-            y = event.y
-            state = event.state
-        
-        mouse_regions = []
-        for region in self.mouse_regions:
-            if region[0] < x < region[2] and region[1] < y < region[3]:
-                mouse_regions.append(region[4])
-        
-        if mouse_regions:
-            area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
         else:
-            area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
+            # avoid double mouse checks - the redraw will also check for mouse!
+            if not self.__drawing_queued:
+                self.__check_mouse(event.x, event.y)
+
+        self.emit("on-mouse-move", event)
 
-        if mouse_regions != self.__prev_mouse_regions:
-            self.emit("mouse-over", mouse_regions)
+    def __on_mouse_enter(self, area, event):
+        self._mouse_in = True
 
-        self.__prev_mouse_regions = mouse_regions
+    def __on_mouse_leave(self, area, event):
+        self._mouse_in = False
+        if self._mouse_sprite:
+            self.emit("on-mouse-out", self._mouse_sprite)
+            self.redraw()
+            self._mouse_sprite = None
 
-    def __on_mouse_out(self, area, event):
-        self.__prev_mouse_regions = None
-        self.emit("mouse-over", [])
+
+    def __on_button_press(self, area, event):
+        target = self.get_sprite_at_position(event.x, event.y)
+        self.__drag_start_x, self.__drag_start_y = event.x, event.y
+
+        self._mouse_down_sprite = target
+
+        if target:
+            target.emit("on-mouse-down", event)
+        self.emit("on-mouse-down", event)
 
     def __on_button_release(self, area, event):
-        if not self.mouse_regions:
-            return
+        target = self.get_sprite_at_position(event.x, event.y)
 
-        x = event.x
-        y = event.y
-        state = event.state
-        
-        mouse_regions = []
-        for region in self.mouse_regions:
-            if region[0] < x < region[2] and region[1] < y < region[3]:
-                mouse_regions.append(region[4])
-
-        if mouse_regions:
-            self.emit("button-release", mouse_regions)
-
- 
-
-
-""" simple example """
-class SampleArea(Area):
-    def __init__(self):
-        Area.__init__(self)
-        self.rect_x, self.rect_y = 100, -100
-        self.rect_width, self.rect_height = 90, 90
-
-        self.text_y = -100
-        
-        
-    def on_expose(self):
-        # on expose is called when we are ready to draw
-        
-        # fill_area is just a shortcut function
-        # feel free to use self.context. move_to, line_to and others
-        self.font_size = 32
-        self.layout.set_text("Hello, World!")
-        
-        self.draw_rect(round(self.rect_x),
-                       round(self.rect_y),
-                       self.rect_width,
-                       self.rect_height,
-                       10)
-        
-        self.set_color("#ff00ff")
-        self.context.fill()
-
-        self.context.move_to((self.width - self.layout.get_pixel_size()[0]) / 2,
-                             self.text_y)
-        
-        self.set_color("#333")
-        self.context.show_layout(self.layout)
-        
-
-class BasicWindow:
-    def __init__(self):
-        window = gtk.Window(gtk.WINDOW_TOPLEVEL)
-        window.set_title("Graphics Module")
-        window.set_size_request(300, 300)
-        window.connect("delete_event", lambda *args: gtk.main_quit())
-    
-        self.graphic = SampleArea()
-        
-        box = gtk.VBox()
-        box.pack_start(self.graphic)
-        
-        button = gtk.Button("Hello")
-        button.connect("clicked", self.on_go_clicked)
-
-        box.add_with_properties(button, "expand", False)
-    
-        window.add(box)
-        window.show_all()
-        
-        # drop the hello on init
-        self.graphic.animate(self.graphic,
-                            dict(text_y = 120),
-                            duration = 0.7,
-                            easing = Easing.Bounce.easeOut)
-
-
-    def on_go_clicked(self, widget):
-        import random
-        
-        # set x and y to random position within the drawing area
-        x = round(min(random.random() * self.graphic.width,
-                      self.graphic.width - self.graphic.rect_width))
-        y = round(min(random.random() * self.graphic.height,
-                      self.graphic.height - self.graphic.rect_height))
-        
-        # here we call the animate function with parameters we would like to change
-        # the easing functions outside graphics module can be accessed via
-        # graphics.Easing
-        self.graphic.animate(self.graphic,
-                             dict(rect_x = x, rect_y = y),
-                             duration = 0.8,
-                             easing = Easing.Elastic.easeOut)
-
-
-if __name__ == "__main__":
-   example = BasicWindow()
-   gtk.main()
-    
\ No newline at end of file
+        if target:
+            target.emit("on-mouse-up", event)
+        self.emit("on-mouse-up", event)
+
+        # trying to not emit click and drag-finish at the same time
+        click = not self.__drag_started or (event.x - self.__drag_start_x) ** 2 + \
+                                           (event.y - self.__drag_start_y) ** 2 < self.drag_distance
+        if (click and self.__drag_started == False) or not self._drag_sprite:
+            if target:
+                target.emit("on-click", event)
+
+            self.emit("on-click", event, target)
+            self.redraw()
+
+        if self._drag_sprite:
+            self._drag_sprite.emit("on-drag-finish", event)
+            self.emit("on-drag-finish", self._drag_sprite, event)
+            self.redraw()
+
+            self._drag_sprite.drag_x, self._drag_sprite.drag_y = None, None
+            self._drag_sprite = None
+        self._mouse_down_sprite = None
+
+        self.__drag_started = False
+        self.__drag_start_x, self__drag_start_y = None, None
+
+    def __on_scroll(self, area, event):
+        self.emit("on-scroll", event)
diff --git a/src/gui/widgets/pytweener.py b/src/gui/widgets/pytweener.py
index 113b4a5..f5cacd7 100644
--- a/src/gui/widgets/pytweener.py
+++ b/src/gui/widgets/pytweener.py
@@ -5,651 +5,339 @@
 # Heavily based on caurina Tweener: http://code.google.com/p/tweener/
 #
 # Released under M.I.T License - see above url
-# Python version by Ben Harling 2009 
+# Python version by Ben Harling 2009
+# All kinds of slashing and dashing by Toms Baugis 2010
 import math
+import collections
+import datetime as dt
+import time
+import re
 
-class Tweener:
-    def __init__(self, duration = 0.5, tween = None):
+class Tweener(object):
+    def __init__(self, default_duration = None, tween = None):
         """Tweener
         This class manages all active tweens, and provides a factory for
         creating and spawning tween motions."""
-        self.currentTweens = []
-        self.defaultTweenType = tween or Easing.Cubic.easeInOut
-        self.defaultDuration = duration or 1.0
- 
-    def hasTweens(self):
-        return len(self.currentTweens) > 0
- 
- 
-    def addTween(self, obj, **kwargs):
-        """ addTween( object, **kwargs) -> tweenObject or False
- 
-            Example:
-            tweener.addTween( myRocket, throttle=50, setThrust=400, tweenTime=5.0, tweenType=tweener.OUT_QUAD )
- 
-            You must first specify an object, and at least one property or function with a corresponding
-            change value. The tween will throw an error if you specify an attribute the object does
-            not possess. Also the data types of the change and the initial value of the tweened item
-            must match. If you specify a 'set' -type function, the tweener will attempt to get the
-            starting value by call the corresponding 'get' function on the object. If you specify a 
-            property, the tweener will read the current state as the starting value. You add both 
-            functions and property changes to the same tween.
- 
-            in addition to any properties you specify on the object, these keywords do additional
-            setup of the tween.
- 
-            tweenTime = the duration of the motion
-            tweenType = one of the predefined tweening equations or your own function
-            onCompleteFunction = specify a function to call on completion of the tween
-            onUpdateFunction = specify a function to call every time the tween updates
-            tweenDelay = specify a delay before starting.
-            """
-        if "tweenTime" in kwargs:
-            t_time = kwargs.pop("tweenTime")
-        else: t_time = self.defaultDuration
- 
-        if "tweenType" in kwargs:
-            t_type = kwargs.pop("tweenType")
-        else: t_type = self.defaultTweenType
- 
-        if "onCompleteFunction" in kwargs:
-            t_completeFunc = kwargs.pop("onCompleteFunction")
-        else: t_completeFunc = None
- 
-        if "onUpdateFunction" in kwargs:
-            t_updateFunc = kwargs.pop("onUpdateFunction")
-        else: t_updateFunc = None
- 
-        if "tweenDelay" in kwargs:
-            t_delay = kwargs.pop("tweenDelay")
-        else: t_delay = 0
- 
-        tw = Tween( obj, t_time, t_type, t_completeFunc, t_updateFunc, t_delay, **kwargs )
-        if tw:    
-            self.currentTweens.append( tw )
+        self.current_tweens = collections.defaultdict(set)
+        self.default_easing = tween or Easing.Cubic.ease_in_out
+        self.default_duration = default_duration or 1.0
+
+    def has_tweens(self):
+        return len(self.current_tweens) > 0
+
+
+    def add_tween(self, obj, duration = None, easing = None, on_complete = None, on_update = None, **kwargs):
+        """
+            Add tween for the object to go from current values to set ones.
+            Example: add_tween(sprite, x = 500, y = 200, duration = 0.4)
+            This will move the sprite to coordinates (500, 200) in 0.4 seconds.
+            For parameter "easing" you can use one of the pytweener.Easing
+            functions, or specify your own.
+            The tweener can handle numbers, dates and color strings in hex ("#ffffff").
+            This function performs overwrite style conflict solving - in case
+            if a previous tween operates on same attributes, the attributes in
+            question are removed from that tween.
+        """
+        if duration is None:
+            duration = self.default_duration
+
+        easing = easing or self.default_easing
+
+        tw = Tween(obj, duration, easing, on_complete, on_update, **kwargs )
+
+        if obj in self.current_tweens:
+            for current_tween in tuple(self.current_tweens[obj]):
+                prev_keys = set((key for (key, tweenable) in current_tween.tweenables))
+                dif = prev_keys & set(kwargs.keys())
+
+                for key, tweenable in tuple(current_tween.tweenables):
+                    if key in dif:
+                        current_tween.tweenables.remove((key, tweenable))
+
+                if not current_tween.tweenables:
+                    current_tween.finish()
+                    self.current_tweens[obj].remove(current_tween)
+
+
+        self.current_tweens[obj].add(tw)
         return tw
- 
-    def removeTween(self, tweenObj):
-        if tweenObj in self.currentTweens:
-            tweenObj.complete = True
-            #self.currentTweens.remove( tweenObj )
- 
-    def getTweensAffectingObject(self, obj):
+
+
+    def get_tweens(self, obj):
         """Get a list of all tweens acting on the specified object
         Useful for manipulating tweens on the fly"""
-        tweens = []
-        for t in self.currentTweens:
-            if t.target is obj:
-                tweens.append(t)
-        return tweens
- 
-    def removeTweeningFrom(self, obj):
-        """Stop tweening an object, without completing the motion
-        or firing the completeFunction"""
-        for t in self.currentTweens:
-            if t.target is obj:
-                t.complete = True
- 
+        return self.current_tweens.get(obj, None)
+
+    def kill_tweens(self, obj = None):
+        """Stop tweening an object, without completing the motion or firing the
+        on_complete"""
+        if obj:
+            try:
+                del self.current_tweens[obj]
+            except:
+                pass
+        else:
+            self.current_tweens = collections.defaultdict(set)
+
+    def remove_tween(self, tween):
+        """"remove given tween without completing the motion or firing the on_complete"""
+        if tween.target in self.current_tweens and tween in self.current_tweens[tween.target]:
+            self.current_tweens[tween.target].remove(tween)
+            if not self.current_tweens[tween.target]:
+                del self.current_tweens[tween.target]
+
     def finish(self):
-        #go to last frame for all tweens
-        for t in self.currentTweens:
-            t.update(t.duration)
-        self.currentTweens = []
- 
-    def update(self, timeSinceLastFrame):
-        removable = []
-        for t in self.currentTweens:
-            t.update(timeSinceLastFrame)
-
-            if t.complete:
-                removable.append(t)
-                
-        for t in removable:
-            self.currentTweens.remove(t)
-            
- 
+        """jump the the last frame of all tweens"""
+        for obj in self.current_tweens:
+            for tween in self.current_tweens[obj]:
+                tween.finish()
+        self.current_tweens = {}
+
+    def update(self, delta_seconds):
+        """update tweeners. delta_seconds is time in seconds since last frame"""
+
+        for obj in tuple(self.current_tweens):
+            for tween in tuple(self.current_tweens[obj]):
+                done = tween._update(delta_seconds)
+                if done:
+                    self.current_tweens[obj].remove(tween)
+                    if tween.on_complete: tween.on_complete(tween.target)
+
+            if not self.current_tweens[obj]:
+                del self.current_tweens[obj]
+
+        return self.current_tweens
+
+
 class Tween(object):
-    def __init__(self, obj, tduration, tweenType, completeFunction, updateFunction, delay, **kwargs):
-        """Tween object:
-            Can be created directly, but much more easily using Tweener.addTween( ... )
-            """
-        #print obj, tduration, kwargs
-        self.duration = tduration
-        self.delay = delay
+    __slots__ = ('tweenables', 'target', 'delta', 'duration', 'ease', 'delta',
+                 'on_complete', 'on_update', 'complete')
+
+    def __init__(self, obj, duration, easing, on_complete, on_update, **kwargs):
+        """Tween object use Tweener.add_tween( ... ) to create"""
+        self.duration = duration
         self.target = obj
-        self.tween = tweenType
-        self.tweenables = kwargs
+        self.ease = easing
+
+        # list of (property, start_value, delta)
+        self.tweenables = set()
+        for key, value in kwargs.items():
+            self.tweenables.add((key, Tweenable(self.target.__dict__[key], value)))
+
         self.delta = 0
-        self.completeFunction = completeFunction
-        self.updateFunction = updateFunction
+        self.on_complete = on_complete
+        self.on_update = on_update
         self.complete = False
-        self.tProps = []
-        self.tFuncs = []
-        self.paused = self.delay > 0
-        self.decodeArguments()
- 
-    def decodeArguments(self):
-        """Internal setup procedure to create tweenables and work out
-           how to deal with each"""
- 
-        if len(self.tweenables) == 0:
-            # nothing to do 
-            print "TWEEN ERROR: No Tweenable properties or functions defined"
-            self.complete = True
-            return
- 
-        for k, v in self.tweenables.items():
- 
-        # check that its compatible
-            if not hasattr( self.target, k):
-                print "TWEEN ERROR: " + str(self.target) + " has no function " + k
-                self.complete = True
-                break
- 
-            prop = func = False
-            startVal = 0
-            newVal = v
- 
-            try:
-                startVal = self.target.__dict__[k]
-                prop = k
-                propName = k
- 
-            except:
-                func = getattr( self.target, k)
-                funcName = k
- 
-            if func:
-                try:
-                    getFunc = getattr(self.target, funcName.replace("set", "get") )
-                    startVal = getFunc()
-                except:
-                    # no start value, assume its 0
-                    # but make sure the start and change
-                    # dataTypes match :)
-                    startVal = newVal * 0
-                tweenable = Tweenable( startVal, newVal - startVal)    
-                newFunc = [ k, func, tweenable]
- 
-                #setattr(self, funcName, newFunc[2])
-                self.tFuncs.append( newFunc )
- 
- 
-            if prop:
-                tweenable = Tweenable( startVal, newVal - startVal)    
-                newProp = [ k, prop, tweenable]
-                self.tProps.append( newProp )  
- 
- 
-    def pause( self, numSeconds=-1 ):
-        """Pause this tween
-            do tween.pause( 2 ) to pause for a specific time
-            or tween.pause() which pauses indefinitely."""
-        self.paused = True
-        self.delay = numSeconds
- 
-    def resume( self ):
-        """Resume from pause"""
-        if self.paused:
-            self.paused=False
- 
-    def update(self, ptime):
-        """Update this tween with the time since the last frame
-            if there is an update function, it is always called
-            whether the tween is running or paused"""
-            
-        if self.complete:
-            return
-        
-        if self.paused:
-            if self.delay > 0:
-                self.delay = max( 0, self.delay - ptime )
-                if self.delay == 0:
-                    self.paused = False
-                    self.delay = -1
-                if self.updateFunction:
-                    self.updateFunction()
-            return
- 
-        self.delta = min(self.delta + ptime, self.duration)
- 
-
-        for propName, prop, tweenable in self.tProps:
-            self.target.__dict__[prop] = self.tween( self.delta, tweenable.startValue, tweenable.change, self.duration )
-        for funcName, func, tweenable in self.tFuncs:
-            func( self.tween( self.delta, tweenable.startValue, tweenable.change, self.duration ) )
- 
- 
+
+    def finish(self):
+        self._update(self.duration)
+
+    def _update(self, ptime):
+        """Update tween with the time since the last frame"""
+        self.delta = self.delta + ptime
+        if self.delta > self.duration:
+            self.delta = self.duration
+
         if self.delta == self.duration:
+            for key, tweenable in self.tweenables:
+                self.target.__setattr__(key, tweenable.target_value)
+        else:
+            fraction = self.ease(self.delta / self.duration)
+
+            for key, tweenable in self.tweenables:
+                self.target.__setattr__(key, tweenable.update(fraction))
+
+        if self.delta == self.duration or len(self.tweenables) == 0:
             self.complete = True
-            if self.completeFunction:
-                self.completeFunction()
- 
-        if self.updateFunction:
-            self.updateFunction()
- 
- 
- 
-    def getTweenable(self, name):
-        """Return the tweenable values corresponding to the name of the original
-        tweening function or property. 
- 
-        Allows the parameters of tweens to be changed at runtime. The parameters
-        can even be tweened themselves!
- 
-        eg:
- 
-        # the rocket needs to escape!! - we're already moving, but must go faster!
-        twn = tweener.getTweensAffectingObject( myRocket )[0]
-        tweenable = twn.getTweenable( "thrusterPower" )
-        tweener.addTween( tweenable, change=1000.0, tweenTime=0.4, tweenType=tweener.IN_QUAD )
- 
-        """
-        ret = None
-        for n, f, t in self.tFuncs:
-            if n == name:
-                ret = t
-                return ret
-        for n, p, t in self.tProps:
-            if n == name:
-                ret = t
-                return ret
-        return ret
- 
-    def Remove(self):
-        """Disables and removes this tween
-            without calling the complete function"""
-        self.complete = True
-
- 
-class Tweenable:
-    def __init__(self, start, change):
-        """Tweenable:
-            Holds values for anything that can be tweened
-            these are normally only created by Tweens"""
-        self.startValue = start
-        self.change = change
-
-
-"""Robert Penner's easing classes ported over from actionscript by Toms Baugis (at gmail com).
-There certainly is room for improvement, but wanted to keep the readability to some extent.
-
-================================================================================
- Easing Equations
- (c) 2003 Robert Penner, all rights reserved. 
- This work is subject to the terms in
- http://www.robertpenner.com/easing_terms_of_use.html.
-================================================================================
-
-TERMS OF USE - EASING EQUATIONS
-
-Open source under the BSD License.
-
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without modification,
-are permitted provided that the following conditions are met:
-
-    * Redistributions of source code must retain the above copyright notice,
-      this list of conditions and the following disclaimer.
-    * Redistributions in binary form must reproduce the above copyright notice,
-      this list of conditions and the following disclaimer in the documentation
-      and/or other materials provided with the distribution.
-    * Neither the name of the author nor the names of contributors may be used
-      to endorse or promote products derived from this software without specific
-      prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
-ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
-ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+        if self.on_update:
+            self.on_update(self.target)
+
+        return self.complete
+
+
+
+
+class Tweenable(object):
+    """a single attribute that has to be tweened from start to target"""
+    __slots__ = ('start_value', 'change', 'decode_func', 'target_value', 'update')
+
+    hex_color_normal = re.compile("#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})")
+    hex_color_short = re.compile("#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])")
+
+
+    def __init__(self, start_value, target_value):
+        self.decode_func = lambda x: x
+        self.target_value = target_value
+
+        def float_update(fraction):
+            return self.start_value + self.change * fraction
+
+        def date_update(fraction):
+            return dt.date.fromtimestamp(self.start_value + self.change * fraction)
+
+        def datetime_update(fraction):
+            return dt.datetime.fromtimestamp(self.start_value + self.change * fraction)
+
+        def color_update(fraction):
+            val = [max(min(self.start_value[i] + self.change[i] * fraction, 255), 0)  for i in range(3)]
+            return "#%02x%02x%02x" % (val[0], val[1], val[2])
+
+
+        if isinstance(start_value, int) or isinstance(start_value, float):
+            self.start_value = start_value
+            self.change = target_value - start_value
+            self.update = float_update
+        else:
+            if isinstance(start_value, dt.datetime) or isinstance(start_value, dt.date):
+                if isinstance(start_value, dt.datetime):
+                    self.update = datetime_update
+                else:
+                    self.update = date_update
+
+                self.decode_func = lambda x: time.mktime(x.timetuple())
+                self.start_value = self.decode_func(start_value)
+                self.change = self.decode_func(target_value) - self.start_value
+
+            elif isinstance(start_value, basestring) \
+             and (self.hex_color_normal.match(start_value) or self.hex_color_short.match(start_value)):
+                self.update = color_update
+                if self.hex_color_normal.match(start_value):
+                    self.decode_func = lambda val: [int(match, 16)
+                                                    for match in self.hex_color_normal.match(val).groups()]
+
+                elif self.hex_color_short.match(start_value):
+                    self.decode_func = lambda val: [int(match + match, 16)
+                                                    for match in self.hex_color_short.match(val).groups()]
+
+                if self.hex_color_normal.match(target_value):
+                    target_value = [int(match, 16)
+                                    for match in self.hex_color_normal.match(target_value).groups()]
+                else:
+                    target_value = [int(match + match, 16)
+                                    for match in self.hex_color_short.match(target_value).groups()]
+
+                self.start_value = self.decode_func(start_value)
+                self.change = [target - start for start, target in zip(self.start_value, target_value)]
+
+
+
+"""Robert Penner's classes stripped from the repetetive c,b,d mish-mash
+(discovery of Patryk Zawadzki). This way we do the math once and apply to
+all the tweenables instead of repeating it for each attribute
 """
-class Easing:
-    class Back:
-        @staticmethod
-        def easeIn(t, b, c, d, s = 1.70158):
-            t = t / d
-            return c * t**2 * ((s+1) * t - s) + b
-
-        @staticmethod
-        def easeOut (t, b, c, d, s = 1.70158):
-            t = t / d - 1
-            return c * (t**2 * ((s + 1) * t + s) + 1) + b
-
-        @staticmethod
-        def easeInOut (t, b, c, d, s = 1.70158):
-            t = t / (d * 0.5)
-            s = s * 1.525
-            
-            if t < 1:
-                return c * 0.5 * (t**2 * ((s + 1) * t - s)) + b
-
-            t = t - 2
-            return c / 2 * (t**2 * ((s + 1) * t + s) + 2) + b
-
-    class Bounce:
-        @staticmethod
-        def easeOut (t, b, c, d):
-            t = t / d
-            if t < 1 / 2.75:
-                return c * (7.5625 * t**2) + b
-            elif t < 2 / 2.75:
-                t = t - 1.5 / 2.75
-                return c * (7.5625 * t**2 + 0.75) + b
-            elif t < 2.5 / 2.75:
-                t = t - 2.25 / 2.75
-                return c * (7.5625 * t**2 + .9375) + b
-            else:
-                t = t - 2.625 / 2.75
-                return c * (7.5625 * t**2 + 0.984375) + b
-
-        @staticmethod
-        def easeIn (t, b, c, d):
-            return c - Easing.Bounce.easeOut(d-t, 0, c, d) + b
-
-        @staticmethod
-        def easeInOut (t, b, c, d):
-            if t < d * 0.5:
-                return Easing.Bounce.easeIn (t * 2, 0, c, d) * .5 + b
-
-            return Easing.Bounce.easeOut (t * 2 -d, 0, c, d) * .5 + c*.5 + b
-
-
-        
-    class Circ:
-        @staticmethod
-        def easeIn (t, b, c, d):
-            t = t / d
-            return -c * (math.sqrt(1 - t**2) - 1) + b
-
-        @staticmethod
-        def easeOut (t, b, c, d):
-            t = t / d - 1
-            return c * math.sqrt(1 - t**2) + b
-
-        @staticmethod
-        def easeInOut (t, b, c, d):
-            t = t / (d * 0.5)
-            if t < 1:
-                return -c * 0.5 * (math.sqrt(1 - t**2) - 1) + b
-            
-            t = t - 2
-            return c*0.5 * (math.sqrt(1 - t**2) + 1) + b
-
-
-    class Cubic:
-        @staticmethod
-        def easeIn (t, b, c, d):
-            t = t / d
-            return c * t**3 + b
-
-        @staticmethod
-        def easeOut (t, b, c, d):
-            t = t / d - 1
-            return c * (t**3 + 1) + b
-
-        @staticmethod
-        def easeInOut (t, b, c, d):
-            t = t / (d * 0.5)
-            if t < 1:
-                return c * 0.5 * t**3 + b
-            
-            t = t - 2
-            return c * 0.5 * (t**3 + 2) + b
-
-
-    class Elastic:
-        @staticmethod
-        def easeIn (t, b, c, d, a = 0, p = 0):
-            if t==0: return b
-
-            t = t / d            
-            if t == 1: return b+c
-            
-            if not p: p = d * .3;
-
-            if not a or a < abs(c):
-                a = c
-                s = p / 4
-            else:
-                s = p / (2 * math.pi) * math.asin(c / a)
-            
-            t = t - 1            
-            return - (a * math.pow(2, 10 * t) * math.sin((t*d-s) * (2 * math.pi) / p)) + b
-
-
-        @staticmethod
-        def easeOut (t, b, c, d, a = 0, p = 0):
-            if t == 0: return b
-            
-            t = t / d
-            if (t == 1): return b + c
-            
-            if not p: p = d * .3;
-
-            if not a or a < abs(c):
-                a = c
-                s = p / 4
-            else:
-                s = p / (2 * math.pi) * math.asin(c / a)
-                
-            return a * math.pow(2,-10 * t) * math.sin((t * d - s) * (2 * math.pi) / p) + c + b
-
-
-        @staticmethod
-        def easeInOut (t, b, c, d, a = 0, p = 0):
-            if t == 0: return b
-            
-            t = t / (d * 0.5)
-            if t == 2: return b + c
-            
-            if not p: p = d * (.3 * 1.5)
-
-            if not a or a < abs(c):
-                a = c
-                s = p / 4
-            else:
-                s = p / (2 * math.pi) * math.asin(c / a)
-                
-            if (t < 1):
-                t = t - 1
-                return -.5 * (a * math.pow(2, 10 * t) * math.sin((t * d - s) * (2 * math.pi) / p)) + b
-                
-            t = t - 1
-            return a * math.pow(2, -10 * t) * math.sin((t * d - s) * (2 * math.pi) / p) * .5 + c + b
-
-
-    class Expo:
-        @staticmethod
-        def easeIn(t, b, c, d):
-            if t == 0:
-                return b
-            else:
-                return c * math.pow(2, 10 * (t / d - 1)) + b - c * 0.001
-
-        @staticmethod
-        def easeOut(t, b, c, d):
-            if t == d:
-                return b + c
-            else:
-                return c * (-math.pow(2, -10 * t / d) + 1) + b
-
-        @staticmethod
-        def easeInOut(t, b, c, d):
-            if t==0:
-                return b
-            elif t==d:
-                return b+c
-
-            t = t / (d * 0.5)
-            
-            if t < 1:
-                return c * 0.5 * math.pow(2, 10 * (t - 1)) + b
-            
-            return c * 0.5 * (-math.pow(2, -10 * (t - 1)) + 2) + b
-
-
-    class Linear:
-        @staticmethod
-        def easeNone(t, b, c, d):
-            return c * t / d + b
-
-        @staticmethod
-        def easeIn(t, b, c, d):
-            return c * t / d + b
-
-        @staticmethod
-        def easeOut(t, b, c, d):
-            return c * t / d + b
-
-        @staticmethod
-        def easeInOut(t, b, c, d):
-            return c * t / d + b
-
-
-    class Quad:
-        @staticmethod
-        def easeIn (t, b, c, d):
-            t = t / d
-            return c * t**2 + b
-
-        @staticmethod
-        def easeOut (t, b, c, d):
-            t = t / d
-            return -c * t * (t-2) + b
-
-        @staticmethod
-        def easeInOut (t, b, c, d):
-            t = t / (d * 0.5)
-            if t < 1:
-                return c * 0.5 * t**2 + b
-            
-            t = t - 1
-            return -c * 0.5 * (t * (t - 2) - 1) + b
-
-
-    class Quart:
-        @staticmethod
-        def easeIn (t, b, c, d):
-            t = t / d
-            return c * t**4 + b
-
-        @staticmethod
-        def easeOut (t, b, c, d):
-            t = t / d - 1
-            return -c * (t**4 - 1) + b
-
-        @staticmethod
-        def easeInOut (t, b, c, d):
-            t = t / (d * 0.5)
-            if t < 1:
-                return c * 0.5 * t**4 + b
-            
-            t = t - 2
-            return -c * 0.5 * (t**4 - 2) + b
-
-    
-    class Quint:
-        @staticmethod
-        def easeIn (t, b, c, d):
-            t = t / d
-            return c * t**5 + b
-
-        @staticmethod
-        def easeOut (t, b, c, d):
-            t = t / d - 1
-            return c * (t**5 + 1) + b
-
-        @staticmethod
-        def easeInOut (t, b, c, d):
-            t = t / (d * 0.5)
-            if t < 1:
-                return c * 0.5 * t**5 + b
-            
-            t = t - 2
-            return c * 0.5 * (t**5 + 2) + b
-
-    class Sine:
-        @staticmethod
-        def easeIn (t, b, c, d):
-            return -c * math.cos(t / d * (math.pi / 2)) + c + b
-
-        @staticmethod
-        def easeOut (t, b, c, d):
-            return c * math.sin(t / d * (math.pi / 2)) + b
-
-        @staticmethod
-        def easeInOut (t, b, c, d):
-            return -c * 0.5 * (math.cos(math.pi * t / d) - 1) + b
-
-
-    class Strong:
-        @staticmethod
-        def easeIn(t, b, c, d):
-            return c * (t/d)**5 + b
-
-        @staticmethod
-        def easeOut(t, b, c, d):
-            return c * ((t / d - 1)**5 + 1) + b
-
-        @staticmethod
-        def easeInOut(t, b, c, d):
-            t = t / (d * 0.5)
-            
-            if t < 1:
-                return c * 0.5 * t**5 + b
-            
-            t = t - 2
-            return c * 0.5 * (t**5 + 2) + b
-
-
-
-class TweenTestObject:
-    def __init__(self):
-        self.pos = 20
-        self.rot = 50
- 
-    def update(self):
-        print self.pos, self.rot
- 
-    def setRotation(self, rot):
-        self.rot = rot
- 
-    def getRotation(self):
-        return self.rot
- 
-    def complete(self):
-        print "I'm done tweening now mommy!"
- 
- 
-if __name__=="__main__":
-    import time
-    T = Tweener()
-    tst = TweenTestObject()
-    mt = T.addTween( tst, setRotation=500.0, tweenTime=2.5, tweenType=T.OUT_EXPO, 
-                      pos=-200, tweenDelay=0.4, onCompleteFunction=tst.complete, 
-                      onUpdateFunction=tst.update )
-    s = time.clock()
-    changed = False
-    while T.hasTweens():
-        tm = time.clock()
-        d = tm - s
-        s = tm
-        T.update( d )
-        if mt.delta > 1.0 and not changed:
- 
-            tweenable = mt.getTweenable( "setRotation" )
- 
-            T.addTween( tweenable, change=-1000, tweenTime=0.7 )
-            T.addTween( mt, duration=-0.2, tweenTime=0.2 )
-            changed = True
-        #print mt.duration,
-        print tst.getRotation(), tst.pos
-        time.sleep(0.06)
-    print tst.getRotation(), tst.pos
+
+def inverse(method):
+    def real_inverse(t, *args, **kwargs):
+        t = 1 - t
+        return 1 - method(t, *args, **kwargs)
+    return real_inverse
+
+def symmetric(ease_in, ease_out):
+    def real_symmetric(t, *args, **kwargs):
+        if t < 0.5:
+            return ease_in(t * 2, *args, **kwargs) / 2
+
+        return ease_out((t - 0.5) * 2, *args, **kwargs) / 2 + 0.5
+    return real_symmetric
+
+class Symmetric(object):
+    def __init__(self, ease_in = None, ease_out = None):
+        self.ease_in = ease_in or inverse(ease_out)
+        self.ease_out = ease_out or inverse(ease_in)
+        self.ease_in_out = symmetric(self.ease_in, self.ease_out)
+
+
+class Easing(object):
+    """Class containing easing classes to use together with the tweener.
+       All of the classes have :func:`ease_in`, :func:`ease_out` and
+       :func:`ease_in_out` functions."""
+
+    Linear = Symmetric(lambda t: t, lambda t: t)
+    Quad = Symmetric(lambda t: t*t)
+    Cubic = Symmetric(lambda t: t*t*t)
+    Quart = Symmetric(lambda t: t*t*t*t)
+    Quint = Symmetric(lambda t: t*t*t*t*t)
+    Strong = Quint #oh i wonder why but the ported code is the same as in Quint
+
+    Circ = Symmetric(lambda t: 1 - math.sqrt(1 - t * t))
+    Sine = Symmetric(lambda t: 1 - math.cos(t * (math.pi / 2)))
+
+
+    def _back_in(t, s=1.70158):
+        return t * t * ((s + 1) * t - s)
+    Back = Symmetric(_back_in)
+
+
+    def _bounce_out(t):
+        if t < 1 / 2.75:
+            return 7.5625 * t * t
+        elif t < 2 / 2.75:
+            t = t - 1.5 / 2.75
+            return 7.5625 * t * t + 0.75
+        elif t < 2.5 / 2.75:
+            t = t - 2.25 / 2.75
+            return 7.5625 * t * t + .9375
+        else:
+            t = t - 2.625 / 2.75
+            return 7.5625 * t * t + 0.984375
+    Bounce = Symmetric(ease_out = _bounce_out)
+
+
+    def _elastic_in(t, springiness = 0, wave_length = 0):
+        if t in(0, 1):
+            return t
+
+        wave_length = wave_length or (1 - t) * 0.3
+
+        if springiness <= 1:
+            springiness = t
+            s = wave_length / 4
+        else:
+            s = wave_length / (2 * math.pi) * math.asin(t / springiness)
+
+        t = t - 1
+        return -(springiness * math.pow(2, 10 * t) * math.sin((t * t - s) * (2 * math.pi) / wave_length))
+    Elastic = Symmetric(_elastic_in)
+
+
+    def _expo_in(t):
+        if t in (0, 1): return t
+        return math.pow(2, 10 * t) * 0.001
+    Expo = Symmetric(_expo_in)
+
+
+
+class _Dummy(object):
+    def __init__(self, a, b, c):
+        self.a = a
+        self.b = b
+        self.c = c
+
+if __name__ == "__main__":
+    import datetime as dt
+
+    tweener = Tweener()
+    objects = []
+
+    for i in range(10000):
+        objects.append(_Dummy(dt.datetime.now(), i-100, i-100))
+
+
+    total = dt.datetime.now()
+
+    t = dt.datetime.now()
+    print "Adding 10000 objects..."
+    for i, o in enumerate(objects):
+        tweener.add_tween(o, a = dt.datetime.now() - dt.timedelta(days=3),
+                             b = i,
+                             c = i,
+                             duration = 1.0,
+                             easing=Easing.Circ.ease_in_out)
+    print dt.datetime.now() - t
+
+    t = dt.datetime.now()
+    print "Updating 10 times......"
+    for i in range(11):  #update 1000 times
+        tweener.update(0.1)
+    print dt.datetime.now() - t



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]