[hamster-applet] updated to the most recent hamster graphics. work in progress; breaks interactivity, might be other



commit a0cfc6486883fcc917e28834f225b170019e23c0
Author: Toms Bauģis <toms baugis gmail com>
Date:   Mon Apr 5 17:24:02 2010 +0100

    updated to the most recent hamster graphics. work in progress; breaks interactivity, might be other breakages.

 src/hamster/charting.py          |  149 ++++---
 src/hamster/graphics.py          |  949 +++++++++++++++++++++++++++-----------
 src/hamster/pytweener.py         |  287 ++++++-------
 src/hamster/stats.py             |    6 +-
 src/hamster/widgets/dayline.py   |   10 +-
 src/hamster/widgets/tags.py      |   14 +-
 src/hamster/widgets/timechart.py |   63 ++--
 7 files changed, 947 insertions(+), 531 deletions(-)
---
diff --git a/src/hamster/charting.py b/src/hamster/charting.py
index 24ecd60..446dc00 100644
--- a/src/hamster/charting.py
+++ b/src/hamster/charting.py
@@ -86,7 +86,7 @@ class Bar(object):
         return str((self.value, self.size))
 
 
-class Chart(graphics.Area):
+class Chart(graphics.Scene):
     """Chart constructor. Optional arguments:
         self.max_bar_width     = pixels. Maximal width of bar. If not specified,
                                  bars will stretch to fill whole area
@@ -119,7 +119,7 @@ class Chart(graphics.Area):
         "bar-clicked": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
     }
     def __init__(self, **args):
-        graphics.Area.__init__(self)
+        graphics.Scene.__init__(self)
 
         # options
         self.max_bar_width     = args.get("max_bar_width", 500)
@@ -137,7 +137,6 @@ class Chart(graphics.Area):
 
         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
 
@@ -157,8 +156,10 @@ class Chart(graphics.Area):
 
         self.mouse_bar = None
         if self.interactive:
-            self.connect("mouse-over", self.on_mouse_over)
-            self.connect("button-release", self.on_clicked)
+            self.connect("on-mouse-over", self.on_mouse_over)
+            self.connect("on-click", self.on_clicked)
+
+        self.connect("on-enter-frame", self.on_enter_frame)
 
         self.bars_selected = []
 
@@ -169,7 +170,7 @@ class Chart(graphics.Area):
         else:
             self.mouse_bar = None
 
-        self.redraw_canvas()
+        self.redraw()
 
     def on_clicked(self, area, bar):
         self.emit("bar-clicked", self.mouse_bar)
@@ -191,11 +192,6 @@ class Chart(graphics.Area):
                                    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"""
@@ -204,7 +200,7 @@ class Chart(graphics.Area):
         self.show()
 
         if not data: #if there is no data, just draw blank
-            self.redraw_canvas()
+            self.redraw()
             return
 
 
@@ -215,13 +211,14 @@ class Chart(graphics.Area):
         if not self.animation:
             self.tweener.finish()
 
-        self.redraw_canvas()
+        self.redraw()
 
 
-    def on_expose(self):
+    def on_enter_frame(self, scene, context):
         # fill whole area
         if self.background:
-            self.fill_area(0, 0, self.width, self.height, self.background)
+            g = graphics.Graphics(context)
+            g.fill_area(0, 0, self.width, self.height, self.background)
 
 
     def _update_targets(self):
@@ -240,9 +237,9 @@ class Chart(graphics.Area):
                         bars[i] = Bar(new_values[i], 0)
                     else:
                         bars[i].value = new_values[i]
-                        self.tweener.killTweensOf(bars[i])
+                        self.tweener.kill_tweens(bars[i])
 
-                    self.tweener.addTween(bars[i], size = bars[i].value / float(max_value))
+                    self.tweener.add_tween(bars[i], size = bars[i].value / float(max_value))
             return bars
 
         retarget(self.bars, self.data)
@@ -263,13 +260,20 @@ class Chart(graphics.Area):
 
 
 class BarChart(Chart):
-    def on_expose(self):
-        Chart.on_expose(self)
+    def on_enter_frame(self, scene, context):
+        Chart.on_enter_frame(self, scene, context)
 
         if not self.data:
             return
 
-        context = self.context
+        g = graphics.Graphics(context)
+
+        # TODO - should handle the layout business in graphics
+        self.layout = context.create_layout()
+        default_font = pango.FontDescription(gtk.Style().font_desc.to_string())
+        default_font.set_size(8 * pango.SCALE)
+        self.layout.set_font_description(default_font)
+
         context.set_line_width(1)
 
 
@@ -299,11 +303,11 @@ class BarChart(Chart):
         self.graph_height = self.height - 15
 
         if self.chart_background:
-            self.fill_area(self.graph_x, self.graph_y,
+            g.fill_area(self.graph_x, self.graph_y,
                            self.graph_width, self.graph_height,
                            self.chart_background)
 
-        self.context.stroke()
+        g.stroke()
 
         # bars and keys
         max_bar_size = self.graph_height
@@ -335,14 +339,14 @@ class BarChart(Chart):
 
 
         for key, bar, data in zip(self.keys, self.bars, self.data):
-            self.set_color(label_color);
+            g.set_color(label_color);
             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)
+                g.move_to(intended_x, self.graph_height + 4)
                 context.show_layout(self.layout)
 
                 prev_label_end = intended_x + label_w + 3
@@ -372,17 +376,17 @@ class BarChart(Chart):
                         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)
