FlexItem with a flex-basis of "auto" still need explicit height and/or width

I’m working on creating a dynamic layout for my plugin with flexboxes, but it’s not behaving the way I would expect based on HTML flexboxes.

I don’t want any of my components to grow or shrink. I am only interested in juce::FlexBox figuring out the layout for me. The components I’m adding to flexboxes are already sized. However, unless I provide explicit measurements for each flex item, they will be 0 x 0. Using the juce::FlexItem::autoValue like so seems to have no effect:

box.items.add(juce::FlexItem(component).withFlex(0, 0, juce::FlexItem::autoValue));

Instead, I have to do the following:

box.items.add(juce::FlexItem(component).withHeight(component.getHeight()).withWidth(component.getWidth()).withFlex(0, 0, juce::FlexItem::autoValue));

This is just a simple example I’ve been working on to understand juce::FlexBox (code below):

  • Main box (column)
    • Slider box (row, wraps)
      • Attack slider
      • Decay slider
      • Sustain slider
      • Release slider
    • Match input amplitude checkbox

I’ve tested the Slider box on its own, and it works exactly as expected since each of its items are manually sized like the second code snippet. Once I nest the Slider box into another flexbox, then things start behaving in ways I don’t understand. I can’t provide an exact width or height of the Slider box because I don’t know how many rows or columns the items will take up. I could calculate that, but the whole point of using a flexbox in my case is to have it figure that out for me. This is what the below code ends up looking like:

I believe that since I haven’t provided explicit measurements the Slider box is assigned a size of 0 x 0 and that is why the mock checkbox is positioned at 0, 0. The mock checkbox also seems to be growing and shrinking, even though I have specified otherwise.

Is it possible to use juce::FlexItem::autoValue to just have automatically laid out flexboxes?

This is a minimal example of the expected/desired behavior in HTML:

<!DOCTYPE html>
<html>

<head>
    <style>
        html,
        body {
            padding: 0;
            margin: 0;
            height: 100%;
        }

        .divider {
            width: 100%;
            height: 2px;
            background-color: black;
        }

        .sliders {
            display: flex;
            flex-wrap: wrap;
        }

        .plugin {
            display: flex;
            flex-direction: column;
        }

        .slider {
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            height: 150px;
            width: 150px;
            margin: 12px;
            border-radius: 50%;
            background-color: gray;
        }
    </style>
</head>

<body>
    <div class="plugin">
        <div class="sliders">
            <div class="slider">Attack</div>
            <div class="slider">Decay</div>
            <div class="slider">Sustain</div>
            <div class="slider">Release</div>
        </div>
        <div class="divider"></div>
        <div class="slider">Match input amplitude</div>
    </div>
</body>

</html>

To temporarily replace the sliders/checkboxes while I figure this out, I’ve created this square component:

#pragma once

#include "JuceHeader.hpp"

class Square : public juce::Component {
public:
  Square(juce::Colour const &color) : m_color(color) { setSize(150, 150); }

  void paint(juce::Graphics &g) override {
    g.setColour(m_color);
    g.drawRoundedRectangle(getLocalBounds().reduced(10).toFloat(), 6, 2);
  }

private:
  juce::Colour m_color;
};

And in my plugin editor, I use it as such:

#pragma once

#include "PluginProcessor.hpp"
#include "components/Square.hpp"

class PseudominEditor : public juce::AudioProcessorEditor {
public:
  PseudominEditor(juce::AudioProcessor &processor);
  ~PseudominEditor() override;

  void paint(juce::Graphics &) override;
  void resized() override;

private:
  Square m_attack = {juce::Colours::red};
  Square m_decay = {juce::Colours::blue};
  Square m_sustain = {juce::Colours::aqua};
  Square m_release = {juce::Colours::orange};
  Square m_matchInputAmplitude = {juce::Colours::purple};
  juce::FlexBox m_mainBox;
  juce::FlexBox m_slidersBox;
};
#include "PluginEditor.hpp"
#include "components/styles/Colors.hpp"

void addFlexItem(juce::FlexBox &box, juce::Component &component) {
  box.items.add(juce::FlexItem(component)
                    .withMinWidth(component.getWidth())
                    .withMinHeight(component.getHeight()));
}

PseudominEditor::PseudominEditor(juce::AudioProcessor &processor,
                                 pseudomin::PluginState &state)
    : AudioProcessorEditor(processor), m_state(state) {
  setSize(700, 250);
  setResizable(true, true);

  addAndMakeVisible(m_attack);
  addAndMakeVisible(m_decay);
  addAndMakeVisible(m_sustain);
  addAndMakeVisible(m_release);
  addAndMakeVisible(m_matchInputAmplitude);

  m_slidersBox.flexDirection = juce::FlexBox::Direction::row;
  m_slidersBox.flexWrap = juce::FlexBox::Wrap::wrap;
  m_slidersBox.alignContent = juce::FlexBox::AlignContent::flexStart;
  addFlexItem(m_slidersBox, m_attack);
  addFlexItem(m_slidersBox, m_decay);
  addFlexItem(m_slidersBox, m_sustain);
  addFlexItem(m_slidersBox, m_release);

  auto sliderHeight = m_attack.getHeight();
  auto sliderWidth = m_attack.getWidth();

  m_mainBox.flexDirection = juce::FlexBox::Direction::column;
  m_mainBox.items.add(
      juce::FlexItem(m_slidersBox).withFlex(0, 0, juce::FlexItem::autoValue));
  addFlexItem(m_mainBox, m_matchInputAmplitude);

  m_mainBox.performLayout(getLocalBounds());
}

PseudominEditor::~PseudominEditor() {}

void PseudominEditor::paint(juce::Graphics &g) {
  g.fillAll(pseudomin::colors::backgroundMain);
}

void PseudominEditor::resized() { m_mainBox.performLayout(getLocalBounds()); }

This assigns the flex-basis property of the flex item an auto value. flex-basis specifies the initial size of the item before it is grown/shrunk according to the flex-grow and flex-shrink properties.

In HTML/CSS, using auto for flex-basis means that the size of the item’s content should be used. I.e. a <p> element with text would use the width of the text.

In JUCE however, components have no awareness of their content and so the only sensible “auto” value that could be chosen is 0. That’s why you’d need to specify the width and height properties explicitly.

If you just want to lay out items with even spacing, then you can give them all the same flex-grow value instead. I.e. call .withFlex(1.0f) on them.

Thanks for the feedback!

I’m confused about this because the components I am using explicitly set their size in their constructor with setSize before they get added to my flexboxes. Is that size just not used? And when is the juce::FlexItem::autoValue constant supposed to be used then?

Unfortunately, I do not want any growing or shrinking. In the HTML example I included, you should be able to see that the components take up none of the positive free space, only as much space as they need to fit their contents. Is this not a supported workflow with juce flexboxes?

No the flex box engine doesn’t read the component’s existing size - it only uses the properties specified on the FlexBox and the FlexItems.

That’s totally possible, just that you have to do a lot of things explicitly in JUCE where they’re done implicitly in HTML/CSS. As I said, JUCE components have no knowledge of their content so if you want an item to be sized to be as small as possible to display its content then you need to explicitly say what that limitation is.

For that you’d typically want to set the minWidth and minHeight properties of the flex items to the required content size to tell the engine not to make the item any small than that.

Interesting. Thanks again!