Examples¶
The following examples demonstrate the use of Cassowary in practical constraint-solving problems.
Quadrilaterals¶
The “Bounded Quadrilateral” demo is the online example provided for Cassowary. The online example is implemented in JavaScript, but the implementation doesn’t alter the way the Cassowary algorithm is used.
The Bounded quadrilateral problem starts with a bounded, two dimensional canvas. We want to draw a quadrilateral on this plane, subject to a number of constraints.
Firstly, we set up the solver system itself:
from cassowary import SimplexSolver, Variable
solver = SimplexSolver()
Then, we set up a convenience class for holding information about points on a 2D plane:
class Point(object):
def __init__(self, identifier, x, y):
self.x = Variable('x' + identifier, x)
self.y = Variable('y' + identifier, y)
def __repr__(self):
return u'(%s, %s)' % (self.x.value, self.y.value)
Now we can set up a set of points to describe the initial location of our quadrilateral - a 190x190 square:
points = [
Point('0', 10, 10),
Point('1', 10, 200),
Point('2', 200, 200),
Point('3', 200, 10),
Point('m0', 0, 0),
Point('m1', 0, 0),
Point('m2', 0, 0),
Point('m3', 0, 0),
]
midpoints = points[4:]
Note that even though we’re drawing a quadrilateral, we have 8 points. We’re tracking the position of the midpoints independent of the corners of our quadrilateral. However, we don’t need to define the position of the midpoints. The position of the midpoints will be set by defining constraints.
Next, we set up some stays. A stay is a constraint that says that a particular
variable shouldn’t be modified unless it needs to be - that it should “stay”
as is unless there is a reason not to. In this case, we’re going to set a stay
for each of the four corners - that is, don’t move the corners unless you have
to. These stays are defined as WEAK
stays – so they’ll have a very low
priority in the constraint system. As a tie breaking mechanism, we’re also
going to set each stay to have a different weight - so the top left corner
(point 1) will be moved in preference to the bottom left corner (point 2), and
so on:
weight = 1.0
multiplier = 2.0
for point in points[:4]:
solver.add_stay(point.x, strength=WEAK, weight=weight)
solver.add_stay(point.y, strength=WEAK, weight=weight)
weight = weight * multiplier
Now we can set up the constraints to define where the midpoints fall. By
definition, each midpoint must fall exactly halfway between two points
that form a line, and that’s exactly what we describe - an expression that
computes the position of the midpoint. This expression is used to construct a
Constraint
, describing that the value of the midpoint must
equal the value of the expression. The Constraint
is then
added to the solver system:
for start, end in [(0, 1), (1, 2), (2, 3), (3, 0)]:
cle = (points[start].x + points[end].x) / 2
cleq = midpoints[start].x == cle
solver.add_constraint(cleq)
cle = (points[start].y + points[end].y) / 2
cleq = midpoints[start].y == cle
solver.add_constraint(cleq)
When we added these constraints, we didn’t provide any arguments - that means
that they will be added as REQUIRED
constraints.
Next, lets add some constraints to ensure that the left side of the quadrilateral stays on the left, and the top stays on top:
solver.add_constraint(points[0].x + 20 <= points[2].x)
solver.add_constraint(points[0].x + 20 <= points[3].x)
solver.add_constraint(points[1].x + 20 <= points[2].x)
solver.add_constraint(points[1].x + 20 <= points[3].x)
solver.add_constraint(points[0].y + 20 <= points[1].y)
solver.add_constraint(points[0].y + 20 <= points[2].y)
solver.add_constraint(points[3].y + 20 <= points[1].y)
solver.add_constraint(points[3].y + 20 <= points[2].y)
Each of these constraints is posed as a Constraint
. For
example, the first expression describes a point 20 pixels to the right of the
x coordinate of the top left point. This Constraint
is
then added as a constraint on the x coordinate of the bottom right (point 2)
and top right (point 3) corners - the x coordinate of these points must be at
least 20 pixels greater than the x coordinate of the top left corner (point
0).
Lastly, we set the overall constraints – the constraints that limit how large our 2D canvas is. We’ll constraint the canvas to be 500x500 pixels, and require that all points fall on that canvas:
for point in points:
solver.add_constraint(point.x >= 0)
solver.add_constraint(point.y >= 0)
solver.add_constraint(point.x <= 500)
solver.add_constraint(point.y <= 500)
This gives us a fully formed constraint system. Now we can use it to answer layout questions. The most obvious question – where are the midpoints?
>>> midpoints[0]
(10.0, 105.0)
>>> midpoints[1]
(105.0, 200.0)
>>> midpoints[2]
(200.0, 105.0)
>>> midpoints[3]
(105.0, 10.0)
You can see from this that the midpoints have been positioned exactly where you’d expect - half way between the corners - without having to explicitly specify their positions.
These relationships will be maintained if we then edit the position of the corners. Lets move the position of the bottom right corner (point 2). We mark the variables associated with that corner as being Edit variables:
solver.add_edit_var(points[2].x)
solver.add_edit_var(points[2].y)
Then, we start an edit, change the coordinates of the corner, and stop the edit:
with solver.edit():
solver.suggest_value(points[2].x, 300)
solver.suggest_value(points[2].y, 400)
As a result of this edit, the midpoints have automatically been updated:
>>> midpoints[0]
(10.0, 105.0)
>>> midpoints[1]
(155.0, 300.0)
>>> midpoints[2]
(250.0, 205.0)
>>> midpoints[3]
(105.0, 10.0)
If you want, you can now repeat the edit process for any of the points - including the midpoints.
GUI layout¶
The most common usage (by deployment count) of the Cassowary algorithm is as the Autolayout mechanism that underpins GUIs in OS X Lion and iOS6. Although there’s lots of code required to make a full GUI toolkit work, the layout problem is a relatively simple case of solving constraints regarding the size and position of widgets in a window.
In this example, we’ll show a set of constraints used to determine the placement of a pair of buttons in a GUI. To simplify the problem, we’ll only worry about the X coordinate; expanding the implementation to include the Y coordinate is a relatively simple exercise left for the reader.
When laying out a GUI, widgets have a width; however, widgets can also change
size. To accommodate this, a widget has two size constraints in each dimension:
a minimum size, and a preferred size. The minimum size is a REQUIRED
constraint that must be met; the preferred size is a STRONG
constraint
that the solver should try to accommodate, but may break if necessary.
The GUI also needs to be concerned about the size of the window that is being laid out. The size of the window can be handled in two ways:
- a
REQUIRED
constraint – i.e., this is the size of the window; show me how to lay out the widgets; or - a
WEAK
constraint – i.e., come up with a value for the window size that accommodates all the other widget constraints. This is the interpretation used to determine an initial window size.
As with the Quadrilateral demo, we start by creating the solver, and creating a storage mechanism to hold details about buttons:
from cassowary import SimplexSolver, Variable
solver = SimplexSolver()
class Button(object):
def __init__(self, identifier):
self.left = Variable('left' + identifier, 0)
self.width = Variable('width' + identifier, 0)
def __repr__(self):
return u'(x=%s, width=%s)' % (self.left.value, self.width.value)
We then define our two buttons, and the variables describing the size of the window on which the buttons will be placed:
b1 = Button('b1')
b2 = Button('b2')
left_limit = Variable('left', 0)
right_limit = Variable('width', 0)
left_limit.value = 0
solver.add_stay(left_limit)
solver.add_stay(right_limit, WEAK)
The left limit is set as a REQUIRED
constraint – the left border can’t
move from coordinate 0. However, the window can expand if necessary to
accommodate the widgets it contains, so the right limit is a WEAK
constraint.
Now we can define the constraints on the button layouts:
# The two buttons are the same width
solver.add_constraint(b1.width == b2.width)
# Button1 starts 50 from the left margin.
solver.add_constraint(b1.left == left_limit + 50)
# Button2 ends 50 from the right margin
solver.add_constraint(left_limit + right_limit == b2.left + b2.width + 50)
# Button2 starts at least 100 from the end of Button1. This is the
# "elastic" constraint in the system that will absorb extra space
# in the layout.
solver.add_constraint(b2.left == b1.left + b1.width + 100)
# Button1 has a minimum width of 87
solver.add_constraint(b1.width >= 87)
# Button1's preferred width is 87
solver.add_constraint(b1.width == 87, strength=STRONG)
# Button2's minimum width is 113
solver.add_constraint(b2.width >= 113)
# Button2's preferred width is 113
solver.add_constraint(b2.width == 113, strength=STRONG)
Since we haven’t imposed a hard constraint on the right hand side, the constraint system will give us the smallest window that will satisfy these constraints:
>>> b1
(x=50.0, width=113.0)
>>> b2
(x=263.0, width=113.0)
>>> right_limit.value
426.0
That is, the smallest window that can accommodate these constraints is 426
pixels wide. However, if the user makes the window larger, we can still lay
out widgets. We impose a new REQUIRED
constraint with the size of the
window:
right_limit.value = 500
right_limit_stay = solver.add_constraint(right_limit, strength=REQUIRED)
>>> b1
(x=50.0, width=113.0)
>>> b2
(x=337.0, width=113.0)
>>> right_limit.value
500.0
That is - if the window size is 500 pixels, the layout will compensate by
putting button2
a little further to the right. The WEAK
stay on the
right limit that we established at the start is ignored in preference for the
REQUIRED
stay.
If the window is then resized again, we can remove the 500 pixel limit, and impose a new limit:
solver.remove_constraint(right_limit_stay)
right_limit.value = 475
right_limit_stay = solver.add_constraint(right_limit, strength=REQUIRED)
solver.add_constraint(right_limit_stay)
>>> b1
(x=50.0, width=113.0)
>>> b2
(x=312.0, width=113.0)
>>> right_limit.value
475.0
Again, button2
has been moved, this time to the left, compensating for the
space that was lost by the contracting window size.