77 F
New York
Tuesday, June 28, 2022

Minimum Viable Declarative GUI in C++

Must Read
lifeadmin
lifeadminhttps://onenews.life
OneNews provides trusted World and US news as well as local and regional perspectives.

Minimum viable declarative GUI in C++

Dev • Mar 22, 2022 • Jean-Michaël Celerier

Here is the code of a minimal GUI toolkit for defining declarative UIs:

// That's it. That's the toolkit.

Well, this plus some extremely routine parts of “ISO/IEC JTC1 SC22 WG21 N 4860 – Programming Languages : C++”, such as… strings… variable declarations… struct definitions… enums… ints… and a couple others. Code so post-modern it circled back to orthodoxy.

So, here is how one uses the C++ language plus this, uh, toolkit to declare a simple item, say, a text label:

struct {
  const char* my_label = "foo";
} my_ui;

Believe me, dear reader, that is actually enough user code to be able to show the following UI:

The UI

By user code, I mean that all the “nice” UI work can be done in, say, foo.hpp:

#pragma once
constexpr struct {
  const char* my_label = "foo";
} my_ui;

without requiring any custom header. Some yet-unknown .cpp file will include foo.hpp, and, no matter what the content of my_ui is, will execute a nice user interface from it. Anyone will be able to create independent libraries which will render UIs according to whatever platform-specific intricacies there are, but the actual UI code will be entirely independent from anything: peak separation of concerns is achieved.

Advanced UI

The code above can be trivially extended to showing not one, but TWO labels:

struct {
  const char* my_label = "foo";
  const char* other_label = "bar";
} my_ui;

The result:

The UI

Now, one notices how the visual position of the labels in the UI has the good taste of matching
the one in the code: the language provides us with a native primitive for ordering disjoint things, so why not just use it ?

Of course, just putting elements in a vertical row may not be sufficient for every UI on earth.

Let’s declare that our UI embraces horizontality instead, in the simplest way possible that I could find:

struct {
  enum { hbox }; 
  const char* my_label = "foo";
  const char* other_label = "bar";
} my_ui;

And we get:

The UI

Nested UI

We may want to imbricate multiple things:

struct {
  enum { hbox }; 

  struct {
    enum { vbox }; 
    const char* a = "foo";
    const char* b = "bar";
  } group1;
  
  struct {
    enum { vbox }; 
    const char* a = "quz";
    const char* b = "qux";
  } group2;
} my_ui;

Gives:

The UI

We may want to go a bit deeper:

The UI

The controls are pointers to simple structs which define a couple of metadatas in the way mentioned in the reflection blogpost.

A nice thing here is that unlike many frameworks, there’s not much memory to leak, as
the very structure of C++ structs is used to define the hierarchical UI.

Of course, there has to be a rendering framework somewhere: the nice thing is that the UI code has no dependency on the actual framework.
Instead, some separate glue code will transmute our declarative UI definition. Thus, which framework exactly mostly does not matter,
for a large amount of potential UIs.

To give an example, the screenshots above were taken with a first prototype of renderer which generates QML / QtQuick code (and runs it).

Here is the exact same UI specification, but rendered with Nuklear instead, which is a fairly different UI paradigm code-wise:

The UI

I have spent an hour on it without prior knowledge of the library and did not find how to do splitters and group boxes well with it, but I’m sure there is a way !

Meanwhile… t r e e s.

The binding from this kind of UI specification to Nuklear holds in a ~200 lines (fairly dirty) file.

Here is the core loop, for reference:

void recurseItem(const auto& item)
{
  avnd::for_each_field_ref(item, [this] (auto& child) {
    this->createItem(child);      
  });
}

template<typename Item>
void createItem(const Item& item)
{
  constexpr int child_count = boost::pfr::tuple_size_v<Item>;
  if constexpr(requires { item.spacing; })
  {
    nk_label(ctx, " ", NK_TEXT_LEFT);
  }
  if constexpr(requires { item.hbox; })
  {
    nk_layout_row_dynamic(ctx, row_height, child_count);
    recurseItem(item);
  }
  else if constexpr(requires { item.vbox; })
  {
    recurseItem(item);
  }
  else if constexpr(requires { item.split; })
  {
    if (nk_tree_push(ctx, NK_TREE_TAB, "Split", NK_MINIMIZED)) {
      recurseItem(item);
      nk_tree_pop(ctx);
    }
  }
  else if constexpr(requires { item.group; })
  {
    if (nk_tree_push(ctx, NK_TREE_TAB, c_str(Item::name()), NK_MINIMIZED)) {
      recurseItem(item);
      nk_tree_pop(ctx);
    }
  }
  else if constexpr(requires { item.tabs; })
  {
    createTabs(item);
  } 
  else
  {
    // Normal widget
    createWidget(item);
  }
}

Glory to the post-library era and to declarative, struct & enum-based specification !

The only remaining step is to integrate it with score’s internal widget set and there will finally be a
way to specify audio and media plug-ins in a way entirely independent from any host app or framework, with zero-cost abstractions.

Here is the example plug-in showcased above, done in two ways:

A few small additional things may be useful:

  • An API for loading images, for instance to render pretty SVG background à la VCVRack.
  • Maybe investigating reactive properties. I have some ideas for those, which may actually fit in these margins 🙂

This work can be tried with the avendish library.

Introducing ossia score 3

Read More

- Advertisement -spot_img

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisement -spot_img
Latest News

Hurricanes, Rangers each bring confidence, promising history into Game 7

RALEIGH, N.C. -- Recent history would say one thing: The Carolina Hurricanes are going to win Game 7 of...
- Advertisement -spot_img

More Articles Like This

- Advertisement -spot_img