def textwidth(ctx, txt, fontname, fontsize):
    """
    Memoized textwidth() function.

    The width of a string returned by textwidth() equals the sum of the
    widths of its constituting characters, so only need to simply cache
    the widths of the individual characters.
    """

    key = fontname + str(fontsize)
    fs = _textwidth_cache[key]
    w = 0

    for c in txt:
        if c not in fs:
            fs[c] = ctx.textwidth(c)
        w += fs[c]

    return w def createpath(ctx, segments, close=True):
    """ Create a path object from a list of segment definitions.

    Each element of the list ``segments`` defines a segment of the path.
    If the element is a single object, it defines an endpoint of a
    straight line. If the element is a 2 element list, it defines the
    two endpoints of a straight line in `[p0, p1]` format. If the
    element is a 4 element lists, it  defines a Bezier-path in `[p0, p1,
    p2, p3]` format, where `p0` and `p3` are the two endpoints and `p1`
    and `p2` the two control points. All points objects should have `x`
    and `y` properties. ``close`` controls whether the resulting path
    shall be automatically closed or not.
    """

    ctx.autoclosepath(close)

    s0 = segments[0]
    if type(s0) in (list, tuple):
        ctx.beginpath(s0[0].x, s0[0].y)
    else:
        ctx.beginpath(s0.x, s0.y)

    for s in segments:
        if type(s) in (list, tuple):
            if len(s) == 2:
                ctx.lineto(s[1].x, s[1].y)
            elif len(s) == 4:
                ctx.curveto(s[1].x, s[1].y, s[2].x, s[2].y, s[3].x, s[3].y)
            else:
                raise ValueError, 'Invalid path segment: %s' % s
        else:
            ctx.lineto(s.x, s.y)

    return ctx.endpath(draw=False) def brightness(col):
    # Formula from:
    # http://www.nbdtech.com/Blog/archive/2008/04/27/Calculating-the-Perceived-Brightness-of-a-Color.aspx
    return math.sqrt(  .241 * pow(col.r, 2)
                     + .691 * pow(col.g, 2)
                     + .068 * pow(col.b, 2)) class Properties(object):
    """ Class for managing configuration properties. """

    def __init__(self, properties, defaults, config, extra_prop_warning=True):
        """
        Load and parse the default config file ``default`` and merge it
        with the configuration ``config`` (defaults will be
        overwritten).

        The ``properties`` dict contains the list of allowed properties
        where the key is the name of the property and the value a
        two-element tuple of which the first element is the class of the
        property and the second element the property's extra parameters
        (note that some property types have mandatory extra parameters,
        e.g. ArrayProperty). For example:

        {
            'fontName':  (StringProperty, {}),
            'fontSizes': (ArrayProperty,  {'type': NumberProperty})
        }

        Warn on property names that are not listed in the ``properties``
        dict if ``extra_prop_warning`` is True.
        """

        c = loaddefaults(defaults)
        c.update(config)
        config = c

        # Build properties dictionary
        self._properties = {}
        for name, prop_params in properties.iteritems():
            # The first parameter is the property class, the second the
            # optional constructor parameters
            prop_class, opts = prop_params
            self._properties[name] = prop_class(name, **opts)

        for name, prop in self._properties.iteritems():
            if name not in config:
                raise ConfigError("Missing property: '%s'" % name)
            e = parse_expr(config[name])
#            print '>>>', name, ':', e
            prop.expr = e
            prop.name = name

        if extra_prop_warning:
            self._warn_extra_props(config)

    def _warn_extra_props(self, config):
        extra_props = set(config.keys()) - set(self._properties.keys())
        for p in extra_props:
            token = config[p][0]
            #TODO make displaying warnings optional? print to stdout?
            print >>sys.stderr, (
                "Warning: Unknown property '%s' in configuration "
                "file '%s' on line %s" % (p, token.file, token.line))

    def eval(self, name, scope=None, vars={}):
        """ Evaluate the value of a property.

        ``name`` is the name of the property, ``scope`` the object in
        whose context the property is to be evaluated and ``vars``
        contains a dict of variable name and value pairs that will be
        injected into the evaluation scope.
        """

        if name not in self._properties:
            # TODO more detailed error message
            raise AttributeError("Property '%s' does not exist" % name)

        p = self._properties[name]
        if scope:
            for propname, varname in scope.property_mappings.iteritems():
                if hasattr(scope, propname):
                    vars[varname] = getattr(scope, propname)
                # TODO triggered by 'basecolor' -- why?
