Constraint-based layout for Scenic—a proof of concept

By: on September 23, 2020

Scenic is an Elixir UI framework intended for fixed-screen devices. In Scenic, UIs are constructed and updated by modifying a scene graph, i.e. a tree of graphical elements such as shapes, buttons and text fields. Elements are positioned on screen by specifying their x,y coordinates.

In this post, I am going to present Jurby, a proof of concept that demonstrates how constraint-based layouting capabilities could be added to Scenic. In constraint-based layout, relations between UI elements are declared and an underlying constraint solver calculates concrete positions for the elements. Jurby uses Furlong, which is a port of the Kiwisolver constraint solver. The team behind Kiwisolver also develop Enaml, a sophisticated constraint-based UI framework. Jurby (clumsily) tries to emulate a small part of Enaml’s functionality.

I have used Scenic in the past for building a Solar PV dashboard, which incorporated calls to a weather forecast API. In the remainder of this post, I will build an example configuration screen that allows the user to input latitude, longtitude, API key and password. The screen will also have a title and two buttons.

The Scenic code to create these elements is:

graph =
  Graph.build(font: :roboto, font_size: 24)
  |> text("Weather API Setup", id: :label_setup, font_size: 36)
  |> text("Latitude:", id: :label_lat)
  |> text("Longtitude:", id: :label_long)
  |> text("Api Key:", id: :label_key)
  |> text("Password:", id: :label_pass)
  |> text_field("", id: :text_lat)
  |> text_field("", id: :text_long)
  |> text_field("", id: :text_key)
  |> text_field("", id: :text_pass)
  |> button("OK", id: :btn_ok)
  |> button("Cancel", id: :btn_cancel)

As I have not positioned any of the elements explicitly, they all appear at their default position of 0,0:

While I could work out appropriate positions by hand, I would prefer the computer to derive them from a logical layout specification. I want to have placed from top to bottom:

  • the title
  • the latitude label and the latitude text field
  • the longtitude label and the longtitude text field
  • the api key label and the api key text field
  • the password label and the password text field
  • the ok button and the cancel button

A vbox (vertical box) is a container that lays out its elements from top to bottom. I also have elements that I want to have laid out horizontally, e.g. the (label, text field)-pairs and the pair of buttons. This can be achieved by placing said elements in an hbox. An hbox (horizontal box) is just like a vbox, except that it places its elements from left to right. In Jurby, vbox and hbox look like this:

Just for the record, Enaml’s boxes are more sophisticated. By default, Jurby’s vbox and hbox do not insert any space between their elements. To separate elements visually, two types of spacer can be added: fixed size and expandable. To lay out my two buttons from left to right with a small space in between, I would declare:

hbox([:btn_ok, space(), :btn_cancel])

The full layout, as described above, can be expressed as follows:

vbox([
  :label_setup,
  hbox([:label_lat, gspace(), :text_lat]),
  hbox([:label_long, gspace(), :text_long]),
  hbox([:label_key, gspace(), :text_key]),
  hbox([:label_pass, gspace(), :text_pass]),
  hbox([:btn_ok, space(), :btn_cancel])
])

In order to calculate positions, Jurby needs to know the size of the window. Given a Scenic graph, the window size and a layout specification, Jurby’s layout function will create and solve a corresponding constraint system and then assign the resulting positions and dimensions to the elements of the graph. In the code snippet shown below, the original graph is piped into the layout function, which returns an updated graph:

graph =
  Graph.build(font: :roboto, font_size: 24)
  |> text("Weather API Setup", id: :label_setup, font_size: 36)
  |> text("Latitude:", id: :label_lat)
  |> text("Longtitude:", id: :label_long)
  |> text("Api Key:", id: :label_key)
  |> text("Password:", id: :label_pass)
  |> text_field("", id: :text_lat)
  |> text_field("", id: :text_long)
  |> text_field("", id: :text_key)
  |> text_field("", id: :text_pass)
  |> button("OK", id: :btn_ok)
  |> button("Cancel", id: :btn_cancel)
  |> layout(
    width,
    height,
    vbox([
      :label_setup,
      hbox([:label_lat, gspace(), :text_lat]),
      hbox([:label_long, gspace(), :text_long]),
      hbox([:label_key, gspace(), :text_key]),
      hbox([:label_pass, gspace(), :text_pass]),
      hbox([:btn_ok, space(), :btn_cancel])
    ]))

The resulting UI is almost good:

The overlap between the labels and text fields is due to some shortcomings in Jurby, which would obviously have to be ironed out in a more serious implementation. However, with the following refinements, the UI becomes acceptable:

|> layout(
  width,
  height,
  vbox([
    :label_setup,
    space(size: 10),
    hbox([:label_lat, gspace(), :text_lat], pin_last: true),
    hbox([:label_long, gspace(), :text_long], pin_last: true),
    hbox([:label_key, gspace(), :text_key], pin_last: true),
    hbox([:label_pass, gspace(), :text_pass], pin_last: true),
    space(size: 10),
    hbox([:btn_ok, space(), :btn_cancel], pin_first: false, pin_last: true)],
    left_margin: 10,
    top_margin: 10,
    right_margin: 10,
    bottom_margin: 10
  ),
  [
    same(:width, [:text_lat, :text_long, :text_key, :text_pass]),
    same(:width, [:btn_ok, :btn_cancel]),
    same(:right, [:btn_cancel, :text_pass])
  ]
)

In the above snippet, I

  • added two extra spacers to the vbox
  • gave the vbox a margin
  • asked the text field hboxes to pin their last element’s right edge to the hbox’s right edge

I also specified some additional constraints. These can relate elements regardless of their logical position in the layout specification. I asked that:

  • the four text fields have the same width
  • both buttons have the same width
  • the right edge of the cancel button be at the same x-coordinate as the right edge of the password text field; for this to work, the hbox must not pin the left edge of its first element to the left edge of the content area

I think this demonstrates that a reasonably pleasant constraint-based layout mechanism could be built for Scenic. While Furlong seems to be a good base for such an undertaking, Jurby should be re-written from scratch, as the underlying code is incomplete and somewhat ungainly. (The clue is in the name; see “The Meaning of Liff”.) A careful review of Enaml would be a good starting point.

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>

*