juce::Grid string interpreter? (feature request or help)

i’ve been messing about with juce::Grid and have found it quite useful (although the learning curve has been steep…) and ended up making an extremely simple css-grid-like string interpreter (using it looks like this)

        auto layout = R"(
            grid-template-columns: 1fr 1fr 1fr 1fr;
            grid-template-rows: 25px 1fr 60px;
            grid-gap: 5px;
            grid-template-areas: 
                "settings settings settings settings"
                "attack decay sustain release"
                "keyboard keyboard keyboard keyboard"
            )";
        
        auto items = std::initializer_list<std::pair<juce::Component*, juce::String>> 
        { 
            { &settingsButton, "settings" },
            { &attackSlider,  "attack"  },
            { &decaySlider,   "decay"   },
            { &sustainSlider, "sustain" },
            { &releaseSlider, "release" },
            { midiKeyboard.get(), "keyboard" }
        };

        interpreter.setLayout (layout, items);

and then in my resized method i just have interpreter.performLayout(getLocalBounds();

i’ve found it’s cleaned up my project’s code base quite nicely and sped up component/gui developement.

I was wondering if this might be useful to the juce team? theres always discussion around improving gui developement and although i would love to continue developing it myself, i don’t have enough knowledge on css grid layouts to make certain decisions on the parser design.

heres the actual GridInterpreter class so far, any suggestions or feedback would be tremendously helpful!

class GridInterpreter
{
    juce::Grid grid;

    void parseLayout(const juce::String& gridLayout)
    {
        juce::StringArray lines;
        lines.addLines(gridLayout);

        for (const auto& line : lines)
        {
            if (line.contains("grid-template-columns:"))
                parseTrack(line, true); // true for columns
            else if (line.contains("grid-template-rows:"))
                parseTrack(line, false); // false for rows
            else if (line.contains("grid-template-areas:"))
                parseArea(line, lines);
            else if (line.contains("grid-gap:"))
                parseGap(line);
        }
    }

    void parseTrack(const juce::String& line, bool isColumn)
    {
        auto values = extractValuesFromLine(line);
        juce::Array<juce::Grid::TrackInfo> trackInfos;

        for (const auto& value : values)
        {
            if (value.contains("fr"))
            {
                auto size = static_cast<unsigned long long>(std::round(value.getDoubleValue()));
                trackInfos.add(juce::Grid::TrackInfo(juce::Grid::Fr(size)));
            }
            else if (value.contains("px"))
            {
                auto size = static_cast<float>(value.getDoubleValue());
                trackInfos.add(juce::Grid::TrackInfo(juce::Grid::Px(size)));
            }
        }

        if (isColumn)
            grid.templateColumns.swapWith(trackInfos);
        else
            grid.templateRows.swapWith(trackInfos);
    }

    void parseArea(const juce::String& line, const juce::StringArray& lines)
    {
        juce::String areaDefinition = line.fromFirstOccurrenceOf(":", false, false).trim();
        int nextLineIndex = lines.indexOf(line) + 1;

        while (nextLineIndex < lines.size() && !lines[nextLineIndex].contains(";"))
        {
            areaDefinition += " " + lines[nextLineIndex].trim();
            nextLineIndex++;
        }

        if (areaDefinition.contains(";"))
            areaDefinition = areaDefinition.upToLastOccurrenceOf(";", false, true);
        else if (nextLineIndex < lines.size())
            areaDefinition += " " + lines[nextLineIndex].upToLastOccurrenceOf(";", false, true);

        juce::String currentArea;
        juce::StringArray tempAreas;

        bool inQuote = false;
        juce::juce_wchar quoteChar = 0;

        for (juce::juce_wchar character : areaDefinition)
        {
            if ((character == '"' || character == '\'') && (!inQuote || character == quoteChar))
            {
                inQuote = !inQuote;
                quoteChar = inQuote ? character : 0;
                
                if (!inQuote && !currentArea.isEmpty())
                {
                    tempAreas.add(currentArea);
                    currentArea.clear();
                }
            }
            else if (inQuote)
                currentArea += character;
        }

        grid.templateAreas.swapWith(tempAreas);
    }

    void parseGap(const juce::String& line)
    {
        auto values = extractValuesFromLine(line);

        for (const auto& value : values)
        {
            if (value.contains("px"))
                grid.setGap(juce::Grid::Px(value.getFloatValue()));
        }
    }

    juce::StringArray extractValuesFromLine(const juce::String& line)
    {
        auto valueString = line.fromFirstOccurrenceOf(":", false, false).trim();
        return juce::StringArray::fromTokens(valueString, " ", "");
    }

    void addComponents(const std::initializer_list<std::pair<juce::Component*, juce::String>>& components)
    {
        grid.items.clear();

        for (const auto& [comp, area] : components)
            grid.items.add(juce::GridItem(comp).withArea(area));
    }

public:
    void setLayout (const juce::String& gridLayout, 
                    const std::initializer_list<std::pair<juce::Component*, juce::String>>& components)
    {
        addComponents(components);
        parseLayout(gridLayout);
    }

    void performLayout (const juce::Rectangle<int>& bounds) 
    {
        grid.performLayout(bounds);
    }
};

EDIT: it currently only handles grid-template stuff and nothing for interpreting repeat() , minmax() , auto-fill , and auto-fit functions, or anything crazy meta like having grids within grids etc

1 Like

In case you didn’t know about it already…

I haven’t used it but I gather it’s along the same lines. I’ll tag @ImJimmi in case he has anything to add.

thanks! yes i am aware of jive :slight_smile: it’s very impressive but I was hoping to make something a little more lightweight and native to juce and juce::Grids rather than a whole gui extension library

Yeah JIVE is quite all-or-nothing so probably overkill for what you need here!

Really like your solution though, looks really clean!

Something I’d love to see in JUCE are some auto-layout container components like juce::GridComponent and juce::FlexComponent. They’d have overloads to addChildComponent() that also took a juce::GridItem or juce::FlexItem, as well as setters for all the grid/flex properties. Then you wouldn’t have to override resized() and everything could be initialised in the constructor.

2 Likes

thanks, very much JIVE inspired too so I really appreciate the effort you’re putting into it, even though it isn’t exactly what I’m looking for.

yeah i think that would be really cool too, something i’ve also thought about. could also introduce some sort of value tree / state management system like you have going on in JIVE!