Proposal: declarative layout system

gui

#1

I’ve been working on a small set of classes that bridge juce::Component with Facebook’s Yoga library to provide a declarative API for representing component heirarchies and layouts. It’s inspired by React.js, with which I have a long history and with which I vastly prefer writing my UIs, and so borrows a lot of its principles. Like with React, I’ve implemented my own rudimentary tree diffing algorithm for merging two component trees with minimal overhead so that you can declare your UI as a function of external state once and the framework will take care of merging subsequent changes.

A quick example component might look like this:

class Chrome : blueprint::Container
{
    using blueprint::Container::Container;

    void paint (Graphics& g) override
    {
        // You still get your standard juce::Component hooks so
        // do whatever you like in here!
    }

    auto renderChildren()
    {
        namespace bp = blueprint;

        return (
            bp::make<bp::Container>(
                {{"flex", 1.0},
                 {"padding", 10.0},
                 {"background-colour", props.getProperty("mouseOver") ? "ffa7a7ff" : "ff272727ff"}},
                bp::make<bp::Container>(
                    {{"flex", 1.0},
                     {"justify-content", center},
                     {"align-items", center}},
                    std::move(children)))
        );
    }
}

The API is very much in flux, but hopefully this gets the rough direction across.

I have a github project up with a working example, showing a case of rendering a tree based on a parameter value from an AudioProcessorValueTreeState, and merging newly constructed trees with the existing: https://github.com/nick-thompson/blueprint

There are many open questions here and I am not exactly a cpp library author (pretty sure I have string copies everywhere), but I’d like to use this work to facilitate a conversation and build this into something much better if there’s enough interest. If you are interested, please look over the existing code and consider the open questions at the bottom of the README on Github. Open an issue on the repository if you have a strong opinion for any of those questions, or feel free to just get into the code and send a PR (i.e. the assignment of string properties to Yoga flex properties is probably garbage currently :P)

cc @tpoole and @ed95 – a lot of this is born out of the conversation at ADC around JUCE considering React Native (which I support for desktop apps) and trying to provide something similar/familiar that seamlessly works for plugin development. I’d love to get your input on this, and if, down the road, this ends up looking like something worth including in JUCE directly, I’m definitely open to it.

Let me know what you think!


#2

That return statement makes me cry because you can’t immediately tell what is being returned or what that object was initialized with. It’s just a mass of curly braces, commas, and strings :confounded:


#3

That’s exactly why I never got friend with javascript…
Nice to see, you can do that in C++ now, big step forward…

But back to topic, anything that helps to separate GUI from business code is great, so that Designers can design and coders can code…
(but that’s why I would prefer to design in a tool, rather than in a code editor)


#4

Usually, if the API is good enough a UI tool could be built to easily create and render this code behind the scenes. If you got a base framework that worked really well, then everything else could come naturally.


#5

Not to mention auto in the function signature


#6

In my opinion, it’s far easier to look at the above and rapidly get a sense of what the layout will look like than it is with, e.g.:

void resized() override
{
    auto area = getLocalBounds();
    auto headerFooterHeight = 36;
    header.setBounds (area.removeFromTop    (headerFooterHeight));
    footer.setBounds (area.removeFromBottom (headerFooterHeight));
    auto sideBarArea = area.removeFromRight (jmax (80, area.getWidth() / 4));
    sidebar.setBounds (sideBarArea);
    auto sideItemHeight = 40;
    auto sideItemMargin = 5;
    sideItemA.setBounds (sideBarArea.removeFromTop (sideItemHeight).reduced (sideItemMargin));
    sideItemB.setBounds (sideBarArea.removeFromTop (sideItemHeight).reduced (sideItemMargin));
    sideItemC.setBounds (sideBarArea.removeFromTop (sideItemHeight).reduced (sideItemMargin));
    auto contentItemHeight = 24;
    orangeContent.setBounds     (area.removeFromTop (contentItemHeight));
    limeContent.setBounds       (area.removeFromTop (contentItemHeight));
    grapefruitContent.setBounds (area.removeFromTop (contentItemHeight));
    lemonContent.setBounds      (area.removeFromTop (contentItemHeight));
}

