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) SaftpumpeEditor::SaftpumpeEditor(SaftpumpeProcessor& p)
: AudioProcessorEditor(&p), processorRef(p) : AudioProcessorEditor(&p), processorRef(p)
{ {
setSize(700, 500); setSize(700, 560);
setResizable(true, this); setResizable(true, this);
setResizeLimits(650, 480, 1100, 800); setResizeLimits(650, 480, 1100, 900);
startTimerHz(30); startTimerHz(30);
auto setupKnob = [this](juce::Slider& s, juce::Label& l, const juce::String& text, auto setupKnob = [this](juce::Slider& s, juce::Label& l, const juce::String& text,
@ -99,7 +99,7 @@ SaftpumpeEditor::SaftpumpeEditor(SaftpumpeProcessor& p)
setupLCD(autoReleaseLCD); setupLCD(autoReleaseLCD);
// Zoom dropdown // Zoom dropdown
zoomLabel.setText("ZOOM", juce::dontSendNotification); zoomLabel.setText("UI SCALE", juce::dontSendNotification);
zoomLabel.setJustificationType(juce::Justification::centredRight); zoomLabel.setJustificationType(juce::Justification::centredRight);
zoomLabel.setColour(juce::Label::textColourId, textDim); zoomLabel.setColour(juce::Label::textColourId, textDim);
zoomLabel.setFont(juce::Font(juce::FontOptions(10.0f))); zoomLabel.setFont(juce::Font(juce::FontOptions(10.0f)));
@ -110,7 +110,7 @@ SaftpumpeEditor::SaftpumpeEditor(SaftpumpeProcessor& p)
zoomBox.addItem("2.0x", 3); zoomBox.addItem("2.0x", 3);
zoomBox.addItem("2.5x", 4); zoomBox.addItem("2.5x", 4);
zoomBox.addItem("3.0x", 5); 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::backgroundColourId, grooveDark);
zoomBox.setColour(juce::ComboBox::textColourId, textColour); zoomBox.setColour(juce::ComboBox::textColourId, textColour);
zoomBox.setColour(juce::ComboBox::arrowColourId, textDim); zoomBox.setColour(juce::ComboBox::arrowColourId, textDim);
@ -135,6 +135,18 @@ SaftpumpeEditor::SaftpumpeEditor(SaftpumpeProcessor& p)
presetBox.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff333338)); presetBox.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff333338));
presetBox.addListener(this); presetBox.addListener(this);
addAndMakeVisible(presetBox); 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() { SaftpumpeEditor::~SaftpumpeEditor() {
@ -146,6 +158,7 @@ SaftpumpeEditor::~SaftpumpeEditor() {
makeupSlider.setLookAndFeel(nullptr); makeupSlider.setLookAndFeel(nullptr);
mixSlider.setLookAndFeel(nullptr); mixSlider.setLookAndFeel(nullptr);
autoReleaseSlider.setLookAndFeel(nullptr); autoReleaseSlider.setLookAndFeel(nullptr);
bypassButton.setLookAndFeel(nullptr);
zoomBox.removeListener(this); zoomBox.removeListener(this);
presetBox.removeListener(this); presetBox.removeListener(this);
stopTimer(); stopTimer();
@ -157,7 +170,7 @@ void SaftpumpeEditor::comboBoxChanged(juce::ComboBox* box) {
int id = box->getSelectedId(); int id = box->getSelectedId();
if (id >= 1 && id <= 5) { if (id >= 1 && id <= 5) {
uiScale = scales[id - 1]; 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) { } else if (box == &presetBox) {
int id = box->getSelectedId() - 1; 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) { juce::String SaftpumpeEditor::formatValue(int idx, float val) {
switch (idx) { switch (idx) {
case 0: return juce::String(val, 1) + " dB"; case 0: return juce::String(val, 1) + " dB";
@ -184,17 +205,22 @@ void SaftpumpeEditor::computeLayout() {
auto bounds = getLocalBounds().toFloat(); auto bounds = getLocalBounds().toFloat();
auto faceBounds = bounds.reduced(8.0f * s); auto faceBounds = bounds.reduced(8.0f * s);
// Header // Content starts below accent line (accent line at faceY + 80*s)
float headerH = 28.0f * s + 16.0f * s + 8.0f * s; float accentY = faceBounds.getY() + 80.0f * s;
float headerY = faceBounds.getY() + 6.0f * s; float contentTop = accentY + 22.0f * s;
float contentTop = headerY + headerH + 4.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; 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, contentTop,
meterW, meterW,
faceBounds.getBottom() - contentTop - 48.0f * s); bottomY - contentTop - 8.0f * s);
// Knob grid area — 2 rows x 4 cols // Knob grid area — 2 rows x 4 cols
float gridLeft = faceBounds.getX() + 14.0f * s; float gridLeft = faceBounds.getX() + 14.0f * s;
@ -214,23 +240,26 @@ void SaftpumpeEditor::computeLayout() {
for (int col = 0; col < knobsPerRow; ++col) { for (int col = 0; col < knobsPerRow; ++col) {
int idx = row * knobsPerRow + col; int idx = row * knobsPerRow + col;
float cx = rowBounds[row].getX() + colW * static_cast<float>(col) + colW / 2.0f; 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}; knobCentre[idx] = {cx, cy};
knobBounds[idx] = juce::Rectangle<float>(cx - knobRadius, cy - knobRadius, knobBounds[idx] = juce::Rectangle<float>(cx - knobRadius, cy - knobRadius,
knobRadius * 2.0f, knobRadius * 2.0f); knobRadius * 2.0f, knobRadius * 2.0f);
} }
} }
// Bottom: scope + zoom + preset // Bottom: scope + side-by-side preset & zoom
float bottomY = meterArea.getBottom() + 8.0f * s;
float bottomH = faceBounds.getBottom() - bottomY - 10.0f * s; float bottomH = faceBounds.getBottom() - bottomY - 10.0f * s;
float ctrlW = 110.0f * s; float rightX = faceBounds.getRight() - ctrlW * 2.0f - 14.0f * s;
float ctrlGap = 6.0f * s;
float rightX = faceBounds.getRight() - ctrlW - 14.0f * s;
presetArea = juce::Rectangle<float>(rightX, bottomY, ctrlW, bottomH); 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, 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() { void SaftpumpeEditor::resized() {
@ -275,15 +304,32 @@ void SaftpumpeEditor::resized() {
setKnobBounds(mixSlider, mixLabel, mixLCD, 6); setKnobBounds(mixSlider, mixLabel, mixLCD, 6);
setKnobBounds(autoReleaseSlider, autoReleaseLabel, autoReleaseLCD, 7); setKnobBounds(autoReleaseSlider, autoReleaseLabel, autoReleaseLCD, 7);
presetLabel.setBounds(static_cast<int>(presetArea.getX()), static_cast<int>(presetArea.getY()), float ctrlLabelH = 14.0f * s;
static_cast<int>(presetArea.getWidth()), static_cast<int>(14.0f * s)); float ctrlBoxH = 24.0f * s;
presetBox.setBounds(static_cast<int>(presetArea.getX()), static_cast<int>(presetArea.getY() + 14.0f * s), float ctrlGap = 2.0f * s;
static_cast<int>(presetArea.getWidth()), static_cast<int>(22.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()), presetLabel.setBounds(presetArea.getX(), presetArea.getY() + ctrlPadY,
static_cast<int>(zoomArea.getWidth()), static_cast<int>(14.0f * s)); presetArea.getWidth(), ctrlLabelH);
zoomBox.setBounds(static_cast<int>(zoomArea.getX()), static_cast<int>(zoomArea.getY() + 14.0f * s), presetBox.setBounds(presetArea.getX(), presetArea.getY() + ctrlPadY + ctrlLabelH + ctrlGap,
static_cast<int>(zoomArea.getWidth()), static_cast<int>(22.0f * s)); 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) { void SaftpumpeEditor::paint(juce::Graphics& g) {
@ -367,26 +413,61 @@ void SaftpumpeEditor::paint(juce::Graphics& g) {
// Meters // Meters
float grH = meterArea.getHeight() * 0.42f; 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(); float gr = processorRef.getGainReductionMeter();
drawMeter(g, grBounds, gr, 0.0f, 24.0f, true); drawMeter(g, grBar, gr, 0.0f, 24.0f, true);
drawPeakHold(g, grBounds, grPeak.peak, true); drawPeakHold(g, grBar, grPeak.peak, true);
g.setColour(textDim); g.setColour(textDim);
g.setFont(juce::Font(juce::FontOptions(10.0f * s))); 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 ioH = meterArea.getBottom() - ioY - 14.0f * s;
float ioW = (meterArea.getWidth() - 3.0f * s) / 2.0f; float ioBarW = (meterArea.getWidth() - meterLegW - 3.0f * s) / 2.0f;
auto inB = juce::Rectangle<float>(meterArea.getX(), ioY, ioW, ioH); auto inBar = juce::Rectangle<float>(meterArea.getX(), ioY, ioBarW, ioH);
auto outB = juce::Rectangle<float>(meterArea.getX() + ioW + 3.0f * s, ioY, ioW, 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, inBar, processorRef.getInputLevel(), -60.0f, 0.0f, false);
drawMeter(g, outB, processorRef.getOutputLevel(), -60.0f, 0.0f, false); drawMeter(g, outBar, processorRef.getOutputLevel(), -60.0f, 0.0f, false);
drawPeakHold(g, inB, inPeak.peak); drawPeakHold(g, inBar, inPeak.peak);
drawPeakHold(g, outB, outPeak.peak); drawPeakHold(g, outBar, outPeak.peak);
g.drawText("IN", inB.translated(0, ioH + 2.0f * s), juce::Justification::centredTop); g.setColour(textDim);
g.drawText("OUT", outB.translated(0, ioH + 2.0f * s), juce::Justification::centredTop); 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 // Scope
drawScope(g, scopeArea); drawScope(g, scopeArea);
@ -395,6 +476,9 @@ void SaftpumpeEditor::paint(juce::Graphics& g) {
g.setColour(juce::Colour(0xff333338)); g.setColour(juce::Colour(0xff333338));
g.drawRoundedRectangle(zoomArea.reduced(2.0f * s), 2.0f * s, 0.5f); g.drawRoundedRectangle(zoomArea.reduced(2.0f * s), 2.0f * s, 0.5f);
g.drawRoundedRectangle(presetArea.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, 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) { void SaftpumpeEditor::drawScope(juce::Graphics& g, juce::Rectangle<float> bounds) {
if (processorRef.isSpectrumMode()) {
drawSpectrum(g, bounds);
return;
}
float s = uiScale; float s = uiScale;
g.setColour(grooveDark); g.setColour(grooveDark);
g.fillRoundedRectangle(bounds, 3.0f * s); 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& inData = processorRef.inputScope;
auto& outData = processorRef.outputScope; auto& outData = processorRef.outputScope;
int numPoints = processorRef.scopeSize; 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; if (numPoints < 2) return;
float width = bounds.getWidth() - 4.0f; 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.setColour(greenLed.withAlpha(0.65f));
g.strokePath(path, juce::PathStrokeType(1.0f)); 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; float legendX = bounds.getX() + 6.0f * s;
g.setFont(juce::Font(juce::FontOptions(9.0f * s))); g.setFont(juce::Font(juce::FontOptions(8.0f * s)));
g.setColour(redLed.withAlpha(0.8f)); g.setColour(redLed.withAlpha(0.5f));
g.drawText("IN", legendX, legendY, 20.0f * s, 10.0f * s, juce::Justification::centredLeft); g.drawText("IN", legendX, legendY, 16.0f * s, 8.0f * s, juce::Justification::centredLeft);
g.setColour(greenLed.withAlpha(0.65f)); g.setColour(greenLed.withAlpha(0.4f));
g.drawText("OUT", legendX + 24.0f * s, legendY, 26.0f * s, 10.0f * s, juce::Justification::centredLeft); 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) { void SaftpumpeEditor::drawScrew(juce::Graphics& g, float cx, float cy, float radius) {

View file

@ -13,11 +13,12 @@ public:
void resized() override; void resized() override;
void timerCallback() override; void timerCallback() override;
void comboBoxChanged(juce::ComboBox* box) override; void comboBoxChanged(juce::ComboBox* box) override;
void mouseUp(const juce::MouseEvent& event) override;
private: private:
SaftpumpeProcessor& processorRef; SaftpumpeProcessor& processorRef;
float uiScale = 1.5f; float uiScale = 1.0f;
// Dark industrial palette // Dark industrial palette
juce::Colour bezelColour{0xff0a0a0c}; juce::Colour bezelColour{0xff0a0a0c};
@ -51,11 +52,16 @@ private:
juce::ComboBox presetBox; juce::ComboBox presetBox;
juce::Label presetLabel; juce::Label presetLabel;
juce::ToggleButton bypassButton;
juce::Label bypassLabel;
using SliderAttachment = juce::AudioProcessorValueTreeState::SliderAttachment; using SliderAttachment = juce::AudioProcessorValueTreeState::SliderAttachment;
using ButtonAttachment = juce::AudioProcessorValueTreeState::ButtonAttachment;
std::unique_ptr<SliderAttachment> thresholdAttachment, ratioAttachment, attackAttachment; std::unique_ptr<SliderAttachment> thresholdAttachment, ratioAttachment, attackAttachment;
std::unique_ptr<SliderAttachment> releaseAttachment, kneeAttachment, makeupAttachment; std::unique_ptr<SliderAttachment> releaseAttachment, kneeAttachment, makeupAttachment;
std::unique_ptr<SliderAttachment> mixAttachment, autoReleaseAttachment; std::unique_ptr<SliderAttachment> mixAttachment, autoReleaseAttachment;
std::unique_ptr<ButtonAttachment> bypassAttachment;
juce::Rectangle<float> meterArea; juce::Rectangle<float> meterArea;
juce::Rectangle<float> scopeArea; juce::Rectangle<float> scopeArea;
@ -80,6 +86,7 @@ private:
void drawPeakHold(juce::Graphics& g, juce::Rectangle<float> bounds, void drawPeakHold(juce::Graphics& g, juce::Rectangle<float> bounds,
float peakNorm, bool fromTop = false); float peakNorm, bool fromTop = false);
void drawScope(juce::Graphics& g, juce::Rectangle<float> bounds); void drawScope(juce::Graphics& g, juce::Rectangle<float> bounds);
void drawSpectrum(juce::Graphics& g, juce::Rectangle<float> bounds);
void drawScrew(juce::Graphics& g, float cx, float cy, float radius); void drawScrew(juce::Graphics& g, float cx, float cy, float radius);
static constexpr float startAngle = juce::MathConstants<float>::pi * 0.75f; static constexpr float startAngle = juce::MathConstants<float>::pi * 0.75f;
@ -95,11 +102,58 @@ private:
void applyPreset(int index); void applyPreset(int index);
juce::Rectangle<float> presetArea; juce::Rectangle<float> presetArea;
juce::Rectangle<float> bypassArea;
struct NoDotLookAndFeel : juce::LookAndFeel_V4 { struct NoDotLookAndFeel : juce::LookAndFeel_V4 {
void drawRotarySlider(juce::Graphics&, int, int, int, int, void drawRotarySlider(juce::Graphics&, int, int, int, int,
float, float, float, juce::Slider&) override {} float, float, float, juce::Slider&) override {}
} lnf; } lnf;
struct ToggleSwitchLNF : juce::LookAndFeel_V4 {
void drawToggleButton(juce::Graphics& g, juce::ToggleButton& btn,
bool hover, bool /*isDown*/) override {
auto bounds = btn.getLocalBounds().toFloat();
float s = bounds.getHeight() / 16.0f;
float w = bounds.getWidth();
float h = bounds.getHeight();
float cornerR = h * 0.3f;
// Groove
g.setColour(juce::Colour(0xff111114));
g.fillRoundedRectangle(bounds, cornerR);
g.setColour(juce::Colours::black.withAlpha(0.5f));
g.drawRoundedRectangle(bounds.expanded(0.5f), cornerR, 1.0f);
// Slider handle
bool on = btn.getToggleState();
float handleW = w * 0.42f;
float handleH = h * 0.72f;
float handleX = on ? (bounds.getRight() - handleW - 3.0f * s) : (bounds.getX() + 3.0f * s);
float handleY = bounds.getY() + (h - handleH) / 2.0f;
auto handleBounds = juce::Rectangle<float>(handleX, handleY, handleW, handleH);
// Handle shadow
g.setColour(juce::Colours::black.withAlpha(0.4f));
g.fillRoundedRectangle(handleBounds.translated(0, 1.0f), cornerR * 0.6f);
// Handle body
juce::Colour handleCol = on ? juce::Colour(0xff00e676) : juce::Colour(0xff4a4a50);
if (hover) handleCol = handleCol.brighter(0.15f);
g.setColour(handleCol);
g.fillRoundedRectangle(handleBounds, cornerR * 0.6f);
// Handle highlight
g.setColour(juce::Colours::white.withAlpha(on ? 0.2f : 0.08f));
g.fillRoundedRectangle(handleBounds.withHeight(handleH * 0.4f).translated(0, 1.0f), cornerR * 0.4f);
// LED glow when bypass active
if (on) {
g.setColour(juce::Colour(0xff00e676).withAlpha(0.15f));
g.fillEllipse(bounds.getCentreX() - w * 0.5f, bounds.getCentreY() - h * 0.6f,
w * 1.0f, h * 1.2f);
}
}
} toggleLnf;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SaftpumpeEditor) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SaftpumpeEditor)
}; };