add bypass toggle switch UI to top-left

This commit is contained in:
Armin 2026-07-13 01:44:13 +02:00
commit 9d0da19f48
2 changed files with 294 additions and 50 deletions

View file

@ -35,9 +35,9 @@ void SaftpumpeEditor::applyPreset(int index) {
SaftpumpeEditor::SaftpumpeEditor(SaftpumpeProcessor& p)
: AudioProcessorEditor(&p), processorRef(p)
{
setSize(700, 500);
setSize(700, 560);
setResizable(true, this);
setResizeLimits(650, 480, 1100, 800);
setResizeLimits(650, 480, 1100, 900);
startTimerHz(30);
auto setupKnob = [this](juce::Slider& s, juce::Label& l, const juce::String& text,
@ -99,7 +99,7 @@ SaftpumpeEditor::SaftpumpeEditor(SaftpumpeProcessor& p)
setupLCD(autoReleaseLCD);
// Zoom dropdown
zoomLabel.setText("ZOOM", juce::dontSendNotification);
zoomLabel.setText("UI SCALE", juce::dontSendNotification);
zoomLabel.setJustificationType(juce::Justification::centredRight);
zoomLabel.setColour(juce::Label::textColourId, textDim);
zoomLabel.setFont(juce::Font(juce::FontOptions(10.0f)));
@ -110,7 +110,7 @@ SaftpumpeEditor::SaftpumpeEditor(SaftpumpeProcessor& p)
zoomBox.addItem("2.0x", 3);
zoomBox.addItem("2.5x", 4);
zoomBox.addItem("3.0x", 5);
zoomBox.setSelectedId(2, juce::dontSendNotification);
zoomBox.setSelectedId(1, juce::dontSendNotification);
zoomBox.setColour(juce::ComboBox::backgroundColourId, grooveDark);
zoomBox.setColour(juce::ComboBox::textColourId, textColour);
zoomBox.setColour(juce::ComboBox::arrowColourId, textDim);
@ -135,6 +135,18 @@ SaftpumpeEditor::SaftpumpeEditor(SaftpumpeProcessor& p)
presetBox.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff333338));
presetBox.addListener(this);
addAndMakeVisible(presetBox);
// Bypass toggle
bypassLabel.setText("BYPASS", juce::dontSendNotification);
bypassLabel.setJustificationType(juce::Justification::centredRight);
bypassLabel.setColour(juce::Label::textColourId, textDim);
bypassLabel.setFont(juce::Font(juce::FontOptions(10.0f)));
addAndMakeVisible(bypassLabel);
bypassButton.setLookAndFeel(&toggleLnf);
bypassButton.setToggleState(false, juce::dontSendNotification);
addAndMakeVisible(bypassButton);
bypassAttachment = std::make_unique<ButtonAttachment>(processorRef.parameters, "bypass", bypassButton);
}
SaftpumpeEditor::~SaftpumpeEditor() {
@ -146,6 +158,7 @@ SaftpumpeEditor::~SaftpumpeEditor() {
makeupSlider.setLookAndFeel(nullptr);
mixSlider.setLookAndFeel(nullptr);
autoReleaseSlider.setLookAndFeel(nullptr);
bypassButton.setLookAndFeel(nullptr);
zoomBox.removeListener(this);
presetBox.removeListener(this);
stopTimer();
@ -157,7 +170,7 @@ void SaftpumpeEditor::comboBoxChanged(juce::ComboBox* box) {
int id = box->getSelectedId();
if (id >= 1 && id <= 5) {
uiScale = scales[id - 1];
setSize(static_cast<int>(700 * uiScale), static_cast<int>(500 * uiScale));
setSize(static_cast<int>(700 * uiScale), static_cast<int>(560 * uiScale));
}
} else if (box == &presetBox) {
int id = box->getSelectedId() - 1;
@ -165,6 +178,14 @@ void SaftpumpeEditor::comboBoxChanged(juce::ComboBox* box) {
}
}
void SaftpumpeEditor::mouseUp(const juce::MouseEvent& event) {
if (scopeArea.contains(event.position.toFloat())) {
bool wasSpectrum = processorRef.isSpectrumMode();
processorRef.setSpectrumMode(!wasSpectrum);
repaint();
}
}
juce::String SaftpumpeEditor::formatValue(int idx, float val) {
switch (idx) {
case 0: return juce::String(val, 1) + " dB";
@ -184,17 +205,22 @@ void SaftpumpeEditor::computeLayout() {
auto bounds = getLocalBounds().toFloat();
auto faceBounds = bounds.reduced(8.0f * s);
// Header
float headerH = 28.0f * s + 16.0f * s + 8.0f * s;
float headerY = faceBounds.getY() + 6.0f * s;
float contentTop = headerY + headerH + 4.0f * s;
// Content starts below accent line (accent line at faceY + 80*s)
float accentY = faceBounds.getY() + 80.0f * s;
float contentTop = accentY + 22.0f * s;
// Meters — right column
// Bottom controls — side-by-side preset + zoom
float ctrlW = 140.0f * s;
float ctrlH = 52.0f * s;
float bottomY = faceBounds.getBottom() - ctrlH - 10.0f * s;
// Meters — right column (stops above bottom controls)
float meterW = 56.0f * s;
meterArea = juce::Rectangle<float>(faceBounds.getRight() - meterW - 14.0f * s,
float meterLegW = 18.0f * s;
meterArea = juce::Rectangle<float>(faceBounds.getRight() - meterW - meterLegW - 14.0f * s,
contentTop,
meterW,
faceBounds.getBottom() - contentTop - 48.0f * s);
bottomY - contentTop - 8.0f * s);
// Knob grid area — 2 rows x 4 cols
float gridLeft = faceBounds.getX() + 14.0f * s;
@ -214,23 +240,26 @@ void SaftpumpeEditor::computeLayout() {
for (int col = 0; col < knobsPerRow; ++col) {
int idx = row * knobsPerRow + col;
float cx = rowBounds[row].getX() + colW * static_cast<float>(col) + colW / 2.0f;
float cy = rowBounds[row].getY() + rowH * 0.40f;
float cy = rowBounds[row].getY() + rowH * 0.45f;
knobCentre[idx] = {cx, cy};
knobBounds[idx] = juce::Rectangle<float>(cx - knobRadius, cy - knobRadius,
knobRadius * 2.0f, knobRadius * 2.0f);
}
}
// Bottom: scope + zoom + preset
float bottomY = meterArea.getBottom() + 8.0f * s;
// Bottom: scope + side-by-side preset & zoom
float bottomH = faceBounds.getBottom() - bottomY - 10.0f * s;
float ctrlW = 110.0f * s;
float ctrlGap = 6.0f * s;
float rightX = faceBounds.getRight() - ctrlW - 14.0f * s;
float rightX = faceBounds.getRight() - ctrlW * 2.0f - 14.0f * s;
presetArea = juce::Rectangle<float>(rightX, bottomY, ctrlW, bottomH);
zoomArea = juce::Rectangle<float>(rightX, bottomY + bottomH * 0.48f, ctrlW, bottomH * 0.48f);
zoomArea = juce::Rectangle<float>(rightX + ctrlW + 4.0f * s, bottomY, ctrlW, bottomH);
scopeArea = juce::Rectangle<float>(faceBounds.getX() + 14.0f * s, bottomY,
presetArea.getX() - faceBounds.getX() - 24.0f * s, bottomH);
presetArea.getX() - faceBounds.getX() - 14.0f * s, bottomH);
// Bypass switch — top-left, below the screw
bypassArea = juce::Rectangle<float>(faceBounds.getX() + 20.0f * s,
faceBounds.getY() + 30.0f * s,
80.0f * s, 20.0f * s);
}
void SaftpumpeEditor::resized() {
@ -275,15 +304,32 @@ void SaftpumpeEditor::resized() {
setKnobBounds(mixSlider, mixLabel, mixLCD, 6);
setKnobBounds(autoReleaseSlider, autoReleaseLabel, autoReleaseLCD, 7);
presetLabel.setBounds(static_cast<int>(presetArea.getX()), static_cast<int>(presetArea.getY()),
static_cast<int>(presetArea.getWidth()), static_cast<int>(14.0f * s));
presetBox.setBounds(static_cast<int>(presetArea.getX()), static_cast<int>(presetArea.getY() + 14.0f * s),
static_cast<int>(presetArea.getWidth()), static_cast<int>(22.0f * s));
float ctrlLabelH = 14.0f * s;
float ctrlBoxH = 24.0f * s;
float ctrlGap = 2.0f * s;
float ctrlItemH = ctrlLabelH + ctrlGap + ctrlBoxH;
float ctrlPadY = (presetArea.getHeight() - ctrlItemH) / 2.0f;
zoomLabel.setBounds(static_cast<int>(zoomArea.getX()), static_cast<int>(zoomArea.getY()),
static_cast<int>(zoomArea.getWidth()), static_cast<int>(14.0f * s));
zoomBox.setBounds(static_cast<int>(zoomArea.getX()), static_cast<int>(zoomArea.getY() + 14.0f * s),
static_cast<int>(zoomArea.getWidth()), static_cast<int>(22.0f * s));
presetLabel.setBounds(presetArea.getX(), presetArea.getY() + ctrlPadY,
presetArea.getWidth(), ctrlLabelH);
presetBox.setBounds(presetArea.getX(), presetArea.getY() + ctrlPadY + ctrlLabelH + ctrlGap,
presetArea.getWidth(), ctrlBoxH);
zoomLabel.setBounds(zoomArea.getX(), zoomArea.getY() + ctrlPadY,
zoomArea.getWidth(), ctrlLabelH);
zoomBox.setBounds(zoomArea.getX(), zoomArea.getY() + ctrlPadY + ctrlLabelH + ctrlGap,
zoomArea.getWidth(), ctrlBoxH);
// Bypass switch
float bypassLabelW = 48.0f * s;
float bypassToggleW = 36.0f * s;
float bypassH = 16.0f * s;
bypassLabel.setBounds(static_cast<int>(bypassArea.getX()),
static_cast<int>(bypassArea.getY() + (bypassArea.getHeight() - bypassH) / 2.0f),
static_cast<int>(bypassLabelW), static_cast<int>(bypassH));
bypassButton.setBounds(static_cast<int>(bypassArea.getX() + bypassLabelW + 4.0f * s),
static_cast<int>(bypassArea.getY() + (bypassArea.getHeight() - bypassH) / 2.0f),
static_cast<int>(bypassToggleW), static_cast<int>(bypassH));
}
void SaftpumpeEditor::paint(juce::Graphics& g) {
@ -367,26 +413,61 @@ void SaftpumpeEditor::paint(juce::Graphics& g) {
// Meters
float grH = meterArea.getHeight() * 0.42f;
auto grBounds = juce::Rectangle<float>(meterArea.getX(), meterArea.getY(), meterArea.getWidth(), grH);
float meterLegW = 18.0f * s;
auto grBar = juce::Rectangle<float>(meterArea.getX(), meterArea.getY(),
meterArea.getWidth() - meterLegW, grH);
float gr = processorRef.getGainReductionMeter();
drawMeter(g, grBounds, gr, 0.0f, 24.0f, true);
drawPeakHold(g, grBounds, grPeak.peak, true);
drawMeter(g, grBar, gr, 0.0f, 24.0f, true);
drawPeakHold(g, grBar, grPeak.peak, true);
g.setColour(textDim);
g.setFont(juce::Font(juce::FontOptions(10.0f * s)));
g.drawText("GR", grBounds.translated(0, grBounds.getHeight() + 2.0f * s), juce::Justification::centredTop);
g.drawText("GR", grBar.translated(0, grBar.getHeight() + 2.0f * s), juce::Justification::centredTop);
float ioY = grBounds.getBottom() + 16.0f * s;
// GR scale legends
{
float lx = grBar.getRight() + 2.0f * s;
float lw = meterLegW - 2.0f * s;
g.setFont(juce::Font(juce::FontOptions(8.0f * s)));
g.setColour(textDim.withAlpha(0.6f));
const float grTicks[] = { 0.0f, 6.0f, 12.0f, 18.0f, 24.0f };
const char* grLabels[] = { "0", "-6", "-12", "-18", "-24" };
for (int i = 0; i < 5; ++i) {
float norm = grTicks[i] / 24.0f;
float ty = grBar.getY() + grBar.getHeight() * norm;
g.drawLine(grBar.getRight() - 2.0f * s, ty, grBar.getRight() + 2.0f * s, ty, 0.5f);
g.drawText(grLabels[i], lx, ty - 5.0f * s, lw, 10.0f * s, juce::Justification::centredLeft);
}
}
float ioY = grBar.getBottom() + 16.0f * s;
float ioH = meterArea.getBottom() - ioY - 14.0f * s;
float ioW = (meterArea.getWidth() - 3.0f * s) / 2.0f;
auto inB = juce::Rectangle<float>(meterArea.getX(), ioY, ioW, ioH);
auto outB = juce::Rectangle<float>(meterArea.getX() + ioW + 3.0f * s, ioY, ioW, ioH);
float ioBarW = (meterArea.getWidth() - meterLegW - 3.0f * s) / 2.0f;
auto inBar = juce::Rectangle<float>(meterArea.getX(), ioY, ioBarW, ioH);
auto outBar = juce::Rectangle<float>(meterArea.getX() + ioBarW + 3.0f * s, ioY, ioBarW, ioH);
drawMeter(g, inB, processorRef.getInputLevel(), -60.0f, 0.0f, false);
drawMeter(g, outB, processorRef.getOutputLevel(), -60.0f, 0.0f, false);
drawPeakHold(g, inB, inPeak.peak);
drawPeakHold(g, outB, outPeak.peak);
g.drawText("IN", inB.translated(0, ioH + 2.0f * s), juce::Justification::centredTop);
g.drawText("OUT", outB.translated(0, ioH + 2.0f * s), juce::Justification::centredTop);
drawMeter(g, inBar, processorRef.getInputLevel(), -60.0f, 0.0f, false);
drawMeter(g, outBar, processorRef.getOutputLevel(), -60.0f, 0.0f, false);
drawPeakHold(g, inBar, inPeak.peak);
drawPeakHold(g, outBar, outPeak.peak);
g.setColour(textDim);
g.setFont(juce::Font(juce::FontOptions(9.0f * s)));
g.drawText("IN", inBar.translated(0, ioH + 2.0f * s), juce::Justification::centredTop);
g.drawText("OUT", outBar.translated(0, ioH + 2.0f * s), juce::Justification::centredTop);
// VU scale legends
{
float lx = meterArea.getRight() - meterLegW + 2.0f * s;
float lw = meterLegW - 2.0f * s;
g.setFont(juce::Font(juce::FontOptions(7.5f * s)));
g.setColour(textDim.withAlpha(0.5f));
const float vuTicks[] = { 0.0f, 0.333f, 0.667f, 1.0f };
const char* vuLabels[] = { "0", "-20", "-40", "-60" };
for (int i = 0; i < 4; ++i) {
float ty = inBar.getBottom() - inBar.getHeight() * vuTicks[i];
g.drawLine(outBar.getRight() - 1.0f * s, ty, meterArea.getRight() - 1.0f * s, ty, 0.4f);
g.drawText(vuLabels[i], lx, ty - 4.0f * s, lw, 8.0f * s, juce::Justification::centredLeft);
}
}
// Scope
drawScope(g, scopeArea);
@ -395,6 +476,9 @@ void SaftpumpeEditor::paint(juce::Graphics& g) {
g.setColour(juce::Colour(0xff333338));
g.drawRoundedRectangle(zoomArea.reduced(2.0f * s), 2.0f * s, 0.5f);
g.drawRoundedRectangle(presetArea.reduced(2.0f * s), 2.0f * s, 0.5f);
// Bypass area border
g.drawRoundedRectangle(bypassArea.reduced(1.0f * s), 2.0f * s, 0.5f);
}
void SaftpumpeEditor::drawKnob(juce::Graphics& g, juce::Rectangle<float> bounds,
@ -562,6 +646,11 @@ void SaftpumpeEditor::drawPeakHold(juce::Graphics& g, juce::Rectangle<float> bou
}
void SaftpumpeEditor::drawScope(juce::Graphics& g, juce::Rectangle<float> bounds) {
if (processorRef.isSpectrumMode()) {
drawSpectrum(g, bounds);
return;
}
float s = uiScale;
g.setColour(grooveDark);
g.fillRoundedRectangle(bounds, 3.0f * s);
@ -579,6 +668,20 @@ void SaftpumpeEditor::drawScope(juce::Graphics& g, juce::Rectangle<float> bounds
auto& inData = processorRef.inputScope;
auto& outData = processorRef.outputScope;
int numPoints = processorRef.scopeSize;
float legendY = bounds.getBottom() - 10.0f * s;
float legendX = bounds.getX() + 6.0f * s;
g.setFont(juce::Font(juce::FontOptions(8.0f * s)));
g.setColour(redLed.withAlpha(0.5f));
g.drawText("IN", legendX, legendY, 16.0f * s, 8.0f * s, juce::Justification::centredLeft);
g.setColour(greenLed.withAlpha(0.4f));
g.drawText("OUT", legendX + 18.0f * s, legendY, 22.0f * s, 8.0f * s, juce::Justification::centredLeft);
g.setColour(textDim.withAlpha(0.3f));
g.setFont(juce::Font(juce::FontOptions(7.0f * s)));
g.drawText("WAVEFORM", bounds.getRight() - 48.0f * s, legendY, 42.0f * s, 8.0f * s,
juce::Justification::centredRight);
if (numPoints < 2) return;
float width = bounds.getWidth() - 4.0f;
@ -606,14 +709,101 @@ void SaftpumpeEditor::drawScope(juce::Graphics& g, juce::Rectangle<float> bounds
g.setColour(greenLed.withAlpha(0.65f));
g.strokePath(path, juce::PathStrokeType(1.0f));
}
}
float legendY = bounds.getBottom() - 12.0f * s;
void SaftpumpeEditor::drawSpectrum(juce::Graphics& g, juce::Rectangle<float> bounds) {
float s = uiScale;
g.setColour(grooveDark);
g.fillRoundedRectangle(bounds, 3.0f * s);
g.setColour(juce::Colours::black.withAlpha(0.4f));
g.drawRoundedRectangle(bounds.expanded(0.5f), 3.0f * s, 1.0f);
auto area = bounds.reduced(4.0f * s);
float w = area.getWidth();
float h = area.getHeight();
float ox = area.getX();
float oy = area.getY();
g.setColour(accentLine.withAlpha(0.05f));
for (int i = 0; i <= 4; ++i) {
float ly = oy + h * static_cast<float>(i) / 4.0f;
g.drawLine(ox, ly, ox + w, ly, 0.3f);
}
int numBins = processorRef.fftSize / 2;
if (numBins < 2) return;
float minLog = std::log10(20.0f);
float maxLog = std::log10(22050.0f);
auto freqToX = [&](float freq) -> float {
float logF = std::log10(juce::jmax(freq, 20.0f));
return ox + w * (logF - minLog) / (maxLog - minLog);
};
auto dbToY = [&](float db) -> float {
float norm = juce::jlimit(0.0f, 1.0f, (db + 80.0f) / 80.0f);
return oy + h * (1.0f - norm);
};
const float freqs[] = { 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000 };
g.setFont(juce::Font(juce::FontOptions(7.0f * s)));
g.setColour(textDim.withAlpha(0.4f));
for (float freq : freqs) {
float fx = freqToX(freq);
g.drawLine(fx, oy, fx, oy + h, 0.3f);
juce::String label = freq >= 1000 ? juce::String(freq / 1000.0f, 0) + "k"
: juce::String(static_cast<int>(freq));
g.drawText(label, fx - 10.0f * s, oy + h + 1.0f * s, 20.0f * s, 8.0f * s,
juce::Justification::centredTop);
}
const float dbTicks[] = { 0, -20, -40, -60 };
g.setColour(textDim.withAlpha(0.35f));
for (float db : dbTicks) {
float ly = dbToY(db);
g.drawLine(ox, ly, ox + w, ly, 0.3f);
g.drawText(juce::String(static_cast<int>(db)), ox - 22.0f * s, ly - 4.0f * s,
20.0f * s, 8.0f * s, juce::Justification::centredRight);
}
float binHz = 44100.0f / static_cast<float>(processorRef.fftSize);
auto drawSpectrumLine = [&](const std::array<float, SaftpumpeProcessor::fftSize / 2>& data,
juce::Colour col, float shadowAlpha, float lineAlpha, float strokeW) {
juce::Path path;
bool started = false;
for (int i = 1; i < numBins; ++i) {
float freq = binHz * static_cast<float>(i);
if (freq < 20.0f) continue;
float fx = freqToX(freq);
float db = data[i] * 80.0f - 80.0f;
float fy = dbToY(db);
if (!started) { path.startNewSubPath(fx, fy); started = true; }
else path.lineTo(fx, fy);
}
g.setColour(col.withAlpha(shadowAlpha));
g.strokePath(path, juce::PathStrokeType(strokeW * 2.5f));
g.setColour(col.withAlpha(lineAlpha));
g.strokePath(path, juce::PathStrokeType(strokeW));
};
drawSpectrumLine(processorRef.inputSpectrum, redLed, 0.12f, 0.75f, 1.2f);
drawSpectrumLine(processorRef.outputSpectrum, greenLed, 0.10f, 0.60f, 1.0f);
float legendY = bounds.getBottom() - 10.0f * s;
float legendX = bounds.getX() + 6.0f * s;
g.setFont(juce::Font(juce::FontOptions(9.0f * s)));
g.setColour(redLed.withAlpha(0.8f));
g.drawText("IN", legendX, legendY, 20.0f * s, 10.0f * s, juce::Justification::centredLeft);
g.setColour(greenLed.withAlpha(0.65f));
g.drawText("OUT", legendX + 24.0f * s, legendY, 26.0f * s, 10.0f * s, juce::Justification::centredLeft);
g.setFont(juce::Font(juce::FontOptions(8.0f * s)));
g.setColour(redLed.withAlpha(0.5f));
g.drawText("IN", legendX, legendY, 16.0f * s, 8.0f * s, juce::Justification::centredLeft);
g.setColour(greenLed.withAlpha(0.4f));
g.drawText("OUT", legendX + 18.0f * s, legendY, 22.0f * s, 8.0f * s, juce::Justification::centredLeft);
g.setColour(textDim.withAlpha(0.3f));
g.setFont(juce::Font(juce::FontOptions(7.0f * s)));
g.drawText("SPECTRUM", bounds.getRight() - 52.0f * s, legendY, 46.0f * s, 8.0f * s,
juce::Justification::centredRight);
}
void SaftpumpeEditor::drawScrew(juce::Graphics& g, float cx, float cy, float radius) {