+                        g.fill_area(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
 
                 last_color = self.key_colors.get(key) or base_color
-                self.draw_bar(exes[key][0],
+                g.fill_area(exes[key][0],
                               self.graph_y + self.graph_height - bar_size,
                               exes[key][1],
                               bar_size,
@@ -410,9 +414,9 @@ class BarChart(Chart):
 
                 # we are in the bar so make sure that the font color is distinguishable
                 if self.colors.is_light(last_color):
-                    self.set_color(label_color)
+                    g.set_color(label_color)
                 else:
-                    self.set_color(self.colors.almost_white)
+                    g.set_color(self.colors.almost_white)
 
                 context.show_layout(self.layout)
 
@@ -422,7 +426,7 @@ class BarChart(Chart):
             grid_color = self.background
         else:
             grid_color = self.get_style().bg[gtk.STATE_NORMAL].to_string()
-            
+
         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
@@ -440,18 +444,18 @@ class BarChart(Chart):
                     label_w, label_h = self.layout.get_pixel_size()
                     context.move_to(legend_width - label_w - 8,
                                     y - label_h / 2)
-                    self.set_color(self.colors.aluminium[4])
+                    g.set_color(self.colors.aluminium[4])
                     context.show_layout(self.layout)
 
-                self.set_color(grid_color)
-                self.context.move_to(legend_width, y)
-                self.context.line_to(self.width, y)
+                g.set_color(grid_color)
+                g.move_to(legend_width, y)
+                g.line_to(self.width, y)
 
 
         #stack keys
         if self.show_stack_labels:
             #put series keys
-            self.set_color(label_color);
+            g.set_color(label_color);
 
             y = self.graph_height
             label_y = None
@@ -512,13 +516,13 @@ class BarChart(Chart):
 
 
 class HorizontalBarChart(Chart):
-    def on_expose(self):
-        Chart.on_expose(self)
+    def on_enter_frame(self, scene, context):
+        Chart.on_enter_frame(self, scene, context)
+        g = graphics.Graphics(context)
 
         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
@@ -532,7 +536,7 @@ class HorizontalBarChart(Chart):
 
 
         if self.chart_background:
-            self.fill_area(self.graph_x, self.graph_y, self.graph_width, self.graph_height, self.chart_background)
+            g.fill_area(self.graph_x, self.graph_y, self.graph_width, self.graph_height, self.chart_background)
 
 
         if not self.data:  # go home if we have nothing
@@ -551,6 +555,13 @@ class HorizontalBarChart(Chart):
 
         max_bar_size = self.graph_width - 15
 
+        # TODO - should handle the layout business in graphics
+        self.layout = context.create_layout()
+        default_font = pango.FontDescription(gtk.Style().font_desc.to_string())
+        default_font.set_size(8 * pango.SCALE)
+        self.layout.set_font_description(default_font)
+
+
         self.layout.set_alignment(pango.ALIGN_RIGHT)
         self.layout.set_ellipsize(pango.ELLIPSIZE_END)
 
@@ -583,7 +594,7 @@ class HorizontalBarChart(Chart):
 
 
         for i, label in enumerate(keys):
-            if self.interactive:
+            if 1 == 2 and self.interactive: # TODO - put interaction back
                 self.register_mouse_region(0,
                                            positions[label][0],
                                            self.width,
@@ -596,9 +607,9 @@ class HorizontalBarChart(Chart):
             label_y = positions[label][0] + (positions[label][1] - label_h) / 2
 
             if i == self.mouse_bar:
-                self.set_color(self.get_style().fg[gtk.STATE_PRELIGHT])
+                g.set_color(self.get_style().fg[gtk.STATE_PRELIGHT])
             else:
-                self.set_color(label_color)
+                g.set_color(label_color)
 
 
             context.move_to(0, label_y)
@@ -617,7 +628,7 @@ class HorizontalBarChart(Chart):
                         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,
+                        g.fill_area(self.graph_x + bar_start,
                                       positions[label][0],
                                       bar_size,
                                       positions[label][1],
@@ -634,7 +645,7 @@ class HorizontalBarChart(Chart):
                 else:
                     last_color = self.key_colors.get(self.keys[i]) or base_color
 
-                self.draw_bar(self.graph_x,
+                g.fill_area(self.graph_x,
                               positions[label][0],
                               bar_size,
                               positions[label][1],
@@ -659,26 +670,26 @@ class HorizontalBarChart(Chart):
 
                 # avoid zero selected bars without any hints
                 if not self.stack_keys and i in self.bars_selected and self.bars[i].value == 0:
-                    self.set_color(self.get_style().bg[gtk.STATE_SELECTED])
+                    g.set_color(self.get_style().bg[gtk.STATE_SELECTED])
                     self.draw_rect(label_x - 2,
                                    label_y - 2,
                                    label_w + 4,
                                    label_h + 4, 4)
-                    self.context.fill()
-                    self.set_color(self.get_style().fg[gtk.STATE_SELECTED])
+                    g.fill()
+                    g.set_color(self.get_style().fg[gtk.STATE_SELECTED])
                 else:
-                    self.set_color(label_color)
+                    g.set_color(label_color)
             else:
                 label_x = self.graph_x + bar_start - label_w - vertical_padding
 
                 if i in self.bars_selected:
-                    self.set_color(self.get_style().fg[gtk.STATE_SELECTED].to_string())
+                    g.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 self.colors.is_light(last_color):
-                        self.set_color(label_color)
+                        g.set_color(label_color)
                     else:
-                        self.set_color(self.colors.almost_white)
+                        g.set_color(self.colors.almost_white)
 
 
             context.move_to(label_x, label_y)
@@ -700,12 +711,10 @@ class HorizontalDayChart(Chart):
         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
+        self.redraw()
 
-        Chart.on_expose(self)
+    def on_enter_frame(self, scene, context):
+        Chart.on_enter_frame(self, scene, context)
         rowcount, keys = len(self.keys), self.keys
 
         start_hour = 0
@@ -732,7 +741,7 @@ class HorizontalDayChart(Chart):
 
 
         if self.chart_background:
-            self.fill_area(self.graph_x, self.graph_y, self.graph_width, self.graph_height, self.chart_background)
+            g.fill_area(self.graph_x, self.graph_y, self.graph_width, self.graph_height, self.chart_background)
 
         if not self.data:  #if we have nothing, let's go home
             return
@@ -780,7 +789,7 @@ class HorizontalDayChart(Chart):
                 tick_color = self.colors.darker(bg_color,  -50)
 
         for i, label in enumerate(keys):
-            self.set_color(label_color)
+            g.set_color(label_color)
 
             self.layout.set_text(label)
             label_w, label_h = self.layout.get_pixel_size()
@@ -795,7 +804,7 @@ class HorizontalDayChart(Chart):
                 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],
@@ -808,13 +817,13 @@ class HorizontalDayChart(Chart):
 
         pace = ((end_hour - start_hour) / 3) / 60 * 60
         last_position = positions[keys[-1]]
-        
-        
+
+
         if self.background:
             grid_color = self.background
         else:
             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)
 
@@ -825,13 +834,13 @@ class HorizontalDayChart(Chart):
 
             context.move_to(self.graph_x + x - label_w / 2,
                             last_position[0] + last_position[1] + 4)
-            self.set_color(self.colors.aluminium[4])
+            g.set_color(self.colors.aluminium[4])
             context.show_layout(self.layout)
 
 
-            self.set_color(grid_color)
-            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,
+            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])
 
 
@@ -856,7 +865,7 @@ class BasicWindow:
 
 
         self.series = ["One", "Two", "Three", "Four", "Five", "Six", "Seven"]
-        self.stacks = ["x", "y", "z", "a", "b", "c", "d"]
+        self.stacks = ["x"]
         self.stack_colors = dict([(stack, None) for stack in self.stacks])
 
         import random
diff --git a/src/hamster/graphics.py b/src/hamster/graphics.py
index 6d32834..d79463d 100644
--- a/src/hamster/graphics.py
+++ b/src/hamster/graphics.py
@@ -1,36 +1,24 @@
 # - 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 pytweener
-from pytweener import Easing
+try:
+    import pytweener
+except: # we can also live without tweener. Scene.animate won't work in this case
+    pytweener = None
+
 import colorsys
+from collections import deque
 
 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)
-
     def parse(self, color):
         assert color is not None
 
@@ -62,59 +50,545 @@ class Colors(object):
         # 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])
+Colors = Colors() # this is a static class, so an instance will do -- TODO - could be bad practice
+
+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://www.cairographics.org/documentation/pycairo/reference/context.html#class-context
+       for detailed description of the cairo drawing functions.
+    """
+    def __init__(self, context = None):
+        self._instructions = deque() # instruction set until it is converted into path-based instructions
+        self.instructions = [] # paths colors and operations
+        self.colors = Colors
+        self.extents = None
+        self.opacity = 1.0
+        self.paths = None
+        self.last_matrix = None
+        self.context = context
+
+    def clear(self):
+        """clear all instructions"""
+        self._instructions = deque()
+        self.paths = []
+
+    def _stroke(self, context): context.stroke()
+    def stroke(self, color = None, alpha = 1):
+        """stroke the line with given color and opacity"""
+        if color or alpha < 1:self.set_color(color, alpha)
+        self._add_instruction(self._stroke,)
+
+    def _fill(self, context): context.fill()
+    def fill(self, color = None, alpha = 1):
+        """fill path with given color and opacity"""
+        if color or alpha < 1:self.set_color(color, alpha)
+        self._add_instruction(self._fill,)
+
+    def _stroke_preserve(self, context): context.stroke_preserve()
+    def stroke_preserve(self, color = None, alpha = 1):
+        """same as stroke, only after stroking, don't discard the path"""
+        if color or alpha < 1:self.set_color(color, alpha)
+        self._add_instruction(self._stroke_preserve,)
+
+    def _fill_preserve(self, context): context.fill_preserve()
+    def fill_preserve(self, color = None, alpha = 1):
+        """same as fill, only after filling, don't discard the path"""
+        if color or alpha < 1:self.set_color(color, alpha)
+        self._add_instruction(self._fill_preserve,)
+
+    def _new_path(self, context): context.new_path()
+    def new_path(self):
+        """discard current path"""
+        self._add_instruction(self._new_path,)
+
+    def _paint(self, context): context.paint()
+    def paint(self):
+        """errrm. paint"""
+        self._add_instruction(self._paint,)
+
+    def _set_source_surface(self, context, image, x, y): context.set_source_surface(image, x, y)
+    def set_source_surface(self, image, x = 0, y = 0): self._add_instruction(self._set_source_surface, image, x, y)
+
+    def _move_to(self, context, x, y): context.move_to(x, y)
+    def move_to(self, x, y):
+        """change current position"""
+        self._add_instruction(self._move_to, x, y)
+
+    def _line_to(self, context, x, y): context.line_to(x, y)
+    def line_to(self, x, y):
+        """draw line"""
+        self._add_instruction(self._line_to, x, y)
+
+    def _curve_to(self, 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 curve. (x2, y2) is the middle point of the curve"""
+        self._add_instruction(self._curve_to, x, y, x2, y2, x3, y3)
+
+    def _close_path(self, context): context.close_path()
+    def close_path(self):
+        """connect end with beginning of path"""
+        self._add_instruction(self._close_path,)
+
+    def _set_line_width(self, context, width):
+        context.set_line_width(width)
+    def set_line_style(self, width = None):
+        """change the width of the line"""
+        if width is not None:
+            self._add_instruction(self._set_line_width, width)
+
+    def _set_color(self, context, r, g, b, a):
+        if a * self.opacity >= 1:
+            context.set_source_rgb(r, g, b)
+        else:
+            context.set_source_rgba(r, g, b, a * self.opacity)
+
+    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"""
+        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)
+
+    def _arc(self, 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):
+        """draw 'perfect' ellipse, opposed to squashed circle. works also for
+           equilateral polygons"""
+        steps = edges or max((32, width, height)) / 3 # the automatic edge case is somewhat arbitrary
+
+        angle = 0
+        step = math.pi * 2 / steps
+        points = []
+        while angle < math.pi * 2:
+            points.append((self.width / 2.0 * math.cos(angle),
+                           self.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, points[0][1] - min_y)
+        for x, y in points:
+            self._line_to(x - min_x, y - min_y)
+        self._line_to(points[0][0] - min_x, points[0][1] - min_y)
+
+
+    def _arc_negative(self, 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)
+
+    def _rounded_rectangle(self, 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)
+
+    def _rectangle(self, 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 _show_layout(self, context, text, font_desc):
+        layout = context.create_layout()
+        layout.set_font_description(font_desc)
+        layout.set_text(text)
+        context.show_layout(layout)
+
+    def show_text(self, text):
+        """display text with system's default font"""
+        font_desc = pango.FontDescription(gtk.Style().font_desc.to_string())
+        self.show_layout(text, font_desc)
+
+    def show_layout(self, text, font_desc):
+        """display text. font_desc is string of pango font description
+           often handier than calling this function directly, is to create
+           a class:Label object
+        """
+        self._add_instruction(self._show_layout, text, font_desc)
+
+    def _remember_path(self, context):
+        context.save()
+        context.identity_matrix()
+        matrix = context.get_matrix()
+
+        new_extents = context.path_extents()
+        self.extents = self.extents or new_extents
+        self.extents = (min(self.extents[0], new_extents[0]),
+                        min(self.extents[1], new_extents[1]),
+                        max(self.extents[2], new_extents[2]),
+                        max(self.extents[3], new_extents[3]))
+
+        self.paths.append(context.copy_path_flat())
+
+        context.restore()
+
+
+    def _add_instruction(self, function, *params):
+        if self.context:
+            function(self.context, *params)
+        else:
+            self.paths = None
+            self._instructions.append((function, params))
+
+
+    def _draw(self, context, with_extents = False):
+        """draw accumulated instructions in context"""
+
+        if self._instructions: #new stuff!
+            self.instructions = deque()
+            current_color = None
+            current_line = None
+            instruction_cache = []
+
+            while self._instructions:
+                instruction, args = self._instructions.popleft()
+
+                if instruction in (self._set_source_surface, self._paint):
+                    self.instructions.append((None, None, None, instruction, args))
+
+                elif instruction == self._show_layout:
+                    x,y = context.get_current_point() or (0,0)
+                    self.instructions.append((None, None, None, self._move_to, (x,y))) #previous move_to call will be actually executed after this
+                    self.instructions.append((None, current_color, None, instruction, args))
+
+                else:
+                    if instruction == self._set_color:
+                        current_color = args
+
+                    if instruction == self._set_line_width:
+                        current_line = args
+
+                    elif instruction in (self._stroke, self._fill, self._stroke_preserve, self._fill_preserve):
+                        self.instructions.append((context.copy_path(), current_color, current_line, instruction, ()))
+                        context.new_path() # reset even on preserve as the instruction will preserve it instead
+                        instruction_cache = []
+                    else:
+                        instruction(context, *args)
+                        instruction_cache.append((instruction, args))
+
+
+                while instruction_cache: # stroke's missing so we just cache
+                    instruction, args = instruction_cache.pop(0)
+                    self.instructions.append((None, None, None, instruction, args))
+
+
+        # if we have been moved around, we should update bounds
+        check_extents = with_extents and context.get_matrix() != self.last_matrix
+        if check_extents:
+            self.paths = deque()
+            self.extents = None
+
+        for path, color, line, instruction, args in self.instructions:
+            if color: self._set_color(context, *color)
+            if line: self._set_line_width(context, *line)
+
+            if path:
+                context.append_path(path)
+                if check_extents:
+                    self._remember_path(context)
+
+            if instruction:
+                instruction(context, *args)
+
+        self.last_matrix = context.get_matrix()
+
+
+
+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-click": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+        "on-drag": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+        #"on-draw": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+    }
+    def __init__(self, x = 0, y = 0, opacity = 1, visible = True, rotation = 0, pivot_x = 0, pivot_y = 0, interactive = True, draggable = False):
+        gtk.Widget.__init__(self)
+        self.child_sprites = []
+        self.graphics = Graphics()
+        self.interactive = interactive
+        self.draggable = draggable
+        self.pivot_x, self.pivot_y = pivot_x, pivot_y # rotation point in sprite's coordinates
+        self.opacity = opacity
+        self.visible = visible
+        self.parent = None
+        self.x, self.y = x, y
+        self.rotation = rotation
+
+    def add_child(self, *sprites):
+        """Add child sprite. Child will be nested within parent"""
+        for sprite in sprites:
+            self.child_sprites.append(sprite)
+            sprite.parent = self
+
+    def _draw(self, context, opacity = 1):
+        if self.visible is False:
+            return
+
+        if self.x or self.y or self.rotation:
+            context.save()
+
+            if self.x or self.y or self.pivot_x or self.pivot_y:
+                context.translate(self.x + self.pivot_x, self.y + self.pivot_y)
+
+            if self.rotation:
+                context.rotate(self.rotation)
+
+            if self.pivot_x or self.pivot_y:
+                context.translate(-self.pivot_x, -self.pivot_y)
+
+        self.graphics.opacity = self.opacity * opacity
+
+        #self.emit("on-draw") # TODO - this is expensive when doing constant redraw with many frames. maybe we can have a simple callback here?
+        self.graphics._draw(context, self.interactive or self.draggable)
+
+        for sprite in self.child_sprites:
+            sprite._draw(context, self.opacity * opacity)
+
+        if self.x or self.y or self.rotation:
+            context.restore()
+
+    def _on_click(self, button_state):
+        self.emit("on-click", button_state)
+
+    def _on_mouse_over(self):
+        # scene will call us when there is mouse
+        self.emit("on-mouse-over")
+
+    def _on_mouse_out(self):
+        # scene will call us when there is mouse
+        self.emit("on-mouse-out")
+
+    def _on_drag(self, x, y):
+        # scene will call us when there is mouse
+        self.emit("on-drag", (x, y))
+
+
+"""a few shapes"""
+class Label(Sprite):
+    def __init__(self, text = "", size = 10, color = None, **kwargs):
+        kwargs.setdefault('interactive', False)
+        Sprite.__init__(self, **kwargs)
+        self.text = text
+        self.color = color
+        self.width, self.height = None, None
+
+        self.font_desc = pango.FontDescription(gtk.Style().font_desc.to_string())
+        self.font_desc.set_size(size * pango.SCALE)
+        self._draw_label()
+
+    def _draw_label(self):
+        self._set_dimensions()
+        self.graphics.move_to(0, 0) #make sure we don't wander off somewhere nowhere
+
+        if self.color:
+            self.graphics.set_color(self.color)
+        self.graphics.show_layout(self.text, self.font_desc)
+
+        if self.interactive: #if label is interactive, draw invisible bounding box for simple hit calculations
+            self.graphics.set_color("#000", 0)
+            self.graphics.rectangle(0,0, self.width, self.height)
+            self.graphics.stroke()
+
+
+    def _set_dimensions(self):
+        context = gtk.gdk.CairoContext(cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, 500, 2000)))
+        layout = context.create_layout()
+        layout.set_font_description(self.font_desc)
+        layout.set_text(self.text)
+
+        self.width, self.height = layout.get_pixel_size()
+
+
+class Shape(Sprite):
+    """shape is a simple continuous shape that can have fill and stroke"""
+    def __init__(self, stroke = None, fill = None, line_width = None, **kwargs):
+        kwargs.setdefault("interactive", False)
+        Sprite.__init__(self, **kwargs)
+        self.stroke = stroke # stroke color
+        self.fill = fill     # fill color
+        self.line_width = line_width
+        self._sprite_dirty = True # a dirty shape needs it's graphics regenerated, because params have changed
+
+    def __setattr__(self, name, val):
+        self.__dict__[name] = val
+        if name not in ('_sprite_dirty', 'x', 'y', 'rotation'):
+            self._sprite_dirty = True
+
+
+    def _draw(self, *args, **kwargs):
+        if self._sprite_dirty:
+            self.graphics.clear()
+            self.draw_shape()
+            self._color()
+            self._sprite_dirty = False
+
+        Sprite._draw(self, *args,  **kwargs)
+
 
+    def draw_shape(self):
+        """implement this function in your subclassed object. leave out stroke
+        and fill instructions - those will be performed by the shape itself, using
+        the stroke and fill attributes"""
+        raise(NotImplementedError, "expected draw_shape functoin in the class")
+
+    def _color(self):
+        if self.line_width:
+            self.graphics.set_line_style(self.line_width)
+
+        if self.fill:
+            if self.stroke:
+                self.graphics.fill_preserve(self.fill)
+            else:
+                self.graphics.fill(self.fill)
+
+        if self.stroke:
+            self.graphics.stroke(self.stroke)
+
+
+
+class Rectangle(Shape):
+    def __init__(self, w, h, corner_radius = 0, **kwargs):
+        self.width, self.height, self.corner_radius = w, h, corner_radius
+        Shape.__init__(self, **kwargs)
+
+    def draw_shape(self):
+        self.graphics.rectangle(0, 0, self.width, self.height, self.corner_radius)
+
+
+class Polygon(Shape):
+    def __init__(self, points, **kwargs):
+        self.points = points
+        Shape.__init__(self, **kwargs)
+
+    def draw_shape(self):
+        if not self.points: return
+
+        self.graphics.move_to(*self.points[0])
+        for point in self.points:
+            self.graphics.line_to(*point)
+        self.graphics.close_path()
+
+class Circle(Shape):
+    def __init__(self, radius, **kwargs):
+        self.radius = radius
+        Shape.__init__(self, **kwargs)
+
+    def draw_shape(self):
+        self.graphics.move_to(self.radius * 2, self.radius)
+        self.graphics.arc(self.radius, self.radius, self.radius, 0, math.pi * 2)
+
+
+
+class Scene(gtk.DrawingArea):
+    """ Widget 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-press": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
-        "button-release": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
-        "mouse-move": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
-        "mouse-click": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, 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-mouse-move": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+        "on-mouse-up": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+        "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,)),
     }
 
-    def __init__(self):
+    def __init__(self, interactive = True, framerate = 80):
         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_press_event", self.__on_button_press)
-        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
+        if interactive:
+            self.set_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
+            self.connect("motion_notify_event", self.__on_mouse_move)
+            self.connect("button_press_event", self.__on_button_press)
+            self.connect("button_release_event", self.__on_button_release)
+
+        self.sprites = []
+        self.framerate = framerate # frame rate
+
         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
+        if pytweener:
+            self.tweener = pytweener.Tweener(0.4, pytweener.Easing.Cubic.ease_in_out)
+
+        self.colors = Colors
+
         self.__drawing_queued = False
 
-        self.mouse_drag = None
+        self._last_frame_time = None
+        self._mouse_sprites = set()
+        self._mouse_drag = None
+        self._drag_sprite = None
+        self._drag_x, self._drag_y = None, None
+        self._button_press_time = None # to distinguish between click and drag
+        self.mouse_x, self.mouse_y = None, None
+
+        self._debug_bounds = False
+
 
-        self.colors = Colors() # handier this way
+    def add_child(self, *sprites):
+        """Add one or several :class:`graphics.Sprite` sprites to scene """
+        for sprite in sprites:
+            self.sprites.append(sprite)
 
-    def on_expose(self):
-        """ on_expose event is where you hook in all your drawing
-            canvas has been initialized for you """
-        raise NotImplementedError
+    def clear(self):
+        """Remove all sprites from scene"""
+        self.sprites = []
 
-    def redraw_canvas(self):
-        """Redraw canvas. Triggers also to do all animations"""
+    def redraw(self):
+        """Queue redraw. The redraw will be performed not more often than
+           the `framerate` allows"""
         if not self.__drawing_queued: #if we are moving, then there is a timeout somewhere already
             self.__drawing_queued = True
-            self.last_frame_time = dt.datetime.now()
+            self._last_frame_time = dt.datetime.now()
             gobject.timeout_add(1000 / self.framerate, self.__interpolate)
 
     """ animation bits """
@@ -122,93 +596,39 @@ class Area(gtk.DrawingArea):
         if not self.window: #will wait until window comes
             return True
 
+        time_since_last_frame = (dt.datetime.now() - self._last_frame_time).microseconds / 1000000.0
+        if pytweener:
+            self.tweener.update(time_since_last_frame)
 
-        time_since_last_frame = (dt.datetime.now() - self.last_frame_time).microseconds / 1000000.0
-        self.tweener.update(time_since_last_frame)
-        self.__drawing_queued = self.tweener.hasTweens()
+        self.__drawing_queued = pytweener and self.tweener.has_tweens()
 
         self.queue_draw() # this will trigger do_expose_event when the current events have been flushed
 
-        self.last_frame_time = dt.datetime.now()
+        self._last_frame_time = dt.datetime.now()
         return self.__drawing_queued
 
 
-    def animate(self, object, params = {}, duration = None, easing = None, callback = None, instant = True):
-        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)
-
-        if instant:
-            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)
-            return
-
-        # make sure that w + h are larger than 2 * corner_radius
-        corner_radius = min(corner_radius, min(w, h) / 2)
-
-        x2, y2 = x + w, y + h
-
-        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 = self.colors.parse(color) #parse whatever we have there into a normalized triplet
+    def animate(self, sprite, instant = True, duration = None, easing = None, on_complete = None, on_update = None, delay = 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).
 
-        if opacity:
-            self.context.set_source_rgba(color[0], color[1], color[2], opacity)
-        elif len(color) == 3:
-            self.context.set_source_rgb(*color)
-        else:
-            self.context.set_source_rgba(*color)
+           By default redraw is requested right after creating the animation.
+           If you would like to add several tweens and only then redraw,
+           set `instant` to False.
+           Example::
+             # tween some_sprite to coordinates (50,100) using default duration and easing
+             scene.animate(some_sprite, x = 50, y = 100)
+        """
+        if not pytweener: # here we complain
+            raise Exception("pytweener not found. Include it to enable animations")
 
+        self.tweener.add_tween(sprite, duration = duration, easing = easing, on_complete = on_complete, on_update = on_update, delay = delay, **kwargs)
 
-    def register_mouse_region(self, x1, y1, x2, y2, region_name):
-        self.mouse_regions.append((x1, y1, x2, y2, region_name))
+        if instant:
+            self.redraw()
 
     """ exposure events """
     def do_configure_event(self, event):
@@ -216,181 +636,184 @@ class Area(gtk.DrawingArea):
 
     def do_expose_event(self, event):
         self.width, self.height = self.window.get_size()
-        self.context = self.window.cairo_create()
+        context = self.window.cairo_create()
 
-        self.context.rectangle(event.area.x, event.area.y,
-                               event.area.width, event.area.height)
-        self.context.clip()
+        context.rectangle(event.area.x, event.area.y,
+                          event.area.width, event.area.height)
+        context.clip()
 
-        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()
+        self.emit("on-enter-frame", context)
 
+        for sprite in self.sprites:
+            sprite._draw(context)
 
-    """ mouse events """
-    def __on_mouse_move(self, area, event):
-        if event.is_hint:
-            x, y, state = event.window.get_pointer()
-        else:
-            x = event.x
-            y = event.y
-            state = event.state
+        self._check_mouse(self.mouse_x, self.mouse_y)
 
-        self.emit("mouse-move", (x, y), state)
 
-        if not self.mouse_regions:
-            return
+        if self._debug_bounds:
+            context.set_line_width(1)
+            context.set_source_rgb(.2, .2, .5)
+            for sprite in self.all_sprites():
+                if sprite.graphics.extents:
+                    x,y,x2,y2 = sprite.graphics.extents
+                    context.rectangle(x, y, x2-x, y2-y)
+            context.stroke()
 
-        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))
 
