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 :
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.
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:
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.
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:
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;
the callAsync looks frankly like a hack;
isn’t there something general here I can extract as a library?
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)
Thanks again all, it’s at least a bit comforting to know I’m not alone
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);
}
};