Integrated cassowary with GTK+ backend.

Includes some fixes to the cassowary implementation revealed by testing.
This commit is contained in:
Russell Keith-Magee 2014-04-22 17:35:15 +08:00
parent 119d2f5346
commit bbd64d0481
12 changed files with 322 additions and 92 deletions

View file

@ -1,2 +1,9 @@
# Implementation of the Cassowary algorithm
# http://www.cs.washington.edu/research/constraints/cassowary/
from .constraint import Equation, Inequality, StayConstraint, EditConstraint
from .expression import Expression
from .simplex_solver import SimplexSolver
from .strength import REQUIRED, STRONG, MEDIUM, WEAK
from .variable import Variable
from .utils import approx_equal

View file

@ -70,7 +70,7 @@ class Inequality(LinearConstraint):
if operator == self.LEQ:
self.expression.add_expression(param1, -1.0)
elif operator == self.GEQ:
self.expression.multply(-1)
self.expression.multiply(-1)
self.expression.add_expression(param1, 1.0)
else:
raise InternalError("Invalid operator in Inequality constructor")
@ -84,7 +84,15 @@ class Inequality(LinearConstraint):
else:
raise InternalError("Invalid operator in Inequality constructor")
# elif isinstance(param2, (float, int)):
elif isinstance(param2, (float, int)):
super(Inequality, self).__init__(param1.clone(), strength=strength, weight=weight)
if operator == self.LEQ:
self.expression.multiply(-1.0)
self.expression.add_expression(Expression(constant=param2), 1.0)
elif operator == self.GEQ:
self.expression.add_expression(Expression(constant=param2), -1.0)
else:
raise InternalError("Invalid operator in Inequality constructor")
else:
raise InternalError("Invalid parameters to Inequality constructor")
@ -142,7 +150,9 @@ class Equation(LinearConstraint):
elif isinstance(param2, Variable):
super(Equation, self).__init__(param1.clone(), strength=strength, weight=weight)
self.expression.add_variable(param2, -1.0)
# elif isinstance(param2, (float, int)):
elif isinstance(param2, (float, int)):
super(Equation, self).__init__(param1.clone(), strength=strength, weight=weight)
self.expression.add_expression(Expression(constant=param2), -1.0)
else:
raise InternalError("Invalid parameters to Equation constructor")
@ -153,9 +163,10 @@ class Equation(LinearConstraint):
elif isinstance(param2, Variable):
super(Equation, self).__init__(Expression(variable=param2), strength=strength, weight=weight)
elif isinstance(param2, (float, int)):
super(Equation, self).__init__(Expression(value=param2), strength=strength, weight=weight)
super(Equation, self).__init__(Expression(constant=param2), strength=strength, weight=weight)
self.expression.add_variable(param1, -1.0)
else:
raise InternalError("Invalid parameters to Equation constructor")
else:
raise InternalError("Invalid parameters to Equation constructor")

View file

@ -7,18 +7,5 @@ class ConstraintNotFound(Exception):
pass
class NonExpression(Exception):
pass
class NotEnoughStays(Exception):
pass
class RequiredFailure(Exception):
pass
class TooDifficult(Exception):
pass

View file