-        if mouse_regions != self.__prev_mouse_regions:
-            self.emit("mouse-over", mouse_regions)
+        self.emit("on-finish-frame", context)
 
-        self.__prev_mouse_regions = mouse_regions
 
-    def __on_mouse_out(self, area, event):
-        self.__prev_mouse_regions = None
-        self.emit("mouse-over", [])
+    """ mouse events """
+    def all_sprites(self, sprites = None):
+        """returns flat list of the sprite tree for simplified iteration"""
 
+        if sprites is None:
+            sprites = self.sprites
 
-    def __on_button_press(self, area, event):
-        x = event.x
-        y = event.y
-        state = event.state
-        self.mouse_drag = (x, y)
+        for sprite in sprites:
+            yield sprite
+            if sprite.child_sprites:
+                for child in self.all_sprites(sprite.child_sprites):
+                    yield child
 
-        if not self.mouse_regions:
-            return
-        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])
+    def __on_mouse_move(self, area, event):
+        if event.is_hint:
+            mouse_x, mouse_y, state = event.window.get_pointer()
+        else:
+            mouse_x = event.x
+            mouse_y = event.y
+            state = event.state
 
-        if mouse_regions:
-            self.emit("button-press", mouse_regions)
+        self.mouse_x, self.mouse_y = mouse_x, mouse_y
 
 
-    def __on_button_release(self, area, event):
-        x = event.x
-        y = event.y
-        state = event.state
+        if self._drag_sprite and self._drag_sprite.draggable and gtk.gdk.BUTTON1_MASK & event.state:
+            self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
 