#                else:
#                    raise ConfigError("Variable '%s' is not evaluated "
#                                      "at this point" % (varname))

        return p.eval(vars) class StringProperty(Property):
    def _validate(self):
        if type(self.value) not in (str, unicode):
            raise ConfigError("Property '%s' must evaluate to a string"
                              % self.name, self.expr[0]) class NumberProperty(Property):
    def __init__(self, name, min=None, max=None):
        super(NumberProperty, self).__init__(name)
        self.min = min
        self.max = max

    def _validate(self):
        if type(self.value) not in (int, float):
            raise ConfigError("Property '%s' must evaluate to a number"
                              % self.name, self.expr)

        if self.min and self.value < self.min:
            raise ConfigError(
                "Number property '%s' must have a value greater "
                "than %s" % (self.name, self.min), self.expr)

        if self.max and self.value > self.max:
            raise ConfigError("Number property '%s' must have a value less "
                              "than %s" % (self.name, self.max), self.expr) class ColorProperty(Property):
    def _validate(self):
        if type(self.value).__name__ != 'Color':
            raise ConfigError("Property '%s' must evaluate to a color"
                              % self.name, self.expr) class EnumProperty(Property):
    def __init__(self, name, values):
        super(EnumProperty, self).__init__(name)
        self.values = values

    def eval(self, vars):
        enumvars = {}
        for value, name in enumerate(self.values):
            enumvars[name] = value
        vars.update(enumvars)
        n = eval_expr(self.expr, vars)

        if type(n) not in (int, float):
            raise ConfigError(
                ("Enum property '%s' must evaluate to a numeric value"
                 % self.name), self.expr)

        n = int(round(n))
        if n < 0 or n >= len(self.values):
            raise ConfigError(
                ("Enum property '%s' evaluated to an invalid "
                 "numeric value: %s" % (self.name, n)), self.expr)

        self.value = self.values[n]
        return self.value class BooleanProperty(Property):
    def eval(self, vars):
        vars = {'no': 0, 'off': 0, 'false': 0, 'yes': 1, 'on': 1, 'true': 1}
        n = eval_expr(self.expr, vars)

        if type(n) not in (int, float):
            raise ConfigError(
                ("Boolean property '%s' must evaluate to a numeric value"
                 % self.name), self.expr)

        self.value = True if n > 0.0 else False
        return self.value class ArrayProperty(Property):
    def __init__(self, name, type):
        super(ArrayProperty, self).__init__(name)
        self.type = type

    def _validate(self):
        # TODO array element type validation
        if type(self.value) != list:
            raise ValueError class Vector2(object):
    """ Class representing two-dimensional vectors.

    The coordinate system used has the following properties:
        - the origo (0,0) is located in the top left corner
        - the positive x direction is from left to right
        - the positive y direction is from top to bottom
        - positive rotation is counter-clockwise
    """
    def __init__(self, *args, **kwargs):
        if args:
            if isinstance(args[0], Vector2):
                self.x = float(args[0].x)
                self.y = float(args[0].y)
                return
            elif len(args) < 2:
                raise ValueError, "Must specify 'x' and 'y' of new vector"
            self.x = float(args[0])
            self.y = float(args[1])
        else:
            if not ('m' in kwargs and 'angle' in kwargs):
                raise ValueError, "Must specify 'm' and 'angle' of new vector"
            m = kwargs['m']
            a = -kwargs['angle']
            self.x = m * math.cos(a)
            self.y = m * math.sin(a)

    def __repr__(self):
        return '(%s, %s)' % (self.x, self.y)

    def magnitude(self):
        return math.sqrt(self.x * self.x + self.y * self.y)

    m = property(magnitude)

    def angle(self):
        a = -math.atan2(self.y, self.x)
        if a < 0:
            a += math.pi * 2
        return a

    a = property(angle)

    def normalize(self):
        m = self.magnitude()
        if m != 0:
            self.x /= m
            self.y /= m
        return self

    def rotate(self, angle):
        angle = -angle
        cos = math.cos(angle)
        sin = math.sin(angle)
        x = self.x * cos - self.y * sin
        y = self.x * sin + self.y * cos
        self.x = x
        self.y = y
        return self

    def __add__(self, s):
        return Vector2(self.x + s.x, self.y + s.y)

    def __iadd__(self, s):
        self.x += s.x
        self.y += s.y
        return self

    def __sub__(self, s):
        return Vector2(self.x - s.x, self.y - s.y)

    def __isub__(self, s):
        self.x -= s.x
        self.y -= s.y
        return self

    def __mul__(self, s):
        return Vector2(self.x * s, self.y * s)

    def __rmul__(self, s):
        return Vector2(self.x * s, self.y * s)

    def __imul__(self, s):
        self.x *= s
        self.y *= s
        return self

    def __div__(self, s):
        return Vector2(self.x / s, self.y / s)

    def __idiv__(self, s):
        self.x /= s
        self.y /= s
        return self
 class Direction(object):
    Top, Right, Bottom, Left = range(4)

