What are constraints
At its most basic, a constraint is a relation between two values. The relation
can be described as a linear equation:
target.attribute = source.attribute × multiplier + constant
For instance, this:
Can be described as:
blue.start = red.end × 1.0 + 8.0
Or:
- the attribute, “start”, of the target, “blue”, which is going to be set by the constraint; this is the left hand side of the equation
- the relation between the left and right hand sides of the equation, in this case equality; relations can also be greater than or equal to,
and less than or equal to - the attribute, “end”, of the source, “red”, which is going to be read by the constraint; this is the right hand side of the equation
- the multiplier, “1.0”, applied to the attribute of the source
- the constant, “8.0”, an offset added to the attribute
A constraint layout is a series of equations like the one above, describing all the relationships between the various parts of your UI.
It’s important to note that the relation is not an assignment, but an equality (or an inequality): both sides of the equation will be solved in a way that satisfies the constraint; this means that the list of constraints can be rearranged; for instance, the example above can be rewritten as:
red.end = blue.start × 1.0 - 8.0
In general, for the sake of convenience and readability, you should arrange your constraints in reading order, from leading to trailing edge, from top to bottom. You should also favour whole numbers for multipliers, and positive numbers for constants.
Solving the layout
Systems of linear equations can have one solution, multiple solutions, or even no solution at all. Additionally, for performance reasons, you don’t really want to recompute all the solutions every time.
Back in 1998, the Cassowary algorithm for solving linear arithmetic constraints was published by Greg J. Badros and Alan Borning, alongside its implementation in C++, Smalltalk, and Java. The Cassowary algorithm tries to solve a system of linear equations by finding its optimal solution; additionally, it does so incrementally, which makes it very useful for user interfaces.
Over the past decade various platforms and toolkits started providing layout managers based on constraints, and most of them used the Cassowary algorithm. The first one was Apple’s AutoLayout, in 2011; in 2016, Google added a ConstraintLayout to the Android SDK.
In 2016, Endless implemented a constraint layout for GTK 3 in a library called Emeus. Starting from that work, GTK 4 now has a GtkConstraintLayout layout manager available for application and widget developers.
The machinery that implements the constraint solver is private to GTK, but the public API provides a layout manager that you can assign to your GtkWidget class, and an immutable GtkConstraint object that describes each constraint you wish to add to the layout, binding two widgets together.
Guiding the constraints
Constraints use widgets as sources and targets, but there are cases when you want to bind a widget attribute to a rectangular region that does not really draw anything on screen. You could add a dummy widget to the layout, and then set its opacity to 0 to avoid it being rendered, but that would add unnecessary overhead to the scene. Instead, GTK provides GtkConstraintGuide, and object whose only job is to contribute to the layout:
In the example above, only the widgets marked as “Child 1” and “Child 2” are going to be visible, while the guide is going to be an empty space.
Guides have a minimum, natural (or preferred), and maximum size. All of them are constraints, which means you can use guides not just as helpers for alignment, but also as flexible spaces in a layout that can grow and shrink.
Describing constraints in a layout
Constraints can be added programmatically, but like many things in GTK, they can also be described inside GtkBuilder UI files, for convenience. If you add a GtkConstraintLayout to your UI file, you can list the constraints and guides inside the special “<constraints>” element:
<object class="GtkConstraintLayout"> <constraints> <constraint target="button1" target-attribute="width" relation="eq" source="button2" source-attribute="width" /> <constraint target="button2" target-attribute="start" relation="eq" source="button1" source-attribute="end" constant="12" /> <constraint target="button1" target-attribute="start" relation="eq" source="super" source-attribute="start" constant="12" /> <constraint target="button2" target-attribute="end" relation="eq" source="super" source-attribute="end" constant="-12"/> </constraints> </object>
You can also describe a guide, using the “<guide>” custom element:
<constraints> <guide min-width="100" max-width="500" /> </constraints>
Visual Format Language
Aside from XML, constraints can also be described using a compact syntax called “Visual Format Language”. VFL descriptions are row and column oriented: you describe each row and column in the layout using a line that visually resembles the layout you’re implementing, for instance:
|-[findButton]-[findEntry(<=250)]-[findNext][findPrev]-|
Describes an horizontal layout where the findButton
widget is separated from the leading edge of the layout manager by some default space, and followed by the same default amount of space; then by the findEntry
widget, which is meant to be at most 250 pixels wide. After the findEntry
widget we have some default space again, followed by two widgets, findNext
and findPrev
, flush one against the other; finally, these two widgets are separated from the trailing edge of the layout manager by the default amount of space.
Using the VFL notation, GtkConstraintLayout will create all the required constraints without necessarily having to describe them all manually.
It’s important to note that VFL cannot describe all possible constraints; in some cases you will need to create them using GtkConstraint’s API.
Limits of a constraint layout
Constraint layouts are immensely flexible because they can implement any layout policy. This flexibility comes at a cost:
- your layout may have too many solutions, which makes it ambiguous and unstable; this can be problematic, especially if your layout is very complex
- your layout may not have any solution. This is usually the case when you’re not using enough constraints; a rule of thumb is to use at least two constraints per target per dimension, since all widgets should have a defined position and size
- the same layout can be described by different series of constraints; in some cases it’s virtually impossible to say which approach is better, which means you will have to experiment, especially when it comes to layouts that dynamically add or remove UI elements, or that allow user interactions like dragging UI elements around
Additionally, at larger scales, a local, ad hoc layout manager may very well be more performant than a constraint based one; if you have a list box that can grow to an unknown amount of rows you should not replace it with a constraint layout unless you measure the performance impact upfront.
Demos
Of course, since we added this new API, we also added a few demos to the GTK Demo application:
As well as a full constraints editor demo:
More information
- The Cassowary constraint solving toolkit, which links the various papers and implementations.
- Emeus, a Cassowary implementation for GTK 3
Shouldn’t it read “red.end = blue.start × 1.0 – 8.0”?
@ncp77173: Indeed; fixed.