@ -1,5 +1,5 @@
from .variable import Variable, AbstractVariable
from .error import NonExpression, InternalError
from .error import InternalError
from .utils import approx_equal
@ -43,13 +43,13 @@ class Expression(object):
elif x.is_constant:
result = self * x.constant
else:
raise NonExpression()
raise TypeError('Cannot multiply expression by non-constant')
return result
def __div__(self, x):
if isinstance(x, (float, int)):
if approx_equal(x, 0):
raise NonExpression()
raise ZeroDivisionError()
result = Expression(constant=self.constant / x)
for clv, value in self.terms.items():
result.set_variable(clv, value / x)
@ -57,7 +57,7 @@ class Expression(object):
if x.is_constant:
result = self / x.constant
else:
raise NonExpression()
raise TypeError('Cannot divide expression by non-constant')
return result
def __add__(self, x):
@ -65,12 +65,20 @@ class Expression(object):
return self.clone().add_expression(x, 1.0)
elif isinstance(x, Variable):
return self.clone().add_variable(x, 1.0)
elif isinstance(x, (int, float)):
return self.clone().add_expression(Expression(constant=x), 1.0)
else:
raise TypeError('Cannot add object of type %s to expression' % type(x))
def __sub__(self, x):
if isinstance(x, Expression):
return self.clone().add_expression(x, -1.0)
elif isinstance(x, Variable):
return self.clone().add_variable(x, -1.0)
elif isinstance(x, (int, float)):
return self.clone().add_expression(Expression(constant=x), -1.0)
else:
raise TypeError('Cannot subtract object of type %s from expression' % type(x))
def add_expression(self, expr, n, subject=None, solver=None):
if isinstance(expr, AbstractVariable):

View file

@ -12,8 +12,7 @@ class SimplexSolver(Tableau):
def __init__(self):
super(SimplexSolver, self).__init__()
self.stay_minus_error_vars = []
self.stay_plus_error_vars = []
self.stay_error_vars = []
self.error_vars = {}
self.marker_vars = {}
@ -34,8 +33,7 @@ class SimplexSolver(Tableau):
def __repr__(self):
parts = []
parts.append('stay_plus_error_vars: %s' % self.stay_plus_error_vars)
parts.append('stay_minus_error_vars: %s' % self.stay_minus_error_vars)
parts.append('stay_error_vars: %s' % self.stay_error_vars)
parts.append('edit_var_map: %s' % self.edit_var_map)
return super(SimplexSolver, self).__repr__() + '\n' + '\n'.join(parts)
@ -96,7 +94,7 @@ class SimplexSolver(Tableau):
z_row = self.rows[self.objective]
# print "z_row", z_row
sw_coeff = cn.strength * cn.weight
if sw_coeff == 0:
# if sw_coeff == 0:
# print "cn ==", cn
# print "adding ", eplus, "and", eminus, "with sw_coeff", sw_coeff
z_row.set_variable(eplus, sw_coeff)
@ -108,8 +106,7 @@ class SimplexSolver(Tableau):
self.insert_error_var(cn, eplus)
if cn.is_stay_constraint:
self.stay_plus_error_vars.append(eplus)
self.stay_minus_error_vars.append(eminus)
self.stay_error_vars.append((eplus, eminus))
elif cn.is_edit_constraint:
prev_edit_constant = cn.expression.constant
@ -136,6 +133,8 @@ class SimplexSolver(Tableau):
self.optimize(self.objective)
self.set_external_variables()
return cn
def add_constraint_no_exception(self, cn):
try:
self.add_constraint(cn)
@ -174,7 +173,7 @@ class SimplexSolver(Tableau):
assert len(self.edit_var_map) == n
except ConstraintNotFound:
raise InternalError('Constraint not found')
raise InternalError('Constraint not found during internal removal')
def add_point_stays(self, points):
# print "add_point_stays", points
@ -201,8 +200,10 @@ class SimplexSolver(Tableau):
for cv in e_vars:
try:
z_row.add_expression(self.rows[cv], -cn.weight * cn.strength, self.objective, self)
# print 'add expression', self.rows[cv]
except KeyError:
z_row.add_variable(cv, -cn.weight * cn.strength, self.objective, self)
# print 'add variable', cv
try:
marker = self.marker_vars.pop(cn)
@ -216,59 +217,94 @@ class SimplexSolver(Tableau):
exit_var = None
min_ratio = 0.0
for v in col:
# print 'check var', v
if v.is_restricted:
# print 'var', v, ' is restricted'
expr = self.rows[v]
coeff = expr.coefficient_for(marker)
# print "Marker", marker, "'s coefficient in", expr, "is", coeff
if coeff < 0:
r = -expr.constant / coeff
if exit_var is None or r < min_ratio: # EXTRA BITS IN JS?
# print 'set exit var = ',v,r
min_ratio = r
exit_var = v
if exit_var is None:
# print "exit_var is still None"
for v in col:
# print 'check var', v
if v.is_restricted:
# print 'var', v, ' is restricted'
expr = self.rows[v]
coeff = expr.coefficient_for(marker)
# print "Marker", marker, "'s coefficient in", expr, "is", coeff
r = expr.constant / coeff
if exit_var is None or r < min_ratio:
# print 'set exit var = ',v,r
min_ratio = r
exit_var = v
if exit_var is None:
# print "exit_var is still None (again)"
if len(col) == 0:
# print 'remove column',marker
self.remove_column(marker)
else:
exit_var = [v for v in col if v != self.objective][-1] # ??
# print 'set exit var', exit_var
if exit_var is not None:
# print 'Pivot', marker, exit_var,
self.pivot(marker, exit_var)
if not self.rows.get(marker):
if self.rows.get(marker):
# print 'remove row', marker
expr = self.remove_row(marker)
if e_vars:
# print 'e_vars exist'
for v in e_vars:
if v != marker:
# print 'remove column',v
self.remove_column(v)
if cn.is_stay_constraint:
if e_vars:
for p_evar, m_evar in zip(self.stay_plus_error_vars):
e_vars.remove(p_evar)
e_vars.remove(m_evar)
# for p_evar, m_evar in self.stay_error_vars:
remaining = []
while self.stay_error_vars:
p_evar, m_evar = self.stay_error_vars.pop()
found = False
try:
# print 'stay constraint - remove plus evar', p_evar
e_vars.remove(p_evar)
found = True
except KeyError:
pass
try:
# print 'stay constraint - remove minus evar', m_evar
e_vars.remove(m_evar)
found = True
except KeyError:
pass
if not found:
remaining.append((p_evar, m_evar))
self.stay_error_vars = remaining
elif cn.is_edit_constraint:
assert e_vars is not None
# print 'edit constraint - remove column', self.edit_var_map[cn.variable].edit_minus
self.remove_column(self.edit_var_map[cn.variable].edit_minus)
del self.edit_var_map[cn.variable]
if e_vars:
for e_var in e_vars:
# print 'Remove error var', e_var
del self.error_vars[e_var]
if self.auto_solve:
# print 'final auto solve'
self.optimize(self.objective)
self.set_external_variables()
@ -510,7 +546,7 @@ class SimplexSolver(Tableau):
exit_var = v
if min_ratio == float('inf'):
raise InternalError('Objective function is unbounded in optimize')
raise RequiredFailure('Objective function is unbounded')
self.pivot(entry_var, exit_var)
@ -530,7 +566,7 @@ class SimplexSolver(Tableau):
def reset_stay_constants(self):
# print "reset_stay_constants"
for p_var, m_var in zip(self.stay_plus_error_vars, self.stay_minus_error_vars):
for p_var, m_var in self.stay_error_vars:
expr = self.rows.get(p_var)
if expr is None:
expr = self.rows.get(m_var)

