#include "PluginEditor.h" TerionProcessorEditor::TerionProcessorEditor(TerionProcessor& p) : AudioProcessorEditor(p), processorRef(p), scope(p), fadeKnob(*p.apvts.getParameter("fadeTime")), sensitivityKnob(*p.apvts.getParameter("sensitivity")), dotSizeKnob(*p.apvts.getParameter("dotSize")), smoothingKnob(*p.apvts.getParameter("smoothing")), afterglowKnob(*p.apvts.getParameter("afterglow")) { setSize(kBaseWidth, kBaseHeight); setResizable(true, true); addAndMakeVisible(scope); // Fade knob addAndMakeVisible(fadeKnob); fadeKnob.setRange(0.1, 10.0); fadeAttachment = std::make_unique( processorRef.apvts, "fadeTime", fadeKnob); // Sensitivity knob addAndMakeVisible(sensitivityKnob); sensitivityKnob.setRange(-12.0, 24.0); sensitivityAttachment = std::make_unique( processorRef.apvts, "sensitivity", sensitivityKnob); // Dot size knob addAndMakeVisible(dotSizeKnob); dotSizeKnob.setRange(0.5, 8.0); dotSizeAttachment = std::make_unique( processorRef.apvts, "dotSize", dotSizeKnob); // Smoothing knob addAndMakeVisible(smoothingKnob); smoothingKnob.setRange(0.0, 0.99); smoothingAttachment = std::make_unique( processorRef.apvts, "smoothing", smoothingKnob); // Afterglow knob addAndMakeVisible(afterglowKnob); afterglowKnob.setRange(0.0, 5.0); afterglowAttachment = std::make_unique( processorRef.apvts, "afterglow", afterglowKnob); // Freeze button addAndMakeVisible(freezeButton); freezeButton.setButtonText("Freeze"); freezeButton.setColour(juce::ToggleButton::textColourId, juce::Colour(0xff888899)); freezeButton.setColour(juce::ToggleButton::tickColourId, juce::Colour(0xff7ecfcf)); freezeAttachment = std::make_unique( processorRef.apvts, "freeze", freezeButton); // Mono button addAndMakeVisible(monoButton); monoButton.setButtonText("Mono"); monoButton.setColour(juce::ToggleButton::textColourId, juce::Colour(0xff888899)); monoButton.setColour(juce::ToggleButton::tickColourId, juce::Colour(0xff7ecfcf)); monoAttachment = std::make_unique( processorRef.apvts, "mono", monoButton); // Scale dropdown addAndMakeVisible(scaleBox); scaleBox.addItem("100%", 1); scaleBox.addItem("125%", 2); scaleBox.addItem("150%", 3); scaleBox.addItem("200%", 4); scaleBox.addItem("250%", 5); scaleBox.addItem("300%", 6); scaleBox.setSelectedId(3, juce::dontSendNotification); scaleBox.onChange = [this] { scaleChanged(); }; scaleChanged(); // After any scale change, recompute the resize limits. const int maxDim = static_cast(kBaseWidth * kMaxScale); setResizeLimits(kBaseWidth, kBaseHeight, maxDim, maxDim); // Knob labels (separate from knobs to ensure correct z-order) auto setupLabel = [this](juce::Label& label, const juce::String& text) { addAndMakeVisible(label); label.setText(text, juce::dontSendNotification); label.setJustificationType(juce::Justification::centredTop); label.setColour(juce::Label::textColourId, juce::Colour(0xff888899)); label.setInterceptsMouseClicks(false, false); }; setupLabel(fadeLabel, fadeKnob.getParameterName()); setupLabel(sensitivityLabel, sensitivityKnob.getParameterName()); setupLabel(dotSizeLabel, dotSizeKnob.getParameterName()); setupLabel(smoothingLabel, smoothingKnob.getParameterName()); setupLabel(afterglowLabel, afterglowKnob.getParameterName()); } TerionProcessorEditor::~TerionProcessorEditor() {} void TerionProcessorEditor::scaleChanged() { int id = scaleBox.getSelectedId(); switch (id) { case 1: currentScale = 1.0f; break; case 2: currentScale = 1.25f; break; case 3: currentScale = 1.5f; break; case 4: currentScale = 2.0f; break; case 5: currentScale = 2.5f; break; case 6: currentScale = 3.0f; break; default: currentScale = 1.5f; break; } const int maxDim = static_cast(kBaseWidth * kMaxScale); setResizeLimits(kBaseWidth, kBaseHeight, maxDim, maxDim); scope.setScale(currentScale); setSize(static_cast(kBaseWidth * currentScale), static_cast(kBaseHeight * currentScale)); } void TerionProcessorEditor::paint(juce::Graphics& g) { g.fillAll(juce::Colour(0xff0a0a0f)); const float s = currentScale; const int footerH = static_cast(kFooterHeight * s); // Footer auto bounds = getLocalBounds(); auto footer = bounds.removeFromBottom(footerH); g.setColour(juce::Colour(0xff15151e)); g.fillRect(footer); g.setColour(juce::Colour(0xff555566)); g.setFont(juce::Font(juce::FontOptions(9.0f * s))); juce::String footerText = "Terion (Git: " + juce::String(TERION_GIT_HASH) + ", " + juce::String(TERION_GIT_DIRTY) + ") | built: " + juce::String(TERION_BUILD_TIME); g.drawFittedText(footerText, footer.reduced(static_cast(10.0f * s), 0), juce::Justification::centredLeft, 1); } void TerionProcessorEditor::resized() { const float s = currentScale; const int footerH = static_cast(kFooterHeight * s); const int knobs = static_cast(kKnobSize * s); const int margin = static_cast(kKnobMargin * s); const int scaleW = static_cast(76.0f * s); const int gap = static_cast(8.0f * s); auto bounds = getLocalBounds(); auto footer = bounds.removeFromBottom(footerH); // Footer: scale dropdown with an 8px gap on the top and right scaleBox.setBounds(footer.getWidth() - scaleW - gap, gap, scaleW, footerH - 2 - gap); // Scope fills the remaining area scope.setBounds(bounds); // Controls layout: knob row sits 8px above the footer, Freeze/Mono stay top-right const int numKnobs = 5; const int rowH = knobs + static_cast(12.0f * s) - static_cast(6.0f * s); const int startX = bounds.getWidth() - (numKnobs * (knobs + margin) + knobs + margin); const int startY = bounds.getHeight() - rowH - gap; fadeKnob.setBounds(startX, startY, knobs, knobs); sensitivityKnob.setBounds(startX + (knobs + margin), startY, knobs, knobs); dotSizeKnob.setBounds(startX + 2 * (knobs + margin), startY, knobs, knobs); smoothingKnob.setBounds(startX + 3 * (knobs + margin), startY, knobs, knobs); afterglowKnob.setBounds(startX + 4 * (knobs + margin), startY, knobs, knobs); const int freezeY = static_cast(20.0f * s); const int freezeH = knobs - static_cast(20.0f * s); freezeButton.setBounds(bounds.getWidth() - knobs - margin, freezeY, knobs, freezeH); monoButton.setBounds(bounds.getWidth() - knobs - margin, freezeY + freezeH + gap, knobs, freezeH); const int labelY = startY + knobs - static_cast(6.0f * s); const int labelH = static_cast(12.0f * s); const float labelFont = 9.0f * s; fadeLabel.setBounds(fadeKnob.getX(), labelY, knobs, labelH); fadeLabel.setFont(juce::Font(juce::FontOptions(labelFont))); sensitivityLabel.setBounds(sensitivityKnob.getX(), labelY, knobs, labelH); sensitivityLabel.setFont(juce::Font(juce::FontOptions(labelFont))); dotSizeLabel.setBounds(dotSizeKnob.getX(), labelY, knobs, labelH); dotSizeLabel.setFont(juce::Font(juce::FontOptions(labelFont))); smoothingLabel.setBounds(smoothingKnob.getX(), labelY, knobs, labelH); smoothingLabel.setFont(juce::Font(juce::FontOptions(labelFont))); afterglowLabel.setBounds(afterglowKnob.getX(), labelY, knobs, labelH); afterglowLabel.setFont(juce::Font(juce::FontOptions(labelFont))); }