-        click = False
-        drag_distance = 5
-        if self.mouse_drag and (self.mouse_drag[0] - x) ** 2 + (self.mouse_drag[1] - y) ** 2 < drag_distance ** 2:
-            #if the drag is less than the drag distance, then we have a click
-            click =  True
-        self.mouse_drag = None
+            # dragging around
+            drag = self._mouse_drag and (self._mouse_drag[0] - event.x) ** 2 + \
+                                        (self._mouse_drag[1] - event.y) ** 2 > 5 ** 2
+            if drag:
+                matrix = cairo.Matrix()
+                if self._drag_sprite.parent:
+                    # TODO - this currently works only until second level - take all parents into account
+                    matrix.rotate(self._drag_sprite.parent.rotation)
+                    matrix.invert()
 
-        if not self.mouse_regions:
-            self.emit("mouse-click", (x,y), [])
-            return
+                if not self._drag_x:
+                    x1,y1 = matrix.transform_point(self._mouse_drag[0], self._mouse_drag[1])
 
-        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])
+                    self._drag_x = self._drag_sprite.x - x1
+                    self._drag_y = self._drag_sprite.y - y1
 
-        if mouse_regions:
-            self.emit("button-release", mouse_regions)
+                mouse_x, mouse_y = matrix.transform_point(mouse_x, mouse_y)
+                new_x = mouse_x + self._drag_x
+                new_y = mouse_y + self._drag_y
 
