Design pattern for bottom-up components layout

Hi JUCErs,

I’m keep bumping into a seemingly simple design issue that I can’t figure out a good pattern for; any good souls care to give their opinion?

In JUCE the component hierarchy seems to be designed for top-down layouts : parents set the bounds of their children, who have no say in these bounds. All examples I’ve seen work like this: the parent’s resize() gets called, it splits its bounds and calls setBounds() on its children, which triggers their resize() etc.

I regularly need a bottom-up or mixed layout strategy, where the parent sets some aspects of the layout, but adapts (partially) to the size fixed by its children. For instance :

Here:

  • A determines B and C’s height
  • B sets its own width according to its given height and other internal data (not known to A)
  • C takes the rest of the width left by B

A major constraint: A knows from B only that it is a Component; it’s trivial to subclass B and add a method returning its width, but I am looking for a more universal solution, where B would actively call e.g. setSize().

I’ve tried to make A listen to B’s size change (ComponentListener) and resize C accordingly but the resulting code looks quite ad-hoc.

Any recommendation?
Thanks a lot in advance!

This might be a good use case for flex box classes if I remember correctly.

This is something I’ve been looking at recently, and something that JUCE seems to do differently to other frameworks.

If you look at HTML, when you create say a button and give it some text, you don’t need to specify a size for it as its size is automatically determined by the content. You can add a padding and a border width on top of that again, without specifying an exact size of the button - this is all thanks to CSS’s Box Model.

Sadly, JUCE doesn’t have any such Box Model - however it’s not difficult to implement.

For a text button, you can find the minimum required size of the content using juce::Font::getStringWidthFloat() and juce::Font::getHeight() (although there’s some caveats there so you might need a little more height depending on the given font). You can then easily have padding and border properties on a widget, for which juce::BorderSize is really useful. Using those 3 things, you can calculate the minimum required size for a particular widget, which you can then use in your layouts.

As @Fandusss says, juce::FlexBox is really handy at this point since you can specify minimumWidth and minimumHeight properties on a juce::FlexItem and any other items in the layout will be moved around accordingly.

I believe Qt does something similar where widgets are given a size when you create them and when you change their content - so when it comes to positioning them the parent will query the widget for its size, rather than telling the widget what size to be.

I suppose there’s no reason why a Component can’t just set its own size in its constructor or when updating its content? Then parents can just query the size and setTopLeftPosition or use FlexBox for layout.

Although I suppose FlexBox is probably setting the full bounds of the Components it is laying out? This might break things on subsequent resizes.

One method we’ve used in the past is having a method in the widget such as int getPreferredWidth() and/or int getPreferredHeight() which allows the parent to query the children for the dimension they should be using.

It could actually help with debugging since I’m sure we’ve all have the situation where a widget isn’t visible because it has 0x0 size - if you gave everything a default size you’d at least always be able to see where they are.

For OP’s case where it’s simply that B wants to maintain a consistent aspect ratio, and so change its width to match its height, that approach wouldn’t work so well.

You’d have to enforce that rule in B’s resized() method, something like:

void B::resized()
{
    setSize (getHeight(), getHeight());
}

But then I don’t think that would play nicely with juce::FlexBox as, IIRC, juce::FlexItems use their own bounds separate from the Component’s and only change the Component’s bounds at the very last minute, at which point the rest of the UI wouldn’t update accordingly.

Again, this is an oddity with JUCE since the items themselves don’t know if they’re flex items or grid items or what - whereas in HTML/CSS items do know (or assume) their parent’s display type as you set flex-specific properties on the item itself.

No matter what, for this situation I think the parent needs to know about the child’s desire to have a fixed aspect ratio.

Hi all, thanks for your answers!

Ok, good to know at least it’s not a trivial thing that I missed.

Will look into it. My initial feeling was it wasn’t appropriate for this particular problem but I might have to delve into it some more.

Yes, this is what I do already and am not satisfied with, hence the “constraint” in my original question. For one, C’s size may vary dynamically, and I want to inform A when it changed. But more importantly, I cannot always know what’s inside C. Think of C as a juce::AudioPluginEditor for instance, that is a black box and will set its size itself dynamically.

Ok, time to tell you my current architecture, which looks to me quite messy for such a simple problem (but works AFAIK). In untested pseudo-code:

struct C : Component {};
struct B : Component {
  void resized() {
    // only an example, the width could be arbitrarily more complex
    setSize(getHeight(), getHeight()); 
  }
};
struct A : Component, ComponentListener {
  A() { b.addComponentListener(this); }
  B b;
  C c;
  void resized() {
    auto bounds = getLocalBounds();
    // this will trigger B::resized(), which will compute its own width, 
    // and trigger a resize of A. On the first call, b's width is 0, in subsequent
    // calls it will have the "right" width.
    b.setBounds(bounds.removeFromRight(b.getWidth()));
    c.setBounds(bounds); // the rest of the space unclaimed by B
  }
  // called upon B's own resize
  void componentMovedOrResized(...) override {
    // callAsync makes sure resized() is called again only *after* A::resized() 
    // is done, which would lead to an incorrect size for C.
    callAsync([this] { resized(); });
  }
};

Problems:

  1. A (and therefore B and C) are laid out entirely twice, once with the incorrect width of 0 for C, once again when we have C’s actual width;
  2. the callAsync looks frankly like a hack;
  3. isn’t there something general here I can extract as a library?

It is indeed hard to come up with a generic strategy.

Two related concepts in JUCE can be found:

  1. ComponentBoundsConstrainer, but not sure it helps
  2. The TextButton has this callback in it’s LookAndFeel: getTextButtonWidthToFitText()

It would work like this:

void A::resized() override
{
    auto bounds = getLocalBounds();
    bounds.removeFromTop (30);   // actual formula is unknown from OP
    auto bWidth = b.getWidthForHeight (bounds.getHeight());

    b.setBounds (bounds.removeFromRight (bWidth));
    c.setBounds (bounds);
}

The drawback is the A needs knowledge of B, because there is no generic interface to ask for a preferred size / aspect / etc.

Flexbox sounds great, but they don’t have an aspect ratio (not even in the original CSS, there are hacks needed as well)

We do this all the time in Loopcloud. A mix of functions called getIdealHeight and some stuff to stop resized methods recursing with a boolean flag :slight_smile:

It is a real pain!

Haha we also have these functions, but call them getMinimumRequiredHeight() :laughing:

There are only two hard things in Computer Science: cache invalidation and naming things.

– Phil Karlton

1 Like

Thanks again all, it’s at least a bit comforting to know I’m not alone :slight_smile:
Some progress on my own code, in case it can help someone. Still not ideal (did not find a way to generalize it), but it’s starting to feel like a design pattern. Problems 1. and 2. from above are fixed.

struct C : Component {};
struct B : Component {
  void resized() {
    // only an example, the width could be arbitrarily more complex
    setSize(getHeight(), getHeight()); 
  }
};
struct A : Component, ComponentListener {
  A() { b.addComponentListener(this); }
  B b;
  C c;
  void resized() {
    // pass a bogus width; it will trigger B::resized(), which will compute its 
    // actual width, and call componentMovedOrResized below to finish laying out components
    b.setSize(0, getHeight());
  }
  // called upon B's resize
  void componentMovedOrResized(...) override {
    // continue the layout work:
    auto bounds = getLocalBounds();
    b.setBounds(bounds.removeFromRight(b.getWidth()); // B's correct width now
    c.setBounds(bounds);
  }
};