Tuesday, October 2, 2012

User interfaces and flightless birds

There are several ways to build a graphical user interface.

The oldest one is absolute positioning. For every button, label, etc., you specify the position and the size yourself (usually in pixels). Anybody who has ever used MFC knows why this is a bad idea. If you want a resizable window, you'll have to catch the window events yourself, and calculate/set the position and size for each widget. Translations usually break the layout; your best bet would be to make separate dialogs for every language.

A very popular method is the box/packing/struts/glue/springs model. It might even have a proper technical name. You define a layout using horizontal and vertical containers, anchor points, springs, fixed spacings, and the toolkit does the layout for you. Much better already!

Another method is the constraints-based layout, and it has been slowly gaining popularity. It is not new; the first Lisp implementations appeared in the early 90s. But for example, it is the recommended way to define user interfaces in Cocoa since OS X Lion, and Adobe also uses it in their Adam and Eve framework. In a constraint, you can say, "okay, I want this button left-aligned with this widget, and somewhere below the title", or "I want this field to be just as wide as that one, and preferably somewhere between 40 and 250". The widgets are then laid out according to your wish list.

UI requirements for Hexahedra

For the readers who just tuned in: in Hexahedra, all mods/plugins run on the server. This means we have the following requirements for the in-game user interface:
  • Network transparency.
  • Has to be able to adapt to different resolutions and screen layouts.
  • Good localization support. Not just translations to other languages, but also adapting to right-to-left languages.
  • Support for client-side themes.
  • Must be easy to define.
Given all that, the constraints-based method sounds pretty good.

The first thing I need is something that eats constraints and poops a widget layout. Turns out such a beast is called a linear constraint solver. There are several, such as Indigo, Ultraviolet, and LP-solve. But the best candidate for the task at hand is Cassowary.

Cassowary hasn't been updated in a decade. It doesn't build on my system without tweaking, it has a bug or two, valgrind reports memory leaks, and defining constraints with it is pretty awkward. Besides, I really want to know how it works in detail.

You can guess where this is heading.


So yeah. Rhea. It still needs to be documented and packaged properly, but it's definitely in a usable state.

The user interface library that is sitting on top of Rhea is still under heavy development, but I can show you where it's headed. Let's set up a frame with three text labels in it, and use Rhea to define and solve the constraints:

  ui::theme t; 
  ui::label a (t, L"AAA"), b (t, L"BBBBBBBBBB"), c (t, L"CCCCC");
  ui::frame box;

  rhea::simplex_solver s;

      box.top == 0,       // Keep the frame's corner at 0,0
      box.left == 0,

      a.top == box.top,   // Align 'a' in the top left corner
      a.left == box.left,

      b.top == box.top,   // Align 'b' along the top
      b.left >= a.right,  // Make sure b is right of a

      // Make sure b is horizontally centered, but make it a
      // weak constraint.  If there's not enough room, b will
      // be placed as close to the center as possible, but always
      // to the right of a (the stronger constraint).
      // Note that 'h_center' is actually a linear expression;
      // the frame class defines it (quite literally) as
      //  "left / 2 + right / 2".
      {b.h_center == box.h_center, rhea::strength::weak()},
      b.right <= box.right,   // Make sure b still fits

      c.top >= a.bottom,      // Place 'c' somewhere below 'a'
      c.bottom <= box.bottom, // Make sure it's inside the frame
      c.left == box.left,     // Keep it left-aligned
      c.right <= box.right

  // And yes that is actually C++.

Now that the user interface is defined, the labels will ask the theme for a preferred size (based on the font and the text), and we can tell the solver what we want. In this example, the theme simply assumes every character in a text label is a 20x20 square. Now, let's say we want the frame to be 200 x 150:


  s.suggest_value(box.width, 200);
  s.suggest_value(box.height, 150);

"Edit variables" are special variables in the Cassowary algorithm. A new value can be suggested for them, and the existing constraints are re-solved with a minimum of overhead. Speed is important to keep the UI feel responsive. The widgets now have the following dimensions:  (shown [left, top ; right, bottom](width x height) .)

    box : [0,0;220,150](220x150)

    a : [0,0;60,20](60x20)
    b : [60,0;220,20](160x20)
    c : [0,130;100,150](100x20)

We suggested a width of 200; the solver decided to make it 20 wider, so it ended up 220 x 150 instead. There was one weak constraint it could drop: 'b' is not centered inside the box. But given all the other requirements, the box just couldn't get any smaller than 220. (That's pretty neat: I never actually had to specify this number anywhere, the solver figured it out for me.)

What if we suggest a width of 600 instead?

    box : [0,0;600,150](600x150)

    a : [0,0;60,20](60x20)
    b : [220,0;380,20](160x20)
    c : [0,20;100,40](100x20)

The 'B' label now has enough breathing space to be centered properly.

So we have a resizable window, where an element is centered, unless it bumps into another. The window has a proper minimum size. The layout will also adjust automatically whenever the labels are translated to another language, or a different theme is used. Not bad for 13 really short lines of c++.

Up next

There have been a few other big changes in Hexahedra, so the next blog post will probably be about ENet (the new UDP network protocol) or IQM (the new model format). But UI Part 2 will look at the behavior side of the widgets, hopefully with some actual video footage.