-        self.emit("mouse-click", (x,y), mouse_regions)
 
+                self._drag_sprite.x, self._drag_sprite.y = new_x, new_y
+                self._drag_sprite._on_drag(new_x, new_y)
+                self.emit("on-drag", self._drag_sprite, (new_x, new_y))
+                self.redraw()
 
+                return
+        else:
+            if not self.__drawing_queued: # avoid double mouse checks - the redraw will also check for mouse!
+                self._check_mouse(event.x, event.y)
 
-""" 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.emit("on-mouse-move", event)
 
-        self.text_y = -100
 
+    def _check_mouse(self, mouse_x, mouse_y):
+        if mouse_x is None: return
 
-    def on_expose(self):
-        # on expose is called when we are ready to draw
+        #check if we have a mouse over
+        over = set()
 
-        # 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!")
+        cursor = gtk.gdk.ARROW
 
-        self.draw_rect(round(self.rect_x),
-                       round(self.rect_y),
-                       self.rect_width,
-                       self.rect_height,
-                       10)
+        for sprite in self.all_sprites():
+            if sprite.interactive and self._check_hit(sprite, mouse_x, mouse_y):
+                if sprite.draggable:
+                    cursor = gtk.gdk.FLEUR
+                else:
+                    cursor = gtk.gdk.HAND2
 
-        self.set_color("#ff00ff")
-        self.context.fill()
+                over.add(sprite)
 
-        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)
+        new_mouse_overs = over - self._mouse_sprites
+        if new_mouse_overs:
+            for sprite in new_mouse_overs:
+                sprite._on_mouse_over()
 
+            self.emit("on-mouse-over", list(new_mouse_overs))
 
-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()
+        gone_mouse_overs = self._mouse_sprites - over
+        if gone_mouse_overs:
+            for sprite in gone_mouse_overs:
+                sprite._on_mouse_out()
+            self.emit("on-mouse-out", list(gone_mouse_overs))
 
-        box = gtk.VBox()
-        box.pack_start(self.graphic)
 
-        button = gtk.Button("Hello")
-        button.connect("clicked", self.on_go_clicked)
+        self._mouse_sprites = over
+        self.window.set_cursor(gtk.gdk.Cursor(cursor))
 
-        box.add_with_properties(button, "expand", False)
 
-        window.add(box)
-        window.show_all()
+    def _check_hit(self, sprite, x, y):
+        if sprite == self._drag_sprite:
+            return True
+
+        if not sprite.graphics.extents:
+            return False
 
-        # drop the hello on init
-        self.graphic.animate(self.graphic,
-                            dict(text_y = 120),
-                            duration = 0.7,
-                            easing = Easing.Bounce.easeOut)
+        sprite_x, sprite_y, sprite_x2, sprite_y2 = sprite.graphics.extents
 
+        if sprite_x <= x <= sprite_x2 and sprite_y <= y <= sprite_y2:
+            paths = sprite.graphics.paths
+            if not paths:
+                return True
 
-    def on_go_clicked(self, widget):
-        import random
+            context = cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, self.width, self.height))
+            for path in paths:
+                context.append_path(path)
+            return context.in_fill(x, y)
+        else:
+            return False
 
-        # 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)
+    def __on_button_press(self, area, event):
+        x = event.x
+        y = event.y
+        state = event.state
+        self._mouse_drag = (x, y)
 
+        over = None
+        for sprite in self.all_sprites():
+            if sprite.interactive and self._check_hit(sprite, event.x, event.y):
+                over = sprite # last one will take precedence
+        self._drag_sprite = over
+        self._button_press_time = dt.datetime.now()
 
-if __name__ == "__main__":
-   example = BasicWindow()
-   gtk.main()
+    def __on_button_release(self, area, event):
+        #if the drag is less than 5 pixles, then we have a click
+        click = self._button_press_time and (dt.datetime.now() - self._button_press_time) < dt.timedelta(milliseconds = 300)
+        self._button_press_time = None
+        self._mouse_drag = None
+        self._drag_x, self._drag_y = None, None
+        self._drag_sprite = None
+
+        if click:
+            targets = []
+            for sprite in self.all_sprites():
+                if sprite.interactive and self._check_hit(sprite, event.x, event.y):
+                    targets.append(sprite)
+                    sprite._on_click(event.state)
+
+            self.emit("on-click", event, targets)
+        self.emit("on-mouse-up")
diff --git a/src/hamster/pytweener.py b/src/hamster/pytweener.py
index c8c3876..64b1235 100644
--- a/src/hamster/pytweener.py
+++ b/src/hamster/pytweener.py
@@ -8,172 +8,145 @@
 # Python version by Ben Harling 2009
 # All kinds of slashing and dashing by Toms Baugis 2010
 import math
+import collections, itertools
 
 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 = default_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
-            onComplete = specify a function to call on completion of the tween
-            onUpdate = 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 "onComplete" in kwargs:
-            t_completeFunc = kwargs.pop("onComplete")
-        else: t_completeFunc = None
-
-        if "onUpdate" in kwargs:
-            t_updateFunc = kwargs.pop("onUpdate")
-        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:
-            tweenlist = self.currentTweens.setdefault(obj, [])
-            tweenlist.append(tw)
-        return tw
-
-    def removeTween(self, tweenObj):
-        tweenObj.complete = True
-
-    def getTweensAffectingObject(self, obj):
+        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, delay = 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.
+        """
+        duration = duration or self.default_duration
+        easing = easing or self.default_easing
+        delay = delay or 0
+
+        tw = Tween(obj, duration, easing, on_complete, on_update, delay, **kwargs )
+        self.current_tweens[obj].add(tw)
+
+
+    def get_tweens(self, obj):
         """Get a list of all tweens acting on the specified object
         Useful for manipulating tweens on the fly"""
