Layout-based UIs

Supported bindings: ossia, others are work-in-progress

To define a custom UI, one has to add a struct ui in the processor definition.

struct MyProcessor {
  struct ui {
   
  };
};

By default, this will do nothing: we have to fill it. ui will be the top-level widget. Child widgets can be added simply by defining struct members. Containers are defined by adding a layout() function which returns an enum value, which may currently be any of the following names:

hbox,
vbox,
container,
group,
split,
tabs,
grid,
spacing,
control,
custom

For instance:

struct ui
{
  static constexpr auto layout() { enum { hbox } d{}; return d; }
  struct {
    static constexpr auto layout() { enum { vbox } d{}; return d; }
    const char* text = "text";
    decltype(&ins::int_ctl) int_widget = &ins::int_ctl;
  } widgets;

  struct {
    static constexpr auto layout() { enum { spacing } d{}; return d; }
    static constexpr auto width() { return 20; }
    static constexpr auto height() { return 20; }
  } a_spacing;

  const char* text = "text2";
};

This defines, conceptually, the following layout:

|-----------------------------|
|  |  text  |                 |
|  |        |                 |
|  | =widg= |  <20px>  text2  |
|  |        |                 |
|  |        |                 |
|-----------------------------|

Layouts

HBox, VBox

These will layout things either horizontally or vertically.

Split

Each children will be separated by a split line (thus generally one would use it to separate layouts).

Grid

This will layout children items in a grid.

Either of rows() and columns() properties can be defined, but not both:

static constexpr auto rows() { return 3; }
static constexpr auto columns() { return 3; }

If columns() is defined, children widget will be laid out in the first row until the column count is reached, then in the second row, etc. until there are no more children items, and conversely if rows() is defined.

That is, given:

struct {
  static constexpr auto layout() { enum { grid } d{}; return d; }
  static constexpr auto columns() { return 3; }
  const char* text1 = "A";
  const char* text2 = "B";
  const char* text3 = "C";
  const char* text4 = "D";
  const char* text5 = "E";
} a_grid;

The layout will be:

|---------|
| A  B  C | 
| D  E    |
|---------|

Instead, if rows() is defined to 3:

|------|
| A  D | 
| B  E | 
| C    |
|------|

Tabs

Tabs will display children items in tabs. Each children item should have a name() property which will be shown in the tab bar.

struct {
  static constexpr auto layout() { enum { tabs } d{}; return d; }
  struct {
    static constexpr auto layout() { enum { hbox } d{}; return d; }
    static constexpr auto name() { return "First tab"; }
    const char* text1 = "A";
    const char* text2 = "B";
  } a_hbox;
  struct {
    static constexpr auto layout() { enum { hbox } d{}; return d; }
    static constexpr auto name() { return "Second tab"; }
    const char* text3 = "C";
    const char* text4 = "D";
  } a_vbox;
} a_tabs;

Properties

Background color

Background colors can be chosen from a standardized set: for now, those are fairly abstract to allow things to work in a variety of environments.

darker,
dark,
mid,
light,
lighter

Setting the color is done by adding this to a layout:

static constexpr auto background() { enum { dark } d{}; return d; }

Explicit positioning

In "group" or "container" layouts, widgets will not be positioned automatically. x and y methods can be used for that.

static constexpr auto x() { return 20; }
static constexpr auto y() { return 20; }

Explicit sizing

Containers can be given an explicit (device independent) pixel size with

static constexpr auto width() { return 100; }
static constexpr auto height() { return 50; }

Otherwise, things will be made to fit in a best-effort way.

Items

Text labels

The simplest item is the text label: simply adding a const char* member is sufficient.

Controls

One can add a control (either input or output) simply by adding a member pointer to it:

struct MyProcessor {
  struct ins {
    halp::hslider_f32<"Foo"> foo;
  } inputs;

  struct ui
  {
    static constexpr auto layout() { enum { hbox } d{}; return d; }
    const char* text = "text";
    decltype(&ins::foo) int_widget = &ins::foo;
  };
};

The syntax without helpers currently needs some repeating as C++ does not yet allow auto as member fields, otherwise it'd just be:

auto int_widget = &ins::foo;

Helpers

Helpers simplify common tasks ; here, C++20 designated-initializers allow us to have a very pretty API and reduce repetitions:

Widget helpers

halp::label l1{
    .text = "some long foo"
  , .x = 100
  , .y = 25
};

halp::item<&ins::foo> widget{
    .x = 75
  , .y = 50
};

Properties helpers

struct ui {
 // If your compiler is recent enough you can do this, 
 // otherwise layout and background enums have to be qualified:
 using enum halp::colors;
 using enum halp::layouts;
 
 halp_meta(name, "Main")
 halp_meta(layout, hbox)
 halp_meta(background, mid)

 struct {
   halp_meta(name, "Widget")
   halp_meta(layout, vbox)
   halp_meta(background, dark)
   halp::item<&ins::int_ctl> widget;
   halp::item<&outs::measure> widget2;
 } widgets;
};