import math, os
import twyg.geomutils as geom
import twyg.textwrap as textwrap
from twyg.common import textwidth, createpath, brightness
from twyg.config import (Properties, StringProperty, NumberProperty,
                         ColorProperty, EnumProperty, BooleanProperty,
                         ArrayProperty)
from twyg.geom import Vector2
from twyg.tree import Direction


        lines    -- list of strings containing the text in each line
        rects    -- list of equi-height rectangles in [x, y, w, h] format
        height   -- height of the containing rectangle
        """

        if not node._textrects:
            return

        preblanks = 0
        for l in node._textlines:
            if not l:
                preblanks += 1
            else:
                break

        postblanks = 0
        for l in reversed(node._textlines):
            if not l:
                postblanks += 1
            else:
                break

        lineheight = node._textrects[0].h

        numnonblanks = len(node._textlines) - preblanks - postblanks
        textheight = numnonblanks * lineheight
        ystart = (node.textheight - textheight) / 2.
        ystart -= preblanks * lineheight

        yoffs = ystart - node._textrects[0].y

        for r in node._textrects:
            r.y += yoffs


class RectNodeDrawer(NodeDrawer):

    def __init__(self, config={}):
        properties = {
            'roundness':     (NumberProperty, {'min': 0.0, 'max': 1.0}),
            'cornerRadius':  (NumberProperty, {'min': 0.0}),
            'roundingStyle': (EnumProperty,   {'values': ('screen', 'arc')})
        }

        super(RectNodeDrawer, self).__init__(properties, 'rect', config)

    def _calc_shape_path(self, node):
        E = self._eval_func(node)

        style = E('roundingStyle')

        if style == 'screen':
            r =  E('roundness')
            r = min(max(0, r / 2.), 1)

            return _ctx.rect(0, 0, node.width, node.height,
                             roundness=r, draw=False)

        elif style == 'arc':
            r = E('cornerRadius')
            r = min(r, node.height / 2.)
            r = min(r, node.width / 2.)

            points = geom.rounded_rect(0, 0, node.width, node.height, r)
            return createpath(_ctx, points, close=False)


class BoxNodeDrawer(NodeDrawer):

    def __init__(self, config={}):
        orientation = ('topleft', 'topright', 'bottomleft', 'bottomright')

        properties = {
            'boxOrientation':      (EnumProperty,   {'values': orientation}),
            'boxDepth':            (NumberProperty, {'min': 0.0}),
            'horizSideColor':      (ColorProperty,  {}),
            'vertSideColor':       (ColorProperty,  {}),
            'strokeColor':         (ColorProperty,  {})
        }

        super(BoxNodeDrawer, self).__init__(properties, 'box', config)

        # Determine 3D depth orientation
        E = self._eval_func(None)

        orientation = E('boxOrientation')
        self._horiz_dir = 0
        self._vert_dir = 0

        if orientation.find('top') > -1:
            self._vert_dir = 1
        elif orientation.find('bottom') > -1:
            self._vert_dir = -1

        if orientation.find('left') > -1:
            self._horiz_dir = -1
        elif orientation.find('right') > -1:
            self._horiz_dir = 1

        if self._horiz_dir == 0:
            self._horiz_dir = 1

        if self._vert_dir == 0:
            self._vert_dir = 1

    def precalc_node(self, node):
        super(BoxNodeDrawer, self).precalc_node(node)

        E = self._eval_func(node)

        node._boxdepth = E('boxDepth')

        # Make the bounding box big enough so that the shadow can fit in
        # too -- the actual coordinate calculations will happen later
        node.bboxwidth += node._boxdepth
        node.bboxheight += node._boxdepth

        y = node.bboxheight / 2
        node._conn_point_left = Vector2(0, y)

        node._conn_point_right = Vector2(node.bboxwidth, y)
