diff --git a/include/sst/jucegui/component-adapters/DiscreteToReference.h b/include/sst/jucegui/component-adapters/DiscreteToReference.h new file mode 100644 index 0000000..52196cf --- /dev/null +++ b/include/sst/jucegui/component-adapters/DiscreteToReference.h @@ -0,0 +1,67 @@ +/* + * sst-juce-gui - an open source library of juce widgets + * built by Surge Synth Team. + * + * Copyright 2023, various authors, as described in the GitHub + * transaction log. + * + * sst-basic-blocks is released under the MIT license, as described + * by "LICENSE.md" in this repository. This means you may use this + * in commercial software if you are a JUCE Licensee. If you use JUCE + * in the open source / GPL3 context, your combined work must be + * released under GPL3. + * + * All source in sst-juce-gui available at + * https://github.com/surge-synthesizer/sst-juce-gui + */ + +#ifndef INCLUDE_SST_JUCEGUI_COMPONENT_ADAPTERS_DISCRETETOREFERENCE_H +#define INCLUDE_SST_JUCEGUI_COMPONENT_ADAPTERS_DISCRETETOREFERENCE_H + +#include +#include + +#include "sst/jucegui/components/DiscreteParamEditor.h" + +namespace sst::jucegui::component_adapters +{ +template struct DiscreteToValueReference : data::Discrete +{ + static_assert(std::is_integral()); + std::unique_ptr widget; + V &underlyer; + static_assert(std::is_base_of::value); + DiscreteToValueReference(std::unique_ptr &wid, V &und) + : widget(std::move(wid)), underlyer(und) + { + setup(); + } + DiscreteToValueReference(V &und) : widget(std::make_unique()), underlyer(und) { setup(); } + + void setup() { widget->setSource(this); } + + std::string label; + void setLabel(const std::string &s) + { + label = s; + widget->repaint(); + } + std::string getLabel() const override { return label; } + + std::function onValueChanged{nullptr}; + int getValue() const override { return underlyer; } + void setValueFromGUI(const int &f) override + { + underlyer = f; + if (onValueChanged) + onValueChanged(f); + } + void setValueFromModel(const int &f) override + { + underlyer = f; + widget->repaint(); + } +}; +} // namespace sst::jucegui::component_adapters + +#endif // CONDUIT_DISCRETETOREFERENCE_H diff --git a/include/sst/jucegui/components/ComponentBase.h b/include/sst/jucegui/components/ComponentBase.h index bfe1ef8..ef0c6ad 100644 --- a/include/sst/jucegui/components/ComponentBase.h +++ b/include/sst/jucegui/components/ComponentBase.h @@ -95,8 +95,8 @@ template struct Modulatable : public data::Continuous::DataListener virtual ~Modulatable() { - if (source) - source->removeGUIDataListener(this); + if (continuous()) + continuous()->removeGUIDataListener(this); } T *asT() { return static_cast(this); } @@ -119,21 +119,56 @@ template struct Modulatable : public data::Continuous::DataListener isEditingMod = b; asT()->repaint(); } - void setSource(data::ContinunousModulatable *s) + + data::Continuous *continuous() + { + switch (source.index()) + { + case 0: + return std::get<0>(source); + case 1: + return std::get<1>(source); + } + assert(false); + return nullptr; + } + data::ContinunousModulatable *continuousModulatable() { - if (source) - source->removeGUIDataListener(this); + if (std::holds_alternative(source)) + { + return std::get(source); + } + return nullptr; + } + + template void setSource(S *s) + { + if (continuous()) + continuous()->removeGUIDataListener(this); source = s; - if (source) - source->addGUIDataListener(this); + if (continuous()) + continuous()->addGUIDataListener(this); asT()->repaint(); } + void clearSource() + { + if (continuous()) + continuous()->removeGUIDataListener(this); + source = (data::ContinunousModulatable *)nullptr; + } + void dataChanged() override { asT()->repaint(); } + void sourceVanished(data::Continuous *s) override + { + assert(s == continuous()); + clearSource(); + } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Modulatable) - data::ContinunousModulatable *source{nullptr}; + std::variant source{ + (data::Continuous *)nullptr}; bool isEditingMod{false}; ModulationDisplay modulationDisplay{NONE}; }; diff --git a/include/sst/jucegui/components/DiscreteParamEditor.h b/include/sst/jucegui/components/DiscreteParamEditor.h index 28acd84..e6541bc 100644 --- a/include/sst/jucegui/components/DiscreteParamEditor.h +++ b/include/sst/jucegui/components/DiscreteParamEditor.h @@ -27,6 +27,11 @@ struct DiscreteParamEditor : public juce::Component, public data::Discrete::DataListener { void dataChanged() override { repaint(); } + void sourceVanished(data::Discrete *d) override + { + assert(d == source); + setSource(nullptr); + } void setSource(data::Discrete *d) { if (data) diff --git a/include/sst/jucegui/data/Continuous.h b/include/sst/jucegui/data/Continuous.h index 1c5d43e..01411ed 100644 --- a/include/sst/jucegui/data/Continuous.h +++ b/include/sst/jucegui/data/Continuous.h @@ -28,13 +28,20 @@ namespace sst::jucegui::data { struct Continuous : public Labeled { - virtual ~Continuous() = default; + virtual ~Continuous() + { + for (auto *dl : guilisteners) + { + dl->sourceVanished(this); + } + }; struct DataListener { virtual ~DataListener() = default; // FIXME - in the future we may want this more fine grained virtual void dataChanged() = 0; + virtual void sourceVanished(Continuous *) = 0; }; void addGUIDataListener(DataListener *l) { guilisteners.insert(l); } void removeGUIDataListener(DataListener *l) { guilisteners.erase(l); } diff --git a/include/sst/jucegui/data/Discrete.h b/include/sst/jucegui/data/Discrete.h index df7f2cc..2a90980 100644 --- a/include/sst/jucegui/data/Discrete.h +++ b/include/sst/jucegui/data/Discrete.h @@ -25,13 +25,18 @@ namespace sst::jucegui::data { struct Discrete : public Labeled { - virtual ~Discrete() = default; + virtual ~Discrete() + { + for (auto *l : guilisteners) + l->sourceVanished(this); + } struct DataListener { virtual ~DataListener() = default; // FIXME - in the future we may want this more fine grained virtual void dataChanged() = 0; + virtual void sourceVanished(Discrete *) = 0; }; void addGUIDataListener(DataListener *l) { guilisteners.insert(l); } void removeGUIDataListener(DataListener *l) { guilisteners.erase(l); } diff --git a/src/sst/jucegui/components/ContinuousParamEditor.cpp b/src/sst/jucegui/components/ContinuousParamEditor.cpp index 8f74561..edda734 100644 --- a/src/sst/jucegui/components/ContinuousParamEditor.cpp +++ b/src/sst/jucegui/components/ContinuousParamEditor.cpp @@ -39,10 +39,10 @@ void ContinuousParamEditor::mouseDown(const juce::MouseEvent &e) mouseMode = DRAG; onBeginEdit(); - if (isEditingMod) - mouseDownV0 = source->getModulationValuePM1(); + if (isEditingMod && continuousModulatable()) + mouseDownV0 = continuousModulatable()->getModulationValuePM1(); else - mouseDownV0 = source->getValue(); + mouseDownV0 = continuous()->getValue(); mouseDownY0 = e.position.y; mouseDownX0 = e.position.x; } @@ -62,7 +62,7 @@ void ContinuousParamEditor::mouseDoubleClick(const juce::MouseEvent &e) return; onBeginEdit(); - source->setValueFromGUI(source->getDefaultValue()); + continuous()->setValueFromGUI(continuous()->getDefaultValue()); onEndEdit(); repaint(); @@ -79,11 +79,11 @@ void ContinuousParamEditor::mouseDrag(const juce::MouseEvent &e) float dy = -(e.position.y - mouseDownY0); float dx = (e.position.x - mouseDownX0); float d = 0; - float minForScaling = source->getMin(); - float maxForScaling = source->getMax(); - if (isEditingMod) + float minForScaling = continuous()->getMin(); + float maxForScaling = continuous()->getMax(); + if (isEditingMod && continuousModulatable()) { - if (source->isModulationBipolar()) + if (continuousModulatable()->isModulationBipolar()) minForScaling = -1.0f; else minForScaling = 0.0f; @@ -100,18 +100,18 @@ void ContinuousParamEditor::mouseDrag(const juce::MouseEvent &e) } if (e.mods.isShiftDown()) d = d * 0.1; - if (isEditingMod) + if (isEditingMod && continuousModulatable()) { - if (source->isModulationBipolar()) + if (continuousModulatable()->isModulationBipolar()) d = d * 0.5; auto vn = std::clamp(mouseDownV0 + d, -1.f, 1.f); - source->setModulationValuePM1(vn); + continuousModulatable()->setModulationValuePM1(vn); mouseDownV0 = vn; } else { - auto vn = std::clamp(mouseDownV0 + d, source->getMin(), source->getMax()); - source->setValueFromGUI(vn); + auto vn = std::clamp(mouseDownV0 + d, continuous()->getMin(), continuous()->getMax()); + continuous()->setValueFromGUI(vn); mouseDownV0 = vn; } mouseDownX0 = e.position.x; @@ -129,25 +129,27 @@ void ContinuousParamEditor::mouseWheelMove(const juce::MouseEvent &e, return; onBeginEdit(); - if (isEditingMod) + if (isEditingMod && continuousModulatable()) { // fixme - callibration and sharing auto d = (wheel.isReversed ? -1 : 1) * wheel.deltaY * (2); if (e.mods.isShiftDown()) d = d * 0.1; - auto vn = std::clamp(source->getModulationValuePM1() + d, -1.f, 1.f); - source->setModulationValuePM1(vn); + auto vn = std::clamp(continuousModulatable()->getModulationValuePM1() + d, -1.f, 1.f); + continuousModulatable()->setModulationValuePM1(vn); } else { // fixme - callibration and sharing - auto d = (wheel.isReversed ? -1 : 1) * wheel.deltaY * (source->getMax() - source->getMin()); + auto d = (wheel.isReversed ? -1 : 1) * wheel.deltaY * + (continuous()->getMax() - continuous()->getMin()); if (e.mods.isShiftDown()) d = d * 0.1; - auto vn = std::clamp(source->getValue() + d, source->getMin(), source->getMax()); - source->setValueFromGUI(vn); + auto vn = std::clamp(continuous()->getValue() + d, continuous()->getMin(), + continuous()->getMax()); + continuous()->setValueFromGUI(vn); } onEndEdit(); repaint(); @@ -155,9 +157,9 @@ void ContinuousParamEditor::mouseWheelMove(const juce::MouseEvent &e, bool ContinuousParamEditor::processMouseActions() { - if (!source) + if (!continuous()) return false; - if (source->isHidden()) + if (continuous()->isHidden()) return false; if (!isEnabled()) return false; diff --git a/src/sst/jucegui/components/DraggableTextEditableValue.cpp b/src/sst/jucegui/components/DraggableTextEditableValue.cpp index a6a9a19..2813f2f 100644 --- a/src/sst/jucegui/components/DraggableTextEditableValue.cpp +++ b/src/sst/jucegui/components/DraggableTextEditableValue.cpp @@ -46,11 +46,11 @@ void DraggableTextEditableValue::setFromEditor() auto t = underlyingEditor->getText(); if (t.isEmpty()) { - source->setValueFromGUI(source->getDefaultValue()); + continuous()->setValueFromGUI(continuous()->getDefaultValue()); } else { - source->setValueAsString(t.toStdString()); + continuous()->setValueAsString(t.toStdString()); } underlyingEditor->setVisible(false); repaint(); @@ -73,7 +73,7 @@ void DraggableTextEditableValue::paint(juce::Graphics &g) g.fillRoundedRectangle(getLocalBounds().toFloat(), 3.f); g.setColour(getColour(Styles::bordercol)); g.drawRoundedRectangle(getLocalBounds().toFloat(), 3.f, 1.f); - if (source && !underlyingEditor->isVisible()) + if (continuous() && !underlyingEditor->isVisible()) { g.setFont(getFont(Styles::labelfont)); if (underlyingEditor->isVisible()) @@ -82,23 +82,24 @@ void DraggableTextEditableValue::paint(juce::Graphics &g) g.setColour(getColour(Styles::textoffcol)); else g.setColour(getColour(Styles::texthoveroffcol)); - g.drawText(source->getValueAsString(), getLocalBounds(), juce::Justification::centred); + g.drawText(continuous()->getValueAsString(), getLocalBounds(), + juce::Justification::centred); } } void DraggableTextEditableValue::mouseDown(const juce::MouseEvent &e) { onBeginEdit(); - valueOnMouseDown = source->getValue(); + valueOnMouseDown = continuous()->getValue(); } void DraggableTextEditableValue::mouseUp(const juce::MouseEvent &e) { onEndEdit(); } void DraggableTextEditableValue::mouseDrag(const juce::MouseEvent &e) { auto d = e.getDistanceFromDragStartY(); auto fac = 0.5f * (e.mods.isShiftDown() ? 0.1f : 1.f); - auto nv = valueOnMouseDown - fac * d * source->getFineQuantizedStepSize(); - nv = std::clamp(nv, source->getMin(), source->getMax()); - source->setValueFromGUI(nv); + auto nv = valueOnMouseDown - fac * d * continuous()->getFineQuantizedStepSize(); + nv = std::clamp(nv, continuous()->getMin(), continuous()->getMax()); + continuous()->setValueFromGUI(nv); repaint(); } void DraggableTextEditableValue::mouseWheelMove(const juce::MouseEvent &event, @@ -109,7 +110,7 @@ void DraggableTextEditableValue::mouseWheelMove(const juce::MouseEvent &event, void DraggableTextEditableValue::mouseDoubleClick(const juce::MouseEvent &e) { - underlyingEditor->setText(source->getValueAsString()); + underlyingEditor->setText(continuous()->getValueAsString()); underlyingEditor->setVisible(true); underlyingEditor->selectAll(); underlyingEditor->grabKeyboardFocus(); diff --git a/src/sst/jucegui/components/HSlider.cpp b/src/sst/jucegui/components/HSlider.cpp index 517cb50..b569c23 100644 --- a/src/sst/jucegui/components/HSlider.cpp +++ b/src/sst/jucegui/components/HSlider.cpp @@ -28,7 +28,7 @@ HSlider::~HSlider() = default; void HSlider::paint(juce::Graphics &g) { - if (!source) + if (!continuous()) { g.fillAll(juce::Colours::red); g.setColour(juce::Colours::white); @@ -36,7 +36,7 @@ void HSlider::paint(juce::Graphics &g) return; } - if (source->isHidden()) + if (continuous()->isHidden()) return; bool vCenter = !showLabel && !showValue; @@ -45,14 +45,14 @@ void HSlider::paint(juce::Graphics &g) { g.setColour(getColour(Styles::labeltextcol)); g.setFont(getFont(Styles::labeltextfont)); - g.drawText(source->getLabel(), getLocalBounds().reduced(2, 1), + g.drawText(continuous()->getLabel(), getLocalBounds().reduced(2, 1), juce::Justification::bottomLeft); } if (showValue) { g.setColour(getColour(Styles::valuetextcol)); g.setFont(getFont(Styles::valuetextfont)); - g.drawText(source->getValueAsString(), getLocalBounds().reduced(2, 1), + g.drawText(continuous()->getValueAsString(), getLocalBounds().reduced(2, 1), juce::Justification::bottomRight); } @@ -93,11 +93,11 @@ void HSlider::paint(juce::Graphics &g) g.fillRoundedRectangle(gutter.reduced(2), gutterheight * 0.25); } - auto v = source->getValue01(); + auto v = continuous()->getValue01(); auto w = (1 - v) * gutter.getWidth(); auto hc = gutter.withTrimmedLeft(gutter.getWidth() - w).withWidth(1).expanded(0, 4).getCentre(); - if (source->isBipolar()) + if (continuous()->isBipolar()) { auto t = hc.getX(); auto b = gutter.getWidth() / 2 + gutter.getX(); @@ -116,15 +116,17 @@ void HSlider::paint(juce::Graphics &g) auto hr = juce::Rectangle(2 * hanRadius, 2 * hanRadius).withCentre(hc); - auto mvplus = std::clamp(v + source->getModulationValuePM1(), 0.f, 1.f); - auto mvminus = std::clamp(v - source->getModulationValuePM1(), 0.f, 1.f); - auto hm = (1.0 - mvplus) * gutter.getWidth(); - auto mpc = - gutter.withTrimmedLeft(gutter.getWidth() - hm).withWidth(1).expanded(0, 4).getCentre(); - auto mpr = juce::Rectangle(2 * hanRadius, 2 * hanRadius).withCentre(mpc); - - if (isEditingMod) + juce::Point mpc{}; + juce::Rectangle mpr{}; + if (isEditingMod && continuousModulatable()) { + auto mvplus = std::clamp(v + continuousModulatable()->getModulationValuePM1(), 0.f, 1.f); + auto mvminus = std::clamp(v - continuousModulatable()->getModulationValuePM1(), 0.f, 1.f); + auto hm = (1.0 - mvplus) * gutter.getWidth(); + mpc = + gutter.withTrimmedLeft(gutter.getWidth() - hm).withWidth(1).expanded(0, 4).getCentre(); + mpr = juce::Rectangle(2 * hanRadius, 2 * hanRadius).withCentre(mpc); + // draw rules { auto t = hc.getX(); @@ -136,7 +138,7 @@ void HSlider::paint(juce::Graphics &g) g.fillRoundedRectangle(val, gutterheight * 0.25); } - if (source->isModulationBipolar()) + if (continuousModulatable()->isModulationBipolar()) { auto t = hc.getX(); auto b = (mvminus)*gutter.getWidth() + gutter.getX(); diff --git a/src/sst/jucegui/components/HSliderFilled.cpp b/src/sst/jucegui/components/HSliderFilled.cpp index e504350..b007b89 100644 --- a/src/sst/jucegui/components/HSliderFilled.cpp +++ b/src/sst/jucegui/components/HSliderFilled.cpp @@ -27,7 +27,7 @@ HSliderFilled::HSliderFilled() void HSliderFilled::paint(juce::Graphics &g) { - if (!source) + if (!continuous()) { g.fillAll(juce::Colours::red); g.setColour(juce::Colours::white); @@ -35,7 +35,7 @@ void HSliderFilled::paint(juce::Graphics &g) return; } - if (source->isHidden()) + if (continuous()->isHidden()) return; // Gutter @@ -66,11 +66,11 @@ void HSliderFilled::paint(juce::Graphics &g) g.fillRoundedRectangle(gutter.reduced(2), rectRad); } - auto v = source->getValue01(); + auto v = continuous()->getValue01(); auto w = (1 - v) * gutter.getWidth(); auto hc = gutter.withTrimmedLeft(gutter.getWidth() - w).withWidth(1).expanded(0, 4).getCentre(); - if (source->isBipolar()) + if (continuous()->isBipolar()) { auto t = hc.getX(); auto b = gutter.getWidth() / 2 + gutter.getX(); @@ -89,15 +89,17 @@ void HSliderFilled::paint(juce::Graphics &g) auto hr = juce::Rectangle(2, gutter.getHeight()).withCentre(hc); - auto mvplus = std::clamp(v + source->getModulationValuePM1(), 0.f, 1.f); - auto mvminus = std::clamp(v - source->getModulationValuePM1(), 0.f, 1.f); - auto hm = (1.0 - mvplus) * gutter.getWidth(); - auto mpc = - gutter.withTrimmedLeft(gutter.getWidth() - hm).withWidth(1).expanded(0, 4).getCentre(); - auto mpr = juce::Rectangle(2 * hanRadius, 2 * hanRadius).withCentre(mpc); - - if (isEditingMod) + juce::Point mpc; + juce::Rectangle mpr; + if (continuousModulatable() && isEditingMod) { + auto mvplus = std::clamp(v + continuousModulatable()->getModulationValuePM1(), 0.f, 1.f); + auto mvminus = std::clamp(v - continuousModulatable()->getModulationValuePM1(), 0.f, 1.f); + auto hm = (1.0 - mvplus) * gutter.getWidth(); + mpc = + gutter.withTrimmedLeft(gutter.getWidth() - hm).withWidth(1).expanded(0, 4).getCentre(); + mpr = juce::Rectangle(2 * hanRadius, 2 * hanRadius).withCentre(mpc); + // draw rules { auto t = hc.getX(); @@ -109,7 +111,7 @@ void HSliderFilled::paint(juce::Graphics &g) g.fillRoundedRectangle(val, rectRad); } - if (source->isModulationBipolar()) + if (continuousModulatable()->isModulationBipolar()) { auto t = hc.getX(); auto b = (mvminus)*gutter.getWidth() + gutter.getX(); diff --git a/src/sst/jucegui/components/Knob.cpp b/src/sst/jucegui/components/Knob.cpp index 71093b4..ac55e80 100644 --- a/src/sst/jucegui/components/Knob.cpp +++ b/src/sst/jucegui/components/Knob.cpp @@ -28,14 +28,20 @@ Knob::~Knob() = default; void Knob::paint(juce::Graphics &g) { auto b = getLocalBounds(); - knobPainter(g, this, source); - + if (continuousModulatable()) + { + knobPainter(g, this, continuousModulatable()); + } + else + { + knobPainter(g, this, continuous()); + } if (drawLabel) { auto textarea = b.withTrimmedTop(b.getWidth()); g.setColour(getColour(Styles::labeltextcol)); g.setFont(getFont(Styles::labeltextfont)); - g.drawText(source->getLabel(), textarea, juce::Justification::centred); + g.drawText(continuous()->getLabel(), textarea, juce::Justification::centred); } } diff --git a/src/sst/jucegui/components/KnobPainter.hxx b/src/sst/jucegui/components/KnobPainter.hxx index f3e93f7..813b7f7 100644 --- a/src/sst/jucegui/components/KnobPainter.hxx +++ b/src/sst/jucegui/components/KnobPainter.hxx @@ -6,12 +6,15 @@ #define CONDUIT_KNOBPAINTER_H #include +#include namespace sst::jucegui::components { template void knobPainter(juce::Graphics &g, T* that, S *source) { + constexpr bool supportsMod = std::is_base_of_v; + if (!source) { g.fillAll(juce::Colours::red); @@ -152,16 +155,19 @@ void knobPainter(juce::Graphics &g, T* that, S *source) g.fillPath(pIn); // modulation arcs - if (that->isEditingMod) + if constexpr (supportsMod) { - pIn = modPath(5, source->getValue01(), source->getModulationValuePM1(), 1); - g.setColour(that->getColour(T::Styles::modvalcol)); - g.fillPath(pIn); - if (source->isModulationBipolar()) + if (that->isEditingMod) { - pIn = modPath(5, source->getValue01(), source->getModulationValuePM1(), -1); - g.setColour(that->getColour(T::Styles::modvalnegcol)); + pIn = modPath(5, source->getValue01(), source->getModulationValuePM1(), 1); + g.setColour(that->getColour(T::Styles::modvalcol)); g.fillPath(pIn); + if (source->isModulationBipolar()) + { + pIn = modPath(5, source->getValue01(), source->getModulationValuePM1(), -1); + g.setColour(that->getColour(T::Styles::modvalnegcol)); + g.fillPath(pIn); + } } } diff --git a/src/sst/jucegui/components/VSlider.cpp b/src/sst/jucegui/components/VSlider.cpp index 51faf8b..e1932f3 100644 --- a/src/sst/jucegui/components/VSlider.cpp +++ b/src/sst/jucegui/components/VSlider.cpp @@ -27,13 +27,13 @@ VSlider::~VSlider() = default; void VSlider::paint(juce::Graphics &g) { - if (!source) + if (!continuous()) { g.fillAll(juce::Colours::red); return; } - if (source->isHidden()) + if (continuous()->isHidden()) return; // Gutter @@ -65,11 +65,11 @@ void VSlider::paint(juce::Graphics &g) g.fillRoundedRectangle(gutter.reduced(2), gutterwidth * 0.25); } - auto v = source->getValue01(); + auto v = continuous()->getValue01(); auto h = (1.0 - v) * gutter.getHeight(); auto hc = gutter.withTrimmedTop(h).withHeight(1).expanded(0, 4).getCentre(); - if (source->isBipolar()) + if (continuous()->isBipolar()) { auto t = hc.getY(); auto b = gutter.getHeight() / 2 + gutter.getY(); @@ -88,8 +88,8 @@ void VSlider::paint(juce::Graphics &g) auto hr = juce::Rectangle(2 * hanRadius, 2 * hanRadius).withCentre(hc); - auto mvplus = std::clamp(v + source->getModulationValuePM1(), 0.f, 1.f); - auto mvminus = std::clamp(v - source->getModulationValuePM1(), 0.f, 1.f); + auto mvplus = std::clamp(v + continuousModulatable()->getModulationValuePM1(), 0.f, 1.f); + auto mvminus = std::clamp(v - continuousModulatable()->getModulationValuePM1(), 0.f, 1.f); auto hm = (1.0 - mvplus) * gutter.getHeight(); auto mpc = gutter.withTrimmedTop(hm).withHeight(1).expanded(0, 4).getCentre(); auto mpr = juce::Rectangle(2 * hanRadius, 2 * hanRadius).withCentre(mpc); @@ -107,7 +107,7 @@ void VSlider::paint(juce::Graphics &g) g.fillRoundedRectangle(val, gutterwidth * 0.25); } - if (source->isModulationBipolar()) + if (continuousModulatable()->isModulationBipolar()) { auto t = hc.getY(); auto b = (1 - mvminus) * gutter.getHeight() + gutter.getY();