From 79e574ffd9c5698bc760f789a5be48543ccab7d7 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 20 Jan 2025 08:33:02 -0500 Subject: [PATCH] Add some layout math (#144) Ideally this would move to sst-jucegui this week and also get a json reader, but this is how far I got before 1.0 and I can now merge it Basically hlist and vlist is way way easier --- src/ui/dahdsr-components.h | 50 ++++--- src/ui/layout/Layout.h | 251 +++++++++++++++++++++++++++++++++ src/ui/lfo-components.h | 54 +++---- src/ui/modulation-components.h | 36 +++-- src/ui/ui-constants.h | 28 ++++ 5 files changed, 362 insertions(+), 57 deletions(-) create mode 100644 src/ui/layout/Layout.h diff --git a/src/ui/dahdsr-components.h b/src/ui/dahdsr-components.h index 169c131..bbffab7 100644 --- a/src/ui/dahdsr-components.h +++ b/src/ui/dahdsr-components.h @@ -24,6 +24,8 @@ #include "ui-constants.h" #include "sst/jucegui/components/RuledLabel.h" +#include "ui/layout/Layout.h" + namespace baconpaul::six_sines::ui { namespace jcmp = sst::jucegui::components; @@ -94,34 +96,42 @@ template struct DAHDSRComponents juce::Rectangle layoutDAHDSRAt(int x, int y) { - if (!titleLab) + if (!titleLab || !slider[0]) return {}; - auto lh = uicTitleLabelHeight; - auto c = asComp(); - auto h = c->getHeight() - y; - auto q = h - uicLabelHeight - uicLabelGap - lh; - auto w = uicSliderWidth; + namespace jlo = sst::jucegui::layout; + + auto lo = jlo::VList() + .at(x, y) + .withHeight(asComp()->getHeight() - y) + .withWidth(nels * uicSliderWidth); - positionTitleLabelAt(x, y, nels * uicSliderWidth, titleLab); + lo.add(titleLabelLayout(titleLab)); + auto sliders = jlo::HList().expandToFill(); - auto bx = juce::Rectangle(x, y + lh, w, h - lh); for (int i = 0; i < nels; ++i) { - if (!slider[i]) - continue; - slider[i]->setBounds(bx.withHeight(q).withTrimmedTop(uicSliderWidth)); + auto sliderStack = jlo::VList().withWidth(uicSliderWidth); + if (i == 0) - triggerButton->setBounds(bx.withHeight(uicSliderWidth).reduced(2)); - if (i % 2) - { - shapes[i / 2]->setBounds(bx.withHeight(uicSliderWidth)); - } - lab[i]->setBounds(bx.withTrimmedTop(q + uicLabelGap)); - bx = bx.translated(uicSliderWidth, 0); + sliderStack.add( + jlo::Component(*triggerButton).withHeight(uicSliderWidth).insetBy(2)); + else if (i % 2) + sliderStack.add(jlo::Component(*shapes[i / 2]).withHeight(uicSliderWidth)); + else + sliderStack.addGap(uicSliderWidth); + + sliderStack.add(jlo::Component(*slider[i]).expandToFill()); + + sliderStack.addGap(uicLabelGap); + sliderStack.add(jlo::Component(*lab[i]).withHeight(uicLabelHeight)); + + sliders.add(sliderStack); } - bx = bx.translated(-uicSliderWidth, 0); - return juce::Rectangle(x, y, 0, 0).withLeft(bx.getRight()).withBottom(bx.getBottom()); + + lo.add(sliders); + auto usedSpace = lo.doLayout(); + return usedSpace.translated(usedSpace.getWidth(), 0); } static constexpr int nels{6}; // dahdsr diff --git a/src/ui/layout/Layout.h b/src/ui/layout/Layout.h new file mode 100644 index 0000000..b439adf --- /dev/null +++ b/src/ui/layout/Layout.h @@ -0,0 +1,251 @@ +/* + * Six Sines + * + * A synth with audio rate modulation. + * + * Copyright 2024-2025, Paul Walker and Various authors, as described in the github + * transaction log. + * + * This source repo is released under the MIT license, but has + * GPL3 dependencies, as such the combined work will be + * released under GPL3. + * + * The source code and license are at https://github.com/baconpaul/six-sines + */ + +#ifndef BACONPAUL_SIX_SINES_UI_LAYOUT_LAYOUT_H +#define BACONPAUL_SIX_SINES_UI_LAYOUT_LAYOUT_H + +#include +#include + +/* + * This is an experiment to see if we can figure out something which works for more props + */ +namespace sst::jucegui::layout +{ +// A#define LOLOG(...) std::cout << __VA_ARGS__ << std::endl; +#define LOLOG(...) ((void)0); +struct LayoutComponent +{ + using loc_t = LayoutComponent; + + loc_t at(int x, int y) + { + auto res = *this; + res.x = x; + res.y = y; + return res; + } + + loc_t withHeight(int h) + { + auto res = *this; + res.h = h; + return res; + } + + loc_t withWidth(int w) + { + auto res = *this; + res.w = w; + return res; + } + + loc_t insetBy(int xi) { return insetBy(xi, xi); } + loc_t insetBy(int xi, int yi) + { + auto res = *this; + res.insetx = xi; + res.insety = yi; + return res; + } + + loc_t expandToFill() + { + auto res = *this; + res.isFilling = true; + return res; + } + + enum LayoutMechanism + { + LEAF, + VLIST, + HLIST + } mechanism; + + loc_t withLayoutMechanism(LayoutMechanism m) + { + auto res = *this; + res.mechanism = m; + return res; + } + + loc_t withComponent(juce::Component &c) + { + auto res = *this; + res.component = juce::Component::SafePointer(&c); + return res; + } + + loc_t withAutoGap(int g) + { + assert(mechanism == VLIST || mechanism == HLIST); + auto res = *this; + res.autoGap = g; + return res; + } + + void add(const loc_t &l) + { + if (autoGap > 0 && !children.empty()) + { + addGap(autoGap); + } + + children.push_back(l); + } + + void addGap(int size) + { + if (mechanism == VLIST) + { + children.push_back(loc_t().withLayoutMechanism(LayoutComponent::LEAF).withHeight(size)); + } + else if (mechanism == HLIST) + { + children.push_back(loc_t().withLayoutMechanism(LayoutComponent::LEAF).withWidth(size)); + } + else + { + assert(false && "addGap only works with hlist/vlist right now"); + } + } + + juce::Rectangle doLayout(const std::string &pfx = "+--") + { + switch (mechanism) + { + case LEAF: + { + auto res = juce::Rectangle(x, y, w, h).reduced(insetx, insety); + if (component) + component->setBounds(res); + LOLOG(pfx << " LEAF " << (component ? component->getTitle() : "-gap-") << " @ " + << res.toString()); + return res; + } + break; + case HLIST: + case VLIST: + { + std::string tag = (mechanism == HLIST ? "hlist" : "vlist"); + + LOLOG(pfx << " " << tag << " x==" << x << " y=" << y << " w=" << w << " h=" << h); + int fillingCount{0}, unfillingSpace{0}; + for (auto &c : children) + { + if (c.isFilling) + { + fillingCount++; + LOLOG(pfx << " " << tag << " Filling Element"); + } + else + { + auto consumes = (mechanism == HLIST ? c.w : c.h); + unfillingSpace += consumes; + LOLOG(pfx << " " << tag << " Specified element consumes=" << consumes + << " ufs=" << unfillingSpace); + } + } + auto leftoverSpace = (mechanism == HLIST ? w : h) - unfillingSpace; + auto fillingItemSize = fillingCount > 0 ? (leftoverSpace / fillingCount) : 0; + LOLOG(pfx << " " << tag << " fc=" << fillingCount << " fis=" << fillingItemSize); + + auto pos = mechanism == VLIST ? y : x; + for (auto &c : children) + { + if (c.isFilling) + { + if (mechanism == HLIST) + { + c.w = std::min(fillingItemSize, leftoverSpace); + } + else + { + c.h = std::min(fillingItemSize, leftoverSpace); + } + + leftoverSpace -= fillingItemSize; + } + + if (mechanism == VLIST) + { + if (c.w == 0) + c.w = w; + c.x = x; + c.y = pos; + LOLOG(pfx << " " << tag << " - setting element to " << c.x << "," << c.y + << " with dims=" << c.w << "," << c.h << " and pos=" << pos); + pos += c.h; + LOLOG(pfx << " " << tag << " Post set pos = " << pos); + } + else + { + if (c.h == 0) + c.h = h; + c.x = pos; + c.y = y; + LOLOG(pfx << " " << tag << " - setting element to " << c.x << "," << c.y + << " with dums=" << c.w << "," << c.h << " and pos=" << pos); + pos += c.w; + } + c.doLayout(pfx + "|--"); + } + + juce::Rectangle res{x, y, w, h}; + LOLOG(pfx << " " << tag << " Returning " << res.toString()); + return res; + } + break; + } + + return {}; + } + + private: + // details go here + int x{0}, y{0}, w{0}, h{0}; + int insetx{0}, insety{0}; + bool isFilling{false}; + + int autoGap{0}; + + std::vector children; + + juce::Component::SafePointer component{nullptr}; +}; + +inline LayoutComponent VList() +{ + return LayoutComponent().withLayoutMechanism(LayoutComponent::VLIST); +} + +inline LayoutComponent HList() +{ + return LayoutComponent().withLayoutMechanism(LayoutComponent::HLIST); +} + +inline LayoutComponent Gap() +{ + return LayoutComponent().withLayoutMechanism(LayoutComponent::LEAF); +} + +inline LayoutComponent Component(juce::Component &c) +{ + return LayoutComponent().withLayoutMechanism(LayoutComponent::LEAF).withComponent(c); +} +} // namespace sst::jucegui::layout + +#endif diff --git a/src/ui/lfo-components.h b/src/ui/lfo-components.h index 237cbda..599c6bd 100644 --- a/src/ui/lfo-components.h +++ b/src/ui/lfo-components.h @@ -23,6 +23,7 @@ #include "patch-data-bindings.h" #include "ui-constants.h" #include "sst/jucegui/components/RuledLabel.h" +#include "ui/layout/Layout.h" namespace baconpaul::six_sines::ui { @@ -85,31 +86,34 @@ template struct LFOComponents if (!titleLab) return {}; - auto c = asComp(); - auto lh = uicLabelHeight; - auto h = c->getHeight() - y; - auto q = h - lh; - auto w = uicKnobSize * 1.5; - - positionTitleLabelAt(x, y, w + uicMargin + uicKnobSize + extraWidth, titleLab); - - auto shapeH = 2 * uicLabeledKnobHeight - uicMargin - lh + 5; - auto bx = juce::Rectangle(x, y + uicTitleLabelHeight, w, h - lh); - shape->setBounds(bx.withHeight(shapeH)); - tempoSync->setBounds(bx.withTrimmedTop(shapeH + uicMargin).withHeight(lh)); - bipolar->setBounds(bx.withTrimmedTop(shapeH + 2 * uicMargin + lh).withHeight(lh)); - isEnv->setBounds(bx.withTrimmedTop(shapeH + 2 * uicMargin + lh) - .withHeight(lh) - .translated(w + uicMargin, 0) - .withWidth(uicKnobSize)); - - bx = bx.translated(w + uicMargin, 0); - positionKnobAndLabel(bx.getX(), bx.getY(), rate, rateL); - positionKnobAndLabel(bx.getX(), bx.getY() + uicLabeledKnobHeight + uicMargin, deform, - deformL); - bx = bx.translated(uicKnobSize + uicMargin, 0); - return juce::Rectangle(x + w + uicKnobSize + uicMargin + extraWidth, y, 0, 0) - .withBottom(bx.getBottom()); + namespace jlo = sst::jucegui::layout; + + auto lo = jlo::VList() + .at(x, y) + .withHeight(asComp()->getHeight() - y) + .withWidth(uicKnobSize * 2.5 + uicMargin); + + lo.add(titleLabelLayout(titleLab)); + + auto columns = jlo::HList().expandToFill().withAutoGap(uicMargin); + + auto col1 = jlo::VList().withWidth(uicKnobSize * 1.5).withAutoGap(uicMargin); + col1.add(jlo::Component(*shape).withHeight(2 * uicLabeledKnobHeight - uicLabelHeight)); + col1.add(jlo::Component(*tempoSync).withHeight(uicLabelHeight)); + col1.add(jlo::Component(*bipolar).withHeight(uicLabelHeight)); + + auto col2 = jlo::VList().withWidth(uicKnobSize).withAutoGap(uicMargin); + col2.add(labelKnobLayout(rate, rateL)); + col2.add(labelKnobLayout(deform, deformL)); + col2.add(jlo::Component(*isEnv).withHeight(uicLabelHeight)); + + columns.add(col1); + columns.add(col2); + + lo.add(columns); + + auto usedSpace = lo.doLayout(); + return usedSpace.translated(usedSpace.getWidth(), 0); } std::unique_ptr rate, deform; diff --git a/src/ui/modulation-components.h b/src/ui/modulation-components.h index 866d175..701b8ed 100644 --- a/src/ui/modulation-components.h +++ b/src/ui/modulation-components.h @@ -135,24 +135,36 @@ template struct ModulationComponents void layoutModulation(const juce::Rectangle &r) { - auto modW{150}; // TODO MOVE TO UIC - auto bx = r.withTrimmedLeft(r.getWidth() - modW); - positionTitleLabelAt(bx.getX(), bx.getY(), modW, modTitleLab); - auto bq = bx.translated(0, uicTitleLabelHeight + uicMargin) - .withHeight(uicLabelHeight + uicMargin); + auto modW{uicModulationWidth}; - auto mks = 2 * uicLabelHeight + 2 * uicMargin; + namespace jlo = sst::jucegui::layout; + + auto lo = jlo::VList() + .at(r.getRight() - uicModulationWidth, r.getY()) + .withHeight(asComp()->getHeight() - r.getY()) + .withWidth(uicModulationWidth); + + lo.add(titleLabelLayout(modTitleLab)); for (int i = 0; i < numModsPer; ++i) { - auto kb = bq.withTrimmedLeft(bq.getWidth() - mks).withHeight(mks); - depthSlider[i]->setBounds(kb); + auto hl = jlo::HList() + .withWidth(uicModulationWidth) + .withHeight(uicKnobSize) + .withAutoGap(uicMargin); + + auto vl = jlo::VList().expandToFill().withAutoGap(uicLabelGap); + vl.add(jlo::Component(*sourceMenu[i]).expandToFill()); + vl.add(jlo::Component(*targetMenu[i]).expandToFill()); - sourceMenu[i]->setBounds(bq.withTrimmedRight(mks)); - bq = bq.translated(0, uicLabelHeight + uicMargin); - targetMenu[i]->setBounds(bq.withTrimmedRight(mks)); - bq = bq.translated(0, uicLabelHeight + 3 * uicMargin); + hl.add(vl); + hl.add(jlo::Component(*depthSlider[i]).withWidth(uicKnobSize)); + + lo.add(hl); + lo.addGap(uicMargin * 2); } + + lo.doLayout(); } void showTargetMenu(int index) diff --git a/src/ui/ui-constants.h b/src/ui/ui-constants.h index a60eb68..b99b864 100644 --- a/src/ui/ui-constants.h +++ b/src/ui/ui-constants.h @@ -17,6 +17,7 @@ #define BACONPAUL_SIX_SINES_UI_UI_CONSTANTS_H #include +#include "ui/layout/Layout.h" namespace baconpaul::six_sines::ui { @@ -33,6 +34,8 @@ static constexpr uint32_t uicTitleLabelInnerBox{18}; static constexpr uint32_t uicLabeledKnobHeight{uicKnobSize + uicLabelHeight + uicLabelGap}; static constexpr uint32_t uicPowerKnobWidth{uicKnobSize + uicPowerButtonSize + uicMargin}; +static constexpr uint32_t uicModulationWidth{150}; + template inline void positionKnobAndLabel(uint32_t x, uint32_t y, const K &k, const L &l) { @@ -86,5 +89,30 @@ template inline void positionTitleLabelAt(int x, int y, int w, cons { t->setBounds(x, y, w, uicTitleLabelInnerBox); } + +// put this in UIC +template inline sst::jucegui::layout::LayoutComponent titleLabelLayout(T &f) +{ + namespace jlo = sst::jucegui::layout; + + auto res = jlo::VList().withHeight(uicTitleLabelHeight); + res.add(jlo::Component(*f).withHeight(uicTitleLabelInnerBox)); + res.add(jlo::Gap().withHeight(uicTitleLabelHeight - uicTitleLabelInnerBox)); + return res; +}; + +template +inline sst::jucegui::layout::LayoutComponent labelKnobLayout(const K &k, const L &l) +{ + namespace jlo = sst::jucegui::layout; + auto res = + jlo::VList().withHeight(uicKnobSize + uicLabelGap + uicLabelHeight).withWidth(uicKnobSize); + res.add(jlo::Component(*k).withHeight(uicKnobSize)); + res.add(jlo::Gap().withHeight(uicLabelGap)); + res.add(jlo::Component(*l).withHeight(uicLabelHeight)); + + return res; +} + } // namespace baconpaul::six_sines::ui #endif // UI_CONSTANTS_H