-        return self.currentTweens.get(obj, [])
+        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 finish(self):
+        """jump the the last frame of all tweens"""
+        for obj in self.current_tweens:
+            for t in self.current_tweens[obj]:
+                t._update(t.duration)
+        self.current_tweens = {}
 
-    def killTweensOf(self, obj):
-        """Stop tweening an object, without completing the motion
-        or firing the completeFunction"""
-        try:
-            del self.currentTweens[obj]
-        except:
-            pass
+    def update(self, delta_seconds):
+        """update tweeners. delta_seconds is time in seconds since last frame"""
 
+        done_list = set()
+        for obj in self.current_tweens:
+            for tween in self.current_tweens[obj]:
+                done = tween._update(delta_seconds)
+                if done:
+                    done_list.add(tween)
+
+        # remove all the completed tweens
+        for tween in done_list:
+            if tween.on_complete:
+                tween.on_complete(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 obj in self.currentTweens:
-            for t in self.currentTweens[obj]:
-                t.update(t.duration)
-        self.currentTweens = {}
-
-    def update(self, timeSinceLastFrame):
-        for obj in self.currentTweens.keys():
-            # updating tweens from last to first and deleting while at it
-            # in order to not confuse the index
-            for i, t in reversed(list(enumerate(self.currentTweens[obj]))):
-                t.update(timeSinceLastFrame)
-                if t.complete:
-                    del self.currentTweens[obj][i]
-
-                if not self.currentTweens[obj]:
-                    del self.currentTweens[obj]
 
 class Tween(object):
-    __slots__ = ['duration', 'delay', 'target', 'tween', 'tweenables', 'delta',
-                 'target', 'ease', 'tweenables', 'delta', 'completeFunction',
-                 'updateFunction', 'complete', 'paused']
+    __slots__ = ('tweenables', 'target', 'delta', 'duration', 'delay',
+                 'ease', 'delta', 'on_complete',
+                 'on_update', 'complete', 'paused')
 
     def __init__(self, obj, duration, easing, on_complete, on_update, delay, **kwargs):
-        """Tween object use Tweener.addTween( ... ) to create"""
+        """Tween object use Tweener.add_tween( ... ) to create"""
         self.duration = duration
         self.delay = delay
         self.target = obj
         self.ease = easing
 
-        # list of (property, start_value, end_value)
-        self.tweenables = [(k, self.target.__dict__[k], v) for k, v in kwargs.items()]
+        # list of (property, start_value, delta)
+        self.tweenables = set(((key, self.target.__dict__[key], value - self.target.__dict__[key]) for key, value in kwargs.items()))
 
         self.delta = 0
-        self.completeFunction = on_complete
-        self.updateFunction = on_update
+        self.on_complete = on_complete
+        self.on_update = on_update
         self.complete = False
 
         self.paused = self.delay > 0
 
-    def pause( self, numSeconds=-1 ):
+    def pause(self, seconds = -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
+        self.delay = seconds
 
-    def resume( self ):
+    def resume(self):
         """Resume from pause"""
         if self.paused:
             self.paused=False
 
-    def update(self, ptime):
+    def _update(self, ptime):
         """Update tween with the time since the last frame
            if there is an update callback, it is always called
            whether the tween is running or paused"""
 
-        if self.complete:
-            return
+        if self.complete: return
 
         if self.paused:
             if self.delay > 0:
-                self.delay = max( 0, self.delay - ptime )
+                self.delay = max(0, self.delay - ptime)
                 if self.delay == 0:
                     self.paused = False
                     self.delay = -1
-                if self.updateFunction:
-                    self.updateFunction()
+                if self.on_update:
+                    self.on_update()
             return
 
         self.delta = self.delta + ptime
         if self.delta > self.duration:
             self.delta = self.duration
 
-
-        for prop, start_value, end_value in self.tweenables:
-            self.target.__dict__[prop] = self.ease(self.delta, start_value, end_value - start_value, self.duration)
+        for prop, start_value, delta_value in self.tweenables:
+            self.target.__dict__[prop] = self.ease(self.delta, start_value, delta_value, self.duration)
 
         if self.delta == self.duration:
             self.complete = True
-            if self.completeFunction:
-                self.completeFunction()
 
-        if self.updateFunction:
-            self.updateFunction()
+        if self.on_update:
+            self.on_update(self.target)
+
+        return self.complete
 
 
 """Robert Penner's easing classes ported over from actionscript by Toms Baugis (at gmail com).
@@ -216,19 +189,23 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 """
 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."""
+
     class Back(object):
         @staticmethod
-        def easeIn(t, b, c, d, s = 1.70158):
+        def ease_in(t, b, c, d, s = 1.70158):
             t = t / d
             return c * t * t * ((s+1) * t - s) + b
 
         @staticmethod
-        def easeOut (t, b, c, d, s = 1.70158):
+        def ease_out (t, b, c, d, s = 1.70158):
             t = t / d - 1
             return c * (t * t * ((s + 1) * t + s) + 1) + b
 
         @staticmethod
-        def easeInOut (t, b, c, d, s = 1.70158):
+        def ease_in_out (t, b, c, d, s = 1.70158):
             t = t / (d * 0.5)
             s = s * 1.525
 
@@ -240,7 +217,7 @@ class Easing(object):
 
     class Bounce(object):
         @staticmethod
-        def easeOut (t, b, c, d):
+        def ease_out (t, b, c, d):
             t = t / d
             if t < 1 / 2.75:
                 return c * (7.5625 * t * t) + b
@@ -255,31 +232,31 @@ class Easing(object):
                 return c * (7.5625 * t * t + 0.984375) + b
 
         @staticmethod
-        def easeIn (t, b, c, d):
-            return c - Easing.Bounce.easeOut(d-t, 0, c, d) + b
+        def ease_in (t, b, c, d):
+            return c - Easing.Bounce.ease_out(d-t, 0, c, d) + b
 
         @staticmethod
-        def easeInOut (t, b, c, d):
+        def ease_in_out (t, b, c, d):
             if t < d * 0.5:
-                return Easing.Bounce.easeIn (t * 2, 0, c, d) * .5 + b
+                return Easing.Bounce.ease_in (t * 2, 0, c, d) * .5 + b
 
-            return Easing.Bounce.easeOut (t * 2 -d, 0, c, d) * .5 + c*.5 + b
+            return Easing.Bounce.ease_out (t * 2 -d, 0, c, d) * .5 + c*.5 + b
 
 
 
     class Circ(object):
         @staticmethod
-        def easeIn (t, b, c, d):
+        def ease_in (t, b, c, d):
             t = t / d
             return -c * (math.sqrt(1 - t * t) - 1) + b
 
         @staticmethod
-        def easeOut (t, b, c, d):
+        def ease_out (t, b, c, d):
             t = t / d - 1
             return c * math.sqrt(1 - t * t) + b
 
         @staticmethod
-        def easeInOut (t, b, c, d):
+        def ease_in_out (t, b, c, d):
             t = t / (d * 0.5)
             if t < 1:
                 return -c * 0.5 * (math.sqrt(1 - t * t) - 1) + b
@@ -290,17 +267,17 @@ class Easing(object):
 
     class Cubic(object):
         @staticmethod
-        def easeIn (t, b, c, d):
+        def ease_in (t, b, c, d):
             t = t / d
             return c * t * t * t + b
 
         @staticmethod
-        def easeOut (t, b, c, d):
+        def ease_out (t, b, c, d):
             t = t / d - 1
             return c * (t * t * t + 1) + b
 
         @staticmethod
-        def easeInOut (t, b, c, d):
+        def ease_in_out (t, b, c, d):
             t = t / (d * 0.5)
             if t < 1:
                 return c * 0.5 * t * t * t + b
@@ -311,7 +288,7 @@ class Easing(object):
 
     class Elastic(object):
         @staticmethod
-        def easeIn (t, b, c, d, a = 0, p = 0):
+        def ease_in (t, b, c, d, a = 0, p = 0):
             if t==0: return b
 
             t = t / d
@@ -330,7 +307,7 @@ class Easing(object):
 
 
         @staticmethod
-        def easeOut (t, b, c, d, a = 0, p = 0):
+        def ease_out (t, b, c, d, a = 0, p = 0):
             if t == 0: return b
 
             t = t / d
@@ -348,7 +325,7 @@ class Easing(object):
 
 
         @staticmethod
-        def easeInOut (t, b, c, d, a = 0, p = 0):
+        def ease_in_out (t, b, c, d, a = 0, p = 0):
             if t == 0: return b
 
             t = t / (d * 0.5)
@@ -372,21 +349,21 @@ class Easing(object):
 
     class Expo(object):
         @staticmethod
-        def easeIn(t, b, c, d):
+        def ease_in(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):
+        def ease_out(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):
+        def ease_in_out(t, b, c, d):
             if t==0:
                 return b
             elif t==d:
@@ -402,35 +379,35 @@ class Easing(object):
 
     class Linear(object):
         @staticmethod
-        def easeNone(t, b, c, d):
+        def ease_none(t, b, c, d):
             return c * t / d + b
 
         @staticmethod
-        def easeIn(t, b, c, d):
+        def ease_in(t, b, c, d):
             return c * t / d + b
 
         @staticmethod
-        def easeOut(t, b, c, d):
+        def ease_out(t, b, c, d):
             return c * t / d + b
 
         @staticmethod
-        def easeInOut(t, b, c, d):
+        def ease_in_out(t, b, c, d):
             return c * t / d + b
 
 
     class Quad(object):
         @staticmethod
-        def easeIn (t, b, c, d):
+        def ease_in (t, b, c, d):
             t = t / d
             return c * t * t + b
 
         @staticmethod
-        def easeOut (t, b, c, d):
+        def ease_out (t, b, c, d):
             t = t / d
             return -c * t * (t-2) + b
 
         @staticmethod
-        def easeInOut (t, b, c, d):
+        def ease_in_out (t, b, c, d):
             t = t / (d * 0.5)
             if t < 1:
                 return c * 0.5 * t * t + b
@@ -441,17 +418,17 @@ class Easing(object):
 
     class Quart(object):
         @staticmethod
-        def easeIn (t, b, c, d):
+        def ease_in (t, b, c, d):
             t = t / d
             return c * t * t * t * t + b
 
         @staticmethod
-        def easeOut (t, b, c, d):
+        def ease_out (t, b, c, d):
             t = t / d - 1
             return -c * (t * t * t * t - 1) + b
 
         @staticmethod
-        def easeInOut (t, b, c, d):
+        def ease_in_out (t, b, c, d):
             t = t / (d * 0.5)
             if t < 1:
                 return c * 0.5 * t * t * t * t + b
@@ -462,17 +439,17 @@ class Easing(object):
 
     class Quint(object):
         @staticmethod
-        def easeIn (t, b, c, d):
+        def ease_in (t, b, c, d):
             t = t / d
             return c * t * t * t * t * t + b
 
         @staticmethod
-        def easeOut (t, b, c, d):
+        def ease_out (t, b, c, d):
             t = t / d - 1
             return c * (t * t * t * t * t + 1) + b
 
         @staticmethod
-        def easeInOut (t, b, c, d):
+        def ease_in_out (t, b, c, d):
             t = t / (d * 0.5)
             if t < 1:
                 return c * 0.5 * t * t * t * t * t + b
@@ -482,29 +459,29 @@ class Easing(object):
 
     class Sine(object):
         @staticmethod
-        def easeIn (t, b, c, d):
+        def ease_in (t, b, c, d):
             return -c * math.cos(t / d * (math.pi / 2)) + c + b
 
         @staticmethod
-        def easeOut (t, b, c, d):
+        def ease_out (t, b, c, d):
             return c * math.sin(t / d * (math.pi / 2)) + b
 
         @staticmethod
-        def easeInOut (t, b, c, d):
+        def ease_in_out (t, b, c, d):
             return -c * 0.5 * (math.cos(math.pi * t / d) - 1) + b
 
 
     class Strong(object):
         @staticmethod
-        def easeIn(t, b, c, d):
+        def ease_in(t, b, c, d):
             return c * (t/d)**5 + b
 
         @staticmethod
-        def easeOut(t, b, c, d):
+        def ease_out(t, b, c, d):
             return c * ((t / d - 1)**5 + 1) + b
 
         @staticmethod
-        def easeInOut(t, b, c, d):
+        def ease_in_out(t, b, c, d):
             t = t / (d * 0.5)
 
             if t < 1:
@@ -535,7 +512,7 @@ if __name__ == "__main__":
 
     t = dt.datetime.now()
     for i, o in enumerate(objects):
-        tweener.addTween(o, a = i, b = i, c = i, tweenTime = 1.0)
+        tweener.add_tween(o, a = i, b = i, c = i, duration = 1.0)
     print "add", dt.datetime.now() - t
 
     t = dt.datetime.now()
@@ -548,10 +525,8 @@ if __name__ == "__main__":
 
     for i in range(10):
         for i, o in enumerate(objects):
-            tweener.killTweensOf(o)
-            tweener.addTween(o, a = i, b = i, c = i, tweenTime = 1.0)
+            tweener.kill_tweens(o)
+            tweener.add_tween(o, a = i, b = i, c = i, duration = 1.0)
     print "kill-add", dt.datetime.now() - t
 
     print "total", dt.datetime.now() - total
-
-
diff --git a/src/hamster/stats.py b/src/hamster/stats.py
index 57ba3c5..2662d78 100644
--- a/src/hamster/stats.py
+++ b/src/hamster/stats.py
@@ -126,15 +126,15 @@ class Stats(object):
 
 
         #ah, just want summary look just like all the other text on the page
-        class CairoText(graphics.Area):
+        class CairoText(graphics.Scene):
             def __init__(self, fontsize = 10):
-                graphics.Area.__init__(self)
+                graphics.Scene.__init__(self)
                 self.text = ""
                 self.fontsize = fontsize
 
             def set_text(self, text):
                 self.text = text
-                self.redraw_canvas()
+                self.redraw()
 
             def on_expose(self):
                 # now for the text - we want reduced contrast for relaxed visuals
diff --git a/src/hamster/widgets/dayline.py b/src/hamster/widgets/dayline.py
index 05525d1..572943c 100644
--- a/src/hamster/widgets/dayline.py
+++ b/src/hamster/widgets/dayline.py
@@ -28,7 +28,7 @@ import datetime as dt
 import colorsys
 
 
-class DayLine(graphics.Area):
+class DayLine(graphics.Scene):
     def get_value_at_pos(self, x):
         """returns mapped value at the coordinates x,y"""
         return x / float(self.width / self.view_minutes)
@@ -36,7 +36,7 @@ class DayLine(graphics.Area):
 
     #normal stuff
     def __init__(self):
-        graphics.Area.__init__(self)
+        graphics.Scene.__init__(self)
 
         self.set_events(gtk.gdk.EXPOSURE_MASK
                                  | gtk.gdk.LEAVE_NOTIFY_MASK
@@ -87,7 +87,7 @@ class DayLine(graphics.Area):
 
         self.show()
 
-        self.redraw_canvas()
+        self.redraw()
 
 
     def on_button_release(self, area, event):
@@ -179,7 +179,7 @@ class DayLine(graphics.Area):
 
                     if end - start > 1:
                         self.highlight = (self.get_time(start), self.get_time(end))
-                        self.redraw_canvas()
+                        self.redraw()
 
                     self.__call_parent_time_changed()
                 else:
@@ -202,7 +202,7 @@ class DayLine(graphics.Area):
             return delta.days * 24 * 60 + delta.seconds / 60
 
     def scroll_to_range_start(self):
-        self.tweener.killTweensOf(self)
+        self.tweener.kill_tweens(self)
         self.animate(self, {"range_start_int": int(time.mktime(self.range_start.timetuple())),
                             "tweenType": graphics.Easing.Expo.easeOut,
                             "tweenTime": 0.4})
diff --git a/src/hamster/widgets/tags.py b/src/hamster/widgets/tags.py
index e49583c..5476bf0 100644
--- a/src/hamster/widgets/tags.py
+++ b/src/hamster/widgets/tags.py
@@ -212,7 +212,7 @@ class TagsEntry(gtk.Entry):
             runtime.dispatcher.del_handler('new_tags_added', self.refresh_tags)
 
 
-class TagBox(graphics.Area):
+class TagBox(graphics.Scene):
     __gsignals__ = {
         'tag-selected': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (str,)),
         'tag-unselected': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (str,)),
@@ -223,13 +223,13 @@ class TagBox(graphics.Area):
         self.hover_tag = None
         self.tags = []
         self.selected_tags = []
-        graphics.Area.__init__(self)
+        graphics.Scene.__init__(self)
 
         self.font_size = 10 #override default font size
 
         if self.interactive:
-            self.connect("mouse-over", self.on_tag_hover)
-            self.connect("button-release", self.on_tag_click)
+            self.connect("on-mouse-over", self.on_tag_hover)
+            self.connect("on-click", self.on_tag_click)
 
     def on_tag_hover(self, widget, regions):
         if regions:
@@ -237,7 +237,7 @@ class TagBox(graphics.Area):
         else:
             self.hover_tag = None
 
-        self.redraw_canvas()
+        self.redraw()
 
     def on_tag_click(self, widget, regions):
         tag = regions[0]
@@ -248,13 +248,13 @@ class TagBox(graphics.Area):
             #self.selected_tags.append(tag)
             self.emit("tag-selected", tag)
 
-        self.redraw_canvas()
+        self.redraw()
 
     def draw(self, tags):
         """Draw chart with given data"""
         self.tags = tags
         self.show()
-        self.redraw_canvas()
+        self.redraw()
 
     def tag_size(self, label):
         text_w, text_h = self.set_text(label)
diff --git a/src/hamster/widgets/timechart.py b/src/hamster/widgets/timechart.py
index 3ae07ab..51d4c75 100644
--- a/src/hamster/widgets/timechart.py
+++ b/src/hamster/widgets/timechart.py
@@ -30,11 +30,11 @@ from bisect import bisect
 DAY = dt.timedelta(1)
 WEEK = dt.timedelta(7)
 
-class TimeChart(graphics.Area):
+class TimeChart(graphics.Scene):
     """this widget is kind of half finished"""
 
     def __init__(self):
-        graphics.Area.__init__(self)
+        graphics.Scene.__init__(self)
         self.start_time, self.end_time = None, None
         self.durations = []
 
@@ -45,6 +45,8 @@ class TimeChart(graphics.Area):
 
         self.tick_totals = []
 
+        self.connect("on-enter-frame", self.on_enter_frame)
+
 
     def draw(self, durations, start_date, end_date):
         self.durations = durations
@@ -107,32 +109,33 @@ class TimeChart(graphics.Area):
 
         self.count_hours()
 
-        self.redraw_canvas()
+        self.redraw()
 
 
-    def on_expose(self):
+    def on_enter_frame(self, scene, context):
         if not self.start_time or not self.end_time:
             return
 
+        g = graphics.Graphics(context)
+
         # figure out colors
         bg_color = self.get_style().bg[gtk.STATE_NORMAL].to_string()
-        if self.colors.is_light(bg_color):
-            bar_color = self.colors.darker(bg_color,  30)
-            tick_color = self.colors.darker(bg_color,  50)
+        if g.colors.is_light(bg_color):
+            bar_color = g.colors.darker(bg_color,  30)
+            tick_color = g.colors.darker(bg_color,  50)
         else:
-            bar_color = self.colors.darker(bg_color,  -30)
-            tick_color = self.colors.darker(bg_color,  -50)
+            bar_color = g.colors.darker(bg_color,  -30)
+            tick_color = g.colors.darker(bg_color,  -50)
 
         # now for the text - we want reduced contrast for relaxed visuals
         fg_color = self.get_style().fg[gtk.STATE_NORMAL].to_string()
-        if self.colors.is_light(fg_color):
-            label_color = self.colors.darker(fg_color,  70)
+        if g.colors.is_light(fg_color):
+            label_color = g.colors.darker(fg_color,  70)
         else:
-            label_color = self.colors.darker(fg_color,  -70)
+            label_color = g.colors.darker(fg_color,  -70)
 
 
-
-        self.context.set_line_width(1)
+        g.set_line_style(width=1)
 
         # major ticks
         if self.end_time - self.start_time < dt.timedelta(days=1):  # about the same day
@@ -177,10 +180,10 @@ class TimeChart(graphics.Area):
 
 
         def line(x, color):
-            self.context.move_to(round(x) + 0.5, 0)
-            self.set_color(color)
-            self.context.line_to(round(x) + 0.5, self.height)
-            self.context.stroke()
+            g.move_to(round(x) + 0.5, 0)
+            g.set_color(color)
+            g.line_to(round(x) + 0.5, self.height)
+            g.stroke()
 
         def somewhere_in_middle(time, color):
             # draws line somewhere in middle of the minor tick
@@ -212,15 +215,15 @@ class TimeChart(graphics.Area):
             bar_size = max(round(self.height * total * 0.8), 1)
             x, bar_width = exes[current_time]
 
-            self.set_color(bar_color)
+            g.set_color(bar_color)
 
             # rounded corners
-            self.draw_rect(x, self.height - bar_size, bar_width - 1, bar_size, 3)
+            g.rectangle(x, self.height - bar_size, bar_width - 1, bar_size, 3)
 
             # straighten out bottom rounded corners
-            self.context.rectangle(x, self.height - min(bar_size, 2), bar_width - 1, min(bar_size, 2))
+            g.rectangle(x, self.height - min(bar_size, 2), bar_width - 1, min(bar_size, 2))
 
-            self.context.fill()
+            g.fill()
 
 
         # tick label format
@@ -239,6 +242,12 @@ class TimeChart(graphics.Area):
 
 
         # tick labels
+        # TODO - should handle the layout business in graphics
+        layout = context.create_layout()
+        default_font = pango.FontDescription(gtk.Style().font_desc.to_string())
+        default_font.set_size(8 * pango.SCALE)
+        layout.set_font_description(default_font)
+
         for current_time, total in self.tick_totals:
             # if we are on the day level, show label only on week start
             if (self.end_time - self.start_time) > dt.timedelta(10) \
@@ -247,11 +256,11 @@ class TimeChart(graphics.Area):
 
             x, bar_width = exes[current_time]
 
-            self.set_color(label_color)
-            self.layout.set_width(int((self.width - x) * pango.SCALE))
-            self.layout.set_markup(current_time.strftime(step_format))
-            self.context.move_to(x + 2, 0)
-            self.context.show_layout(self.layout)
+            g.set_color(label_color)
+            layout.set_width(int((self.width - x) * pango.SCALE))
+            layout.set_markup(current_time.strftime(step_format))
+            g.move_to(x + 2, 0)
+            context.show_layout(layout)
 
 
     def count_hours(self):



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