And that code is taken from the latest tutorial on advanced layout techniques with JUCE. But this is just a classic conversation between declarative and imperative programming paradigms, for which we can find all angles of the debate elsewhere on the internet.

To the point about syntactic preferences, imo it’s not an important detail of the system, and indeed it’d be pretty easy to write some sort of syntactic sugar tool on top of the provided API. This is exactly what JSX is to React. Unfortunately with React, nobody cared to look at the bigger picture initially because either the native JS was “too ugly” or the JSX invoked a “I can’t have brackets in my javascript!” response.

@TonyAtHarrison that’s likely more a result of my inexperience writing C++ libraries than it is an indicator of the library’s application. My hope is to communicate a pattern and let the gurus tell me where the C++ is wrong. Either way, I’ve been considering that exact detail of the API quite a lot today and believe I have a better approach for that bit. Updates to come!


#7

That is definitely true, if you are used to create flexbox layouts a lot. For me personally I created a elaborate layout machine for nested layouts, in the style of QVBoxLayout etc. But not to code, but rather have an editor and drag around the widgets and align them automatically, until I and my client are happy. And that it will still support arbitrary sizes (i.e. adapt to changes by some kind of proportional rules).

Also one must admit, that the tutorial you quoted doesn’t even do anything fancy, so for somebody, who knows flexbox already, this is an amazing approach.

From my days work experience though, we end up getting a Zeplin project shown, and then start to implement that thing from scratch. I still think how we can turn the projects the designer do on their tools already automatically into a native program.


#8

Right, agreed, if you’ve never dealt with the flexbox spec, then this can be quite intimidating. I also don’t intend for this layout sytem to solve everybody’s problems, but if you’ve worked in React/React Native and are seeking something familiar in the JUCE ecosystem it might be right up your alley.

Don’t get me wrong, I’d love to see a PhotoShop/AffinityDesigner/Illustrator/Sketch plugin that can export a fully featured juce::Component that automagically binds my parameter interactions etc. But to my knowledge, that tool still doesn’t exist, despite so many new products promising just that.

So for the time being if I can make my experience a little easier while I still have to write the code manually, I’ll take it :slight_smile:


#9

Which ones?

I have a lot of past experience writing apps that design things and export. I once pretty much did a whole RuledCanvas type things with resizable objects, layers etc (Fireworks type).

I know what I have in my head but I still am green with C++ to offer any rational advice on the implementation.


#10

I very much agree it would be nice to keep the “layout” aspect all in one place. In our plugins we currently use the rectangle slicing approach you mentioned with our “setup” in the constructor, and it’s made for some very lengthy and messy organization :disappointed_relieved:

Here’s a quick example of an approach I’ve been playing around with, it uses a simple layout node struct that has some functions to set component attributes (like colour, properties, or a juce::Component::Positioner) that can be daisy chained together. Behind the scenes it’s just a wrapper for various juce::Component methods and for adding component children, but it allows you to write your UI layout directly instead of writing hierarchy, positioning, etc. all spread out:

// in the constructor of my main component, with members:
    // juce::GroupComponent myGroup;
    // juce::Slider mySlider[2];

ComponentLayout::create(
        // create() takes an initial Positioner object, which controls the top level component
        new ComponentLayout::FlexPositioner(
            *this,
            juce::FlexBox(),
            juce::FlexItem()
        ),
        // we also pass an initial bounds for the top level component, so that
        // our top flex positioner knows what area to work on
        juce::Rectangle<int>(600, 400),
    {
    ComponentLayout::Node(myGroup) 
    .withPositioner<ComponentLayout::FlexPositioner>(
        juce::FlexBox(
            juce::FlexBox::JustifyContent::spaceAround
        ),
        juce::FlexItem()
            .withMinWidth(400)
            .withMinHeight(200)
    )
    .withProperties({
        {juce::Identifier("foobar"), juce::var(24)}
    })
    .withColours({
        {juce::GroupComponent::outlineColourId, juce::Colours::red}
    })
    .withChildren({
        ComponentLayout::Node(mySlider[0])
        .withPositioner<ComponentLayout::FlexPositioner>(
            juce::FlexBox(),
            juce::FlexItem()
                .withMinWidth(100)
                .withMinHeight(30)
        ),
        ComponentLayout::Node(mySlider[1])
        .withPositioner<ComponentLayout::FlexPositioner>(
            juce::FlexBox(),
            juce::FlexItem()
                .withMinWidth(100)
                .withMinHeight(30)
        )
        })
    });

