From 9d0da19f488b1abdb401959b49b3553ff8d7516a Mon Sep 17 00:00:00 2001 From: Armin Date: Mon, 13 Jul 2026 01:44:13 +0200 Subject: [PATCH] add bypass toggle switch UI to top-left --- Source/PluginEditor.cpp | 286 +++++++++++++++++++++++++++++++++------- Source/PluginEditor.h | 56 +++++++- 2 files changed, 293 insertions(+), 49 deletions(-) diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 6a04695..14ad1a2 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -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(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(700 * uiScale), static_cast(500 * uiScale)); + setSize(static_cast(700 * uiScale), static_cast(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(faceBounds.getRight() - meterW - 14.0f * s, + float meterLegW = 18.0f * s; + meterArea = juce::Rectangle(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(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(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(rightX, bottomY, ctrlW, bottomH); - zoomArea = juce::Rectangle(rightX, bottomY + bottomH * 0.48f, ctrlW, bottomH * 0.48f); + zoomArea = juce::Rectangle(rightX + ctrlW + 4.0f * s, bottomY, ctrlW, bottomH); scopeArea = juce::Rectangle(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(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(presetArea.getX()), static_cast(presetArea.getY()), - static_cast(presetArea.getWidth()), static_cast(14.0f * s)); - presetBox.setBounds(static_cast(presetArea.getX()), static_cast(presetArea.getY() + 14.0f * s), - static_cast(presetArea.getWidth()), static_cast(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(zoomArea.getX()), static_cast(zoomArea.getY()), - static_cast(zoomArea.getWidth()), static_cast(14.0f * s)); - zoomBox.setBounds(static_cast(zoomArea.getX()), static_cast(zoomArea.getY() + 14.0f * s), - static_cast(zoomArea.getWidth()), static_cast(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(bypassArea.getX()), + static_cast(bypassArea.getY() + (bypassArea.getHeight() - bypassH) / 2.0f), + static_cast(bypassLabelW), static_cast(bypassH)); + bypassButton.setBounds(static_cast(bypassArea.getX() + bypassLabelW + 4.0f * s), + static_cast(bypassArea.getY() + (bypassArea.getHeight() - bypassH) / 2.0f), + static_cast(bypassToggleW), static_cast(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(meterArea.getX(), meterArea.getY(), meterArea.getWidth(), grH); + float meterLegW = 18.0f * s; + auto grBar = juce::Rectangle(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(meterArea.getX(), ioY, ioW, ioH); - auto outB = juce::Rectangle(meterArea.getX() + ioW + 3.0f * s, ioY, ioW, ioH); + float ioBarW = (meterArea.getWidth() - meterLegW - 3.0f * s) / 2.0f; + auto inBar = juce::Rectangle(meterArea.getX(), ioY, ioBarW, ioH); + auto outBar = juce::Rectangle(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 bounds, @@ -562,6 +646,11 @@ void SaftpumpeEditor::drawPeakHold(juce::Graphics& g, juce::Rectangle bou } void SaftpumpeEditor::drawScope(juce::Graphics& g, juce::Rectangle 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 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 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 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(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(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(db)), ox - 22.0f * s, ly - 4.0f * s, + 20.0f * s, 8.0f * s, juce::Justification::centredRight); + } + + float binHz = 44100.0f / static_cast(processorRef.fftSize); + + auto drawSpectrumLine = [&](const std::array& 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(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) { diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index a6d9531..22fb879 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -13,11 +13,12 @@ public: void resized() override; void timerCallback() override; void comboBoxChanged(juce::ComboBox* box) override; + void mouseUp(const juce::MouseEvent& event) override; private: SaftpumpeProcessor& processorRef; - float uiScale = 1.5f; + float uiScale = 1.0f; // Dark industrial palette juce::Colour bezelColour{0xff0a0a0c}; @@ -51,11 +52,16 @@ private: juce::ComboBox presetBox; juce::Label presetLabel; + juce::ToggleButton bypassButton; + juce::Label bypassLabel; + using SliderAttachment = juce::AudioProcessorValueTreeState::SliderAttachment; + using ButtonAttachment = juce::AudioProcessorValueTreeState::ButtonAttachment; std::unique_ptr thresholdAttachment, ratioAttachment, attackAttachment; std::unique_ptr releaseAttachment, kneeAttachment, makeupAttachment; std::unique_ptr mixAttachment, autoReleaseAttachment; + std::unique_ptr bypassAttachment; juce::Rectangle meterArea; juce::Rectangle scopeArea; @@ -80,6 +86,7 @@ private: void drawPeakHold(juce::Graphics& g, juce::Rectangle bounds, float peakNorm, bool fromTop = false); void drawScope(juce::Graphics& g, juce::Rectangle bounds); + void drawSpectrum(juce::Graphics& g, juce::Rectangle bounds); void drawScrew(juce::Graphics& g, float cx, float cy, float radius); static constexpr float startAngle = juce::MathConstants::pi * 0.75f; @@ -95,11 +102,58 @@ private: void applyPreset(int index); juce::Rectangle presetArea; + juce::Rectangle bypassArea; struct NoDotLookAndFeel : juce::LookAndFeel_V4 { void drawRotarySlider(juce::Graphics&, int, int, int, int, float, float, float, juce::Slider&) override {} } 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(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) };