Cassowary / Kiwi-Solver Elixir Port

By: on August 11, 2020

A large variety of 2d diagrams and UI layouts can be described declaratively using x,y-coordinates and spatial relations. These spatial relations, or ‘constraints’, can be expressed as systems of linear in-/equalities, which can be solved automatically by a linear constraint solver. Once the solver has assigned a concrete value to each variable in the system, it has effectively calculated the optimum sizes and positions of all layout elements, and thus, the layout can be rendered.

Cassowary / Kiwi-Solver is a well-known incremental constraint solving toolkit for UI applications, with implementations available in C++, Python, JavaScript, Haxe, C, Java, Objective-C, Smalltalk, Haskell, Go, Rust, Common Lisp and Swift. I am pleased to be able to add an Elixir port (‘Furlong‘) to this list. Furlong provides the fundamental Kiwi-Solver functionality in Elixir, but does not include any conveniences built on top of the solver.

An introductory example

A 2d box can be represented by the x-coordinates of its left and right side as well as the y-coordinates of its top and bottom. For the sake of simplicity, I will omit the y-axis, leaving us with two variables, left and right, to represent an example box. Furlong uses Erlang references to identify variables.

iex(1)> b1_l = make_ref() # box 1, left and
iex(2)> b1_r = make_ref() # right x-coordinate

The core part of Furlong is the Solver, which maintains a system of constraints.

iex(3)> import Furlong.Solver
iex(4)> system = new()

We can now add to our system some simple constraints, for example, that our box be at least ten units wide. Currently, the constraints need to be specified via the functions in Furlong.Symbolics. In the future, I might add a macro that allows one to write constraints more naturally.

iex(5)> import Furlong.Symbolics
iex(6)> w1 = make_ref # width of box 1
iex(7)> system =
...(7)> system |>
...(7)> add_constraint(eq(w1, subtract(b1_r, b1_l))) |>
...(7)> add_constraint(gte(w1, 10))

Let’s assume that we have a drawing area of size maxx * maxy units. Our box is to be placed inside this area:

iex(8)> maxx = make_ref()
iex(9)> system =
...(9)> system |> 
...(9)> add_constraint(gte(b1_l, 0)) |>
...(9)> add_constraint(lte(b1_r, maxx))

Let’s set maxx to 640 units:

iex(11)> system = add_constraint(system, eq(maxx, 640))

Let’s find out where the example box has been placed by the solver:

iex(11)> value?(system, b1_l) 
0.0
iex(12)> value?(system, b1_r)
640.0

The solver made our box as big as the drawing area, which is a fine solution, indeed.

Let’s add in another box to the right of our first box, with at least a certain amount of space in between and let’s demand that the second box have exactly the same width as the first one:

iex(13)> b2_l = make_ref()
iex(14)> b2_r = make_ref()
iex(15)> w2 = make_ref()
iex(16)> spacer = make_ref()

iex(17)> system =
...(17)> system |>
...(17)> add_constraint(gte(b2_l, add(b1_r, spacer))) |>
...(17)> add_constraint(lte(b2_r, maxx)) |>
...(17)> add_constraint(eq(w2, subtract(b2_r, b2_l))) |>
...(17)> add_constraint(eq(w1, w2)) |>
...(17)> add_constraint(lte(spacer, divide(maxx, 10))) |>
...(17)> add_constraint(gte(spacer, divide(maxx, 20)))

iex(18)> {{value?(system, b1_l), value?(system, b1_r)}, {value?(system, b2_l), value?(system, b2_r)}}
{{588.0, 598.0}, {630.0, 640.0}}

Both boxes have the same width and box 2 is to the right of box 1. If we wanted a more aesthetically pleasing positioning, we could for example demand that the width of the drawing area be fully used up by the boxes and the spacer:

iex(19)> system = add_constraint(system, eq(add(add(w1, w2), spacer), maxx))

iex(20)> {{value?(system, b1_l), value?(system, b1_r)}, {value?(system, b2_l), value?(system, b2_r)}}
{{0.0, 304.0}, {336.0, 640.0}}

Finally, let’s make the drawing area resizable:

iex(21)> system = 
...(21)> system |>
...(21)> remove_constraint(eq(maxx, 640)) |>
...(21)> add_edit_variable(maxx, :strong) |> 
...(21)> suggest_value(maxx, 1000)

iex(22)> {{value?(system, b1_l), value?(system, b1_r)}, {value?(system, b2_l), value?(system, b2_r)}} 
{{0.0, 475.0}, {525.0, 1000}}

iex(23)> system = suggest_value(system, maxx, 240)
iex(24)> {{value?(system, b1_l), value?(system, b1_r)}, {value?(system, b2_l), value?(system, b2_r)}}
{{0.0, 114.0}, {126.0, 240.0}}

Conclusion

While the example above barely scratches the surface, I hope that you already have a sense of how more complex relations such as padding, equal spacing between several boxes or vertical/horizontal alignment could be specified. Be aware that not everything can be expressed with linear constraints, such as, for example, breaking text into paragraphs. If you want to use constraint-based layouts (perhaps for server-side generation of diagrams), you will probably want to create abstractions that manage obvious constraints automatically. For further information, I suggest you visit overconstrained.io or have a look at the Kiwi-Solver documentation.

Share

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*