with the result:

Here the layout isn’t hard-coded around FlexBox per say, as long you pass in a juce::Component::Positioner subclass you can do whatever you want to the bounds without the need for parsing

Here’s the full header with the ComponentLayout classes:

ComponentLayout.h (4.4 KB)


#11

I think this looks very promising. Something we’d like to do in the future is to improve the visibility of third-party JUCE modules, possibly hosting then in some kind of communal space, and this looks like a great fit for that.


#12

We are using a json layout system. I want to advocate moving all the layouting out of the code. I makes designing an app much faster. Ours looks like this:

{
  "type":       "stack",
  "size":       [0.25, 1],
  "anchor":     "right",
  "direction":  "horizontalEven",
  "children":   [
    {
      "id":       "save",
      "styleId":  "path_button",
      "type":     "button",
      "size":     [-1, 1],
      "padding":  [0],
      "iconFile": "save_svg",
      "toggle":   false
    },
    {
      "id":       "undo",
      "styleId":  "path_button",
      "type":     "button",
      "size":     [-1, 1],
      "padding":  [0],
      "iconFile": "undo_svg",
      "toggle":   false
    },
    {
      "id":       "redo",
      "styleId":  "path_button",
      "type":     "button",
      "size":     [-1, 1],
      "padding":  [0],
      "iconFile": "redo_svg",
      "toggle":   false
    },
    {
      "id":       "preferences",
      "styleId":  "path_button",
      "type":     "button",
      "size":     [-1, 1],
      "padding":  [0],
      "iconFile": "preferences_svg",
      "toggle":   false
    }
  ]
}

When you save the layout file it automatically reloads everything on the fly. In code you get a single layout component. If you need to connect to a slider you query the layout for the id of the component.
It’s not a highly professional piece of software, but it works great for us. I’d love seeing something QML-like for JUCE.


#13

Same approach here, I created the ffLayouts module almost 3 years ago. My thinking was also, that GUI changes could be made by the designer (the person, not a software). Only difference is, that I used ValueTree instead of JSON.

However, this was solving hierarchical, resizable layouts. I understand that a declarative layout also involves behaviour, so it makes sense to be more in the code. Sure, it could use the JUCE javascript engine (?).

To me it makes perfect sense to put the different approaches in modules, so everybody can pick their favourite approach.

(Now I am tempted to get back to my project and fix the half done editor and add CSS style colour schemes :wink: )


#14

Thanks guys, I appreciate the positive energy!

One of the nice things about writing this declarative style API in plain old C++ is that you can define your layout properties wherever you like. You could have a totally separate file that initializes a bunch of std::initializer_list variables named however you like so that when you’re writing the component composition you can just bp::make<bp::Container>(rootLayoutId, children...).

I think also that a declarative API lends itself particularly well to some kind of GUI Editor frontend. Consider the Yoga Playground for example: https://yogalayout.com/playground. They let you add and adjust layout nodes just by setting relevant properties and watching the layout flow. When you’re done they automatically spit out some code for you, because under the hood the code you need looks really similar to the system they made: a set of nodes that each has properties and some children ordering. It’s up to the implementation underneath the declarative API that actually resolves the layout constraints given the properties.

I’ve already made some big changes in my local branch since my first post here, I will update shortly!


#15

You know I was thinking about this.

It seems to me from a complete naive POV, that an external Builder pattern that had like a strategy to configure each node as it was being built based off a class and a .json file seems pretty concrete even from the perspective of know nothing of the internals of JUCE Component (yet).

Maybe turn this problem inside out? Maybe in code could be realized if you solved the building outside?