Cassowary, Cocoa Autolayout, and enaml constraints

Tue 22 May 2012 by Matthew Scott

If you’ve ever worked with a GUI toolkit such as Qt or wxWidgets, or any number of native GUI platforms such as Cocoa or Windows API, you’ll already understand that layout can be a pain.

This is especially true if you want a layout that gracefully recalculates when you resize a window or add widgets to a dynamic interface.

There is no shortage of generalized concepts such as boxes and grids and springs and struts, along with specialized layout engines such as grid bags. Some of these do their job simply, but are limited in the scope of what you can accomplish with them. Others allow more complex interactions and relationships between widget metrics, but

Thankfully, an algorithm named Cassowary was created in the late 1990s to solve a wide range of user interface layout problems. It does so by using a single algorithm that solves a system of linear equality and inequality constraints.

Cassowary has been refined over subsequent years, and ported from C++ to other languages. It is now gaining traction in the mainstream app development realm. Recently, Apple adopted it as the basis for its next generation Cocoa Autolayout system, and Enthought adopted it as the basis for the layout system in its new GUI development toolkit for Python, enaml.

Below, you’ll learn how Cassowary works, how you can make use of Cassowary using Cocoa Autolayout or Enthought enaml, and how the two compare for creating a form layout with moderate complexity.

Crash course in Cassowary

In a nutshell, this is how Cassowary works with your GUI toolkit:

  1. You define a set of constraints for a set of widgets, using the API provided by your GUI toolkit. Each constraint is a linear equality or inequality that describes one aspect of the layout requirement that is required or preferred.

    One way to use equalities is to maintain spacing between widgets, as in “widget1.left == widget2.right + 6“. Inequalities may be used to declare a minimum size, such as “widget1.width >= 20“, or even to declare a relationship between the metrics of two different widgets, as in “widget1.height == widget2.height * 1.2“.

  2. Cassowary uses these constraints, along with additional constraints provided by your GUI toolkit about default/preferred widget metrics, to solve the entire system of constraints.

    Depending on your constraints, Cassowary will either resolve them completely, or, in the face of ambiguous constraints, will resolve as much as it can.

  3. Cassowary, by way of your GUI toolkit, updates the now-constrained variables, and your GUI toolkit redraws your layout based on those new values.

    Each time a widget metric changes, such as resizing a window or dynamically adding or removing a widget, your GUI toolkit will call upon Cassowary to repeat steps 2 and 3.

Bringing Cassowary into mainstream app development

Cocoa Autolayout

Cocoa is an API that originated in the NeXTSTEP operating system, then evolved to become the standard way to build apps for Mac OS X, then later was adapted for iOS.

I won’t go into depth explaining the details of Cocoa Autolayout here, except to say that the Cocoa Autolayout video from 2011 WWDC does a superb job of introducing the concepts of Cassowary in general and showing how to use it in practice using Cocoa and Xcode.

Enthought enaml

enaml is “a framework for writing declarative user interfaces in Python”. If you like declarative programming, that description doesn’t do it justice. It was created to succeed Enthought’s TraitsUI as a multi-platform framework for creating GUIs bound to data models based on Enthought’s Traits library.

Like TraitsUI, it supports either Qt (via PySide) or wxWidgets (via wxPython), and it leverages the benefits of Traits.

Unlike TraitsUI, enaml discards some of the older conventions of programmatic GUI construction in favor of newer ones. It provides a DSL (domain specific language) that allows you to construct a GUI with less code, via a declarative superset of Python. It also uses the Cassowary solver as the foundation of its layout engine, providing constructs that allow you to easily express common layouts in a very human-readable manner.

enaml is still early in its development, but comes from a mature team. In my evaluation, its authors have done an impressive job. There are a few shortcomings, such as inaccurate documentation that lags behind current work, but the code is well-written and reasonably self-documenting. Someone comfortable reading and writing Python, and using “traditional” GUI development methodologies, will be able to understand and navigate enaml.

To get an idea of what enaml code looks like, view the enaml examples. Each example provides code, followed by screenshots of the resulting GUI.

Example layout using Cocoa autolayout

Apple offers some Autolayout demos in their developer documentation.

One of the demos, showing how to programmatically layout a form with a variety of constraints, was used in the Cocoa Autolayout video from 2011 WWDC.

For convenience, I’ve posted a copy of the FindPanelLayout demo; scroll to line 108 to view the code that defines the following layout constraints:

  • Basic layout of two rows, each containing two buttons and a text field.
  • Hard minimum width of text fields.
  • Alignment of the left edge of the text fields.
  • Lowering of content “hugging” priority of text fields, so they expand to fill extra space, and the buttons do not.
  • A preference for each pair of buttons, on the row with the smallest combined button width, to be equally sized when expanding to fill extra space.

Here is a demonstration of these constraints in action using Cocoa Autolayout:

Example layout ported to enaml

I ported the demo from Cocoa to a set of enaml constraints that result in the same layout.

Here is a demonstration of these constraints in action using enaml:

I’ve posted a complete copy of the FindPanelLayout demo ported to enaml. It’s beyond the scope of this article to describe all aspects of enaml, so we’ll highlight the list of constraints that govern the layout of the main window’s container.

Please refer to the Enaml Components and Layout documentation for general coverage of how to specify constraints.

First, the example uses vbox and hbox to describe the basic vertical and horizontal layout of the two rows. These functions are helpers provided by enaml to translate common layout patterns into the constraints needed by Cassowary:

vbox(
    hbox(find, find_next, find_field),
    hbox(replace, replace_and_find, replace_field),
),

Next, it uses another helper function, align, to ensure that the top values of each component in a given row are equal:

align('top', find, find_next, find_field),
align('top', replace, replace_and_find, replace_field),

Using a simple equality, it aligns the left edge of the two fields:

find_field.left == replace_field.left,

You may notice that the align function could be used here instead. Indeed, the following will result in the same constraint:

align('left', find_field, replace_field)

Finally, to ensure that the narrower row of buttons are of equal width, it ensures that the width of each pair of buttons are equal, but sets the priority of these equalities to weak, so that any default rules will take precedence when available:

(find.width == find_next.width) | 'weak',
(replace.width == replace_and_find.width) | 'weak',

Without these constraints, there will be horizontal space to fill in the row containing the narrower buttons. Cassowary will not know how to solve unambiguously in this case, and will end up picking one of the narrower buttons randomly to expand and fill space:

Without the weak priority, the width of both pairs of buttons will become equal, thus more horizontal space than is desired will be used:

As above, you can replace these constraints with calls to align:

align('width', find, find_next) | 'weak',
align('width', replace, replace_and_find) | 'weak',

In some cases this is appropriate. You’ll want to decide on a case-by-case basis which style is more semantically useful. Your goal when writing constraints should not only be to layout your widgets, but also to make it easy to read constraints in a way that describes the intent of your layout.

For example, the combination of vbox and hbox constraints could be rewritten as follows to use fewer constraints and to more closely match the Cocoa Autolayout example, but I found it more meaningful to use the nested approach demonstrated above.

vbox(find_field, replace_field),
hbox(find, find_next, find_field),
hbox(replace, replace_and_find, replace_field),

Comparison between enaml constraints and Cocoa autolayout

Similarities

  • Both toolkits use the Cassowary constraint solver.

    This aspect alone makes for quite a bit of overlap, and allows for some advanced layout concepts to be defined in similar ways, despite fundamental differences in GUI toolkit and programming language.

  • Many widget variables are available in both toolkits.

    For example, aligning the left side of each field, and giving equal width to the smaller pair of buttons, were able to be accomplished using both toolkits. In each toolkit, Cassowary has access to the left and width of a widget, among other common variables.

Differences

  • enaml is less noisy.

    While Cocoa Autolayout has an “ASCII art” parser designed to increase expressiveness and readability, I found that constraints written in enaml were much easier to read at-a-glance.

    (Note that I am biased toward Python, having had much more experience with it than with Objective C.)

  • Some types of constraints are unique to a specific GUI toolkit.

    There are probably some widget variables that are present in Cocoa and absent in enaml, and vice versa.

    Because of this, there are likely to be some specific layouts that are easy to describe in one toolkit but may be difficult or impossible in the other, despite that Cassowary is used in both cases to solve constraints.

  • Some default constraints differ.

    Similarly, there are some default constraints that differ between toolkits.

    In the example above, the Cocoa Autolayout version had to specify a minimum width for text fields, and also had to change their hugging priority so they’d expand horizontally and the buttons wouldn’t.

    The enaml version needed neither of those constraints, as the default constraints already defined such behavior.

    Another difference in the example is that the top of widgets in a horizontal box is already aligned, while the enaml version required the align(‘top’, …) constraints.

The future of Cassowary in mainstream app development

enaml is very new, and Cocoa Autolayout is not much older; as of this writing, neither have been available for more than one year. These two toolkits are bringing the Cassowary algorithm to common professional app development scenarios.

Hopefully, we’ll see wider adoption of Cassowary in other GUI toolkits, especially those used by popular mobile devices. Also as of this writing, Cocoa Autolayout is not yet available for iOS, and Cassowary has not been ported to Android.

One of the original creators of Cassowary has created a Javascript port of Cassowary, complete with a touch-compatible demonstration. As others discover this port, we may see some interesting uses of it to create more useful and visually appealing web apps.


Comments

Do you like podcasts? Check out Fanscribed. It helps podcasts use crowdsourcing to create high-quality transcripts.

Fanscribed is created and maintained by Elevencraft Inc.


I'm Matthew Scott, and my company Elevencraft Inc. exists to help you wisely use tech to solve problems:

  • "Full stack" development
  • Pair-programming
  • Python and Django support
  • Modern dev tools, like git

Ready to find out how we can work together? Email me at matthew@11craft.com


Elevencraft Inc.
Springfield, MO, USA
matthew@11craft.com
+1 360 389-2512