View file

@ -2,4 +2,4 @@
EPSILON = 1e-8
def approx_equal(a, b):
return abs(a - b) < EPSILON
return abs(a - b) < EPSILON

View file

@ -29,13 +29,18 @@ class Container(Widget):
def __init__(self):
super(Container, self).__init__()
self._impl = NSView.alloc().init()
self.constraints = []
self.children = []
self.constraints = {}
def add(self, widget):
self.children.append(widget)
self._impl.addSubview_(widget._impl)
def constrain(self, constraint):
"Add the given constraint to the widget."
if constraint in self.constraints:
return
widget = constraint.attr.widget._impl
identifier = constraint.attr.identifier
@ -62,4 +67,4 @@ class Container(Widget):
self._impl.addConstraint_(constraint._impl)
self.constraints.append(constraint)
self.constraints[constraint] = constraint._impl

View file

@ -71,29 +71,6 @@ class Attribute(object):
self.multiplier = float(multiplier)
self.constant = float(constant)
@property
def update_strategy(self):
update_strategies = {
Attribute.LEFT: Value.MINIMUM,
Attribute.RIGHT: Value.MAXIMUM,
Attribute.TOP: Value.MINIMUM,
Attribute.BOTTOM: Value.MAXIMUM,
Attribute.WIDTH: Value.MAXIMUM,
Attribute.HEIGHT: Value.MAXIMUM,
Attribute.CENTER_X: Value.AVERAGE,
Attribute.CENTER_Y: Value.AVERAGE,
# Attribute.BASELINE: Value.AVERAGE,
}
if True: # Check for RTL
update_strategies[Attribute.LEADING] = Value.MINIMUM
update_strategies[Attribute.TRAILING] = Value.MAXIMUM
else:
update_strategies[Attribute.LEADING] = Value.MAXIMUM
update_strategies[Attribute.TRAILING] = Value.MINIMUM
return update_strategies[self.identifier]
@property
def identifier_label(self):
return {

View file

@ -1,4 +1,6 @@
from tailor.widget import WidgetBase
from tailor.cassowary import Expression
from tailor.constraint import Attribute
from tailor.widget import WidgetBase, BoundingBox
def wrapped_handler(widget, handler):
@ -9,11 +11,40 @@ def wrapped_handler(widget, handler):
class Widget(WidgetBase):
def __init__(self):
super(Widget, self).__init__()
self._bounding_box = BoundingBox()
self._expand_horizontal = True
self._expand_vertical = True
def _expression(self, identifier):
if identifier == Attribute.LEFT:
return Expression(variable=self._bounding_box.x)
elif identifier == Attribute.RIGHT:
return Expression(variable=self._bounding_box.x) + Expression(variable=self._bounding_box.width)
elif identifier == Attribute.TOP:
return Expression(variable=self._bounding_box.y)
elif identifier == Attribute.BOTTOM:
return Expression(variable=self._bounding_box.y) + Expression(variable=self._bounding_box.height)
elif identifier == Attribute.LEADING:
return Expression(variable=self._bounding_box.x)
elif identifier == Attribute.TRAILING:
return Expression(variable=self._bounding_box.x) + Expression(variable=self._bounding_box.width)
elif identifier == Attribute.WIDTH:
return Expression(variable=self._bounding_box.width)
elif identifier == Attribute.HEIGHT:
return Expression(variable=self._bounding_box.height)
elif identifier == Attribute.CENTER_X:
return Expression(variable=self._bounding_box.x) + (Expression(variable=self._bounding_box.width) / 2)
elif identifier == Attribute.CENTER_Y:
return Expression(variable=self._bounding_box.y) + (Expression(variable=self._bounding_box.height) / 2)
# elif identifier == self.BASELINE:
# return ...
@property
def width_hints(self):
def _width_hint(self):
return self._impl.get_preferred_width()
@property
def height_hints(self):
return self._impl.get_preferred_width()
def _height_hint(self):
return self._impl.get_preferred_height()

View file

@ -13,6 +13,9 @@ def wrapped_handler(widget, handler):
class Button(Widget):
def __init__(self, label, on_press=None):
super(Button, self).__init__()
# Buttons have a fixed drawn height. If their space allocation is
# greater than what is provided, center the button vertically.
self._expand_vertical = False
self.on_press = on_press

View file

@ -1,65 +1,218 @@
from gi.repository import Gtk, cairo
from tailor.constraint import LayoutManager
from tailor.constraint import Constraint
from tailor.cassowary import SimplexSolver, StayConstraint, WEAK, STRONG, REQUIRED, Equation, Inequality, Expression, approx_equal
from tailor.gtk.widgets.base import Widget
class LayoutManager(SimplexSolver):
def __init__(self, bounding_box):
super(LayoutManager, self).__init__()
self.bounding_box = bounding_box
self.children = {}
# Enforce a hard constraint that the container starts at 0,0
self.add_constraint(StayConstraint(self.bounding_box.x, strength=REQUIRED))
self.add_constraint(StayConstraint(self.bounding_box.y, strength=REQUIRED))
# # Add a weak constraint for the bounds of the container.
self.width_constraint = StayConstraint(self.bounding_box.width, strength=WEAK)
self.height_constraint = StayConstraint(self.bounding_box.height, strength=WEAK)
self.add_constraint(self.width_constraint)
self.add_constraint(self.height_constraint)
def add_constraint(self, constraint):
print constraint
return super(LayoutManager, self).add_constraint(constraint)
def add_widget(self, widget):
constraints = set()
min_width, preferred_width = widget._width_hint
min_height, preferred_height = widget._height_hint
print min_width, preferred_width
print min_height, preferred_height
# REQUIRED: Widget width must exceed minimum.
constraints.add(self.add_constraint(
Inequality(
Expression(variable=widget._bounding_box.width),
Inequality.GEQ,
min_width,
)
))
# REQUIRED: Widget height must exceed minimum
constraints.add(self.add_constraint(
Inequality(
Expression(variable=widget._bounding_box.height),
Inequality.GEQ,
min_height,
)
))
# STRONG: Adhere to preferred widget width
constraints.add(self.add_constraint(
Equation(
Expression(variable=widget._bounding_box.width),
preferred_width,
strength=STRONG
)
))
# STRONG: Try to adhere to preferred widget height
constraints.add(self.add_constraint(
Equation(
Expression(variable=widget._bounding_box.height),
preferred_height,
strength=STRONG
)
))
print constraints
self.children[widget] = constraints
def enforce(self, width, height):
self.remove_constraint(self.width_constraint)
self.remove_constraint(self.height_constraint)
self.bounding_box.width.value = width
self.bounding_box.height.value = height
self.width_constraint = StayConstraint(self.bounding_box.width, strength=REQUIRED)
self.height_constraint = StayConstraint(self.bounding_box.height, strength=REQUIRED)
self.add_constraint(self.width_constraint)
self.add_constraint(self.height_constraint)
def relax(self):
self.remove_constraint(self.width_constraint)
self.remove_constraint(self.height_constraint)
self.bounding_box.width.value = 0
self.bounding_box.height.value = 0
self.width_constraint = StayConstraint(self.bounding_box.width, strength=WEAK)
self.height_constraint = StayConstraint(self.bounding_box.height, strength=WEAK)
self.add_constraint(self.width_constraint)
self.add_constraint(self.height_constraint)
class TContainer(Gtk.Fixed):
def __init__(self, layout_manager):
super(TContainer, self).__init__()
self.children = []
self.layout_manager = layout_manager
def do_get_preferred_width(self):
# Calculate the minimum and natural width of the container.
print "PREFERRED WIDTH"
return 290, 370
# hint = self.layout_manager.layout(None, None)[self.container]
# return hint.right.vmin - hint.left.vmin, hint.right.vpref - hint.left.vpref,
width = self.layout_manager.bounding_box.width.value
# print "PREFERRED WIDTH", width
return width, width
def do_get_preferred_height(self):
# Calculate the minimum and natural height of the container.
print "PREFERRED HEIGHT"
return 100, 150
# hint = self.layout_manager.layout(None, None)[self.container]
# return hint.bottom.vmin - hint.top.vmin, hint.bottom.vpref - hint.top.vpref,
height = self.layout_manager.bounding_box.height.value
# print "PREFERRED HEIGHT", height
return height, height
def do_size_allocate(self, allocation):
print "Size allocate", allocation.width, 'x', allocation.height, ' @ ', allocation.x, 'x', allocation.y
hints = self.layout_manager.layout(allocation.width, allocation.height)
for widget, hint in hints.items():
if widget == self.layout_manager.container:
print 'LAYOUT CONTAINER'
self.set_allocation(allocation)
elif not widget._impl.get_visible():
self.set_allocation(allocation)
# Temporarily enforce a size requirement based on the allocation
self.layout_manager.enforce(allocation.width, allocation.height)
for widget in self.layout_manager.children:
print widget, widget._bounding_box
if not widget._impl.get_visible():
print "CHILD NOT VISIBLE"
else:
print "CHILD", widget
min_width, preferred_width = widget._width_hint
min_height, preferred_height = widget._height_hint
x_pos = widget._bounding_box.x.value
if widget._expand_horizontal:
width = widget._bounding_box.width.value
else:
x_pos = x_pos + ((widget._bounding_box.width.value - preferred_width) / 2.0)
width = preferred_width
y_pos = widget._bounding_box.y.value
if widget._expand_vertical:
height = widget._bounding_box.height.value
else:
y_pos = y_pos + ((widget._bounding_box.height.value - preferred_height) / 2.0)
height = preferred_height
child_allocation = cairo.RectangleInt()
child_allocation.x = hint.left.vpref
child_allocation.y = hint.top.vpref
child_allocation.width = hint.right.vpref - hint.left.vpref
child_allocation.height = hint.bottom.vpref - hint.top.vpref
child_allocation.x = x_pos
child_allocation.y = y_pos
child_allocation.width = width
child_allocation.height = height
widget._impl.size_allocate(child_allocation)
# Restore the unbounded allocation
self.layout_manager.relax()
class Container(Widget):
_RELATION = {
Constraint.LTE: Inequality.LEQ,
Constraint.GTE: Inequality.GEQ
}
def __init__(self):
super(Container, self).__init__()
self._layout_manager = LayoutManager(self)
self._layout_manager = LayoutManager(self._bounding_box)
self._impl = TContainer(self._layout_manager)
self._children = []
self._constraints = []
def add(self, widget):
self._layout_manager.reset()
self._children.append(widget)
self._impl.add(widget._impl)
self._layout_manager.add_widget(widget)
def constrain(self, constraint):
"Add the given constraint to the widget."
self._layout_manager.reset()
self._constraints.append(constraint)
widget = constraint.attr.widget
identifier = constraint.attr.identifier
if constraint.related_attr:
related_widget = constraint.related_attr.widget
related_identifier = constraint.related_attr.identifier
expr1 = widget._expression(identifier)
if not approx_equal(constraint.attr.multiplier, 1.0):
expr1 = expr1 * constraint.attr.multiplier
if not approx_equal(constraint.attr.constant, 0.0):
expr1 = expr1 + constraint.attr.constant
expr2 = related_widget._expression(related_identifier)
if not approx_equal(constraint.related_attr.multiplier, 1.0):
expr2 = expr2 * constraint.related_attr.multiplier
if not approx_equal(constraint.related_attr.constant, 0.0):
expr2 = expr2 + constraint.related_attr.constant
print 'E1', expr1
print 'E2', expr2
if constraint.relation == Constraint.EQUAL:
self._layout_manager.add_constraint(Equation(expr1, expr2))
else:
self._layout_manager.add_constraint(Inequality(expr1, self._RELATION[constraint.relation], expr2))
else:
expr = widget._expression(identifier)
if not approx_equal(constraint.attr.multiplier, 1.0):
expr = expr * constraint.attr.multiplier
print 'E', expr
if constraint.relation == Constraint.EQUAL:
self._layout_manager.add_constraint(Equation(expr, constraint.attr.constant))
else:
self._layout_manager.add_constraint(Equation(expr, self._RELATION[constraint.relation], constraint.attr.constant))

View file

@ -1,6 +1,18 @@
from tailor.cassowary import Variable
from tailor.constraint import Attribute
class BoundingBox(object):
def __init__(self):
self.x = Variable('x', 0.0)
self.y = Variable('y', 0.0)
self.width = Variable('width', 0.0)
self.height = Variable('height', 0.0)
def __repr__(self):
return u'%sx%s @ %s,%s' % (self.width.value, self.height.value, self.x.value, self.y.value)
class WidgetBase(object):
def __init__(self):
self.LEFT = Attribute(self, Attribute.LEFT)