horizont/Source/PluginEditor.cpp

354 lines
13 KiB
C++
Raw Normal View History

2026-08-10 12:55:02 +02:00
#include "PluginEditor.h"
#include <cmath>
#include <limits>
namespace
{
juce::String formatHz (float v)
{
return juce::String (juce::roundToInt (v)) + " Hz";
}
juce::String formatSec (float v)
{
return juce::String (v, 1) + " s";
}
juce::String formatSize (float v)
{
return juce::String (v, 2) + "x";
}
juce::String formatPct (float v)
{
return juce::String (juce::roundToInt (v * 100.0f)) + " %";
}
juce::String formatSemis (float v)
{
const int semis = juce::roundToInt (v);
if (semis > 0)
return "+" + juce::String (semis) + " st";
if (semis < 0)
return juce::String (semis) + " st";
return "0 st";
}
juce::StringArray presetNames()
{
juce::StringArray names;
for (const auto& p : getPresets())
names.add (p.name);
return names;
}
constexpr float scalePresets[] = { 0.75f, 0.9f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f };
constexpr int numScalePresets = (int) (sizeof (scalePresets) / sizeof (scalePresets[0]));
juce::StringArray scaleNames()
{
juce::StringArray names;
for (float v : scalePresets)
names.add (juce::String (juce::roundToInt (v * 100.0f)) + " %");
return names;
}
juce::String buildFooterText()
{
#ifndef HORIZONT_VERSION
#define HORIZONT_VERSION "0.0.0"
#endif
#ifndef HORIZONT_GIT_REVISION
#define HORIZONT_GIT_REVISION "unknown"
#endif
#ifndef HORIZONT_BUILD_TIMESTAMP
#define HORIZONT_BUILD_TIMESTAMP "unknown"
#endif
juce::String text ("HORIZONT");
2026-08-10 12:55:02 +02:00
text << " · git " << HORIZONT_GIT_REVISION;
text << " · build " << HORIZONT_BUILD_TIMESTAMP;
return text;
}
}
HorizontAudioProcessorEditor::HorizontAudioProcessorEditor (HorizontAudioProcessor& p,
juce::AudioProcessorValueTreeState& s)
: AudioProcessorEditor (&p),
processor (p),
apvts (s),
display (p)
{
lookAndFeel = std::make_unique<HorizontLookAndFeel>();
setLookAndFeel (lookAndFeel.get());
titleLabel.setText ("HORIZONT", juce::dontSendNotification);
titleLabel.setFont (juce::Font (juce::FontOptions (22.0f, juce::Font::bold)));
titleLabel.setColour (juce::Label::textColourId, juce::Colour (0xFFE8F6F4));
addAndMakeVisible (titleLabel);
subtitleLabel.setText ("Algorithmic Spatial Processor", juce::dontSendNotification);
subtitleLabel.setFont (juce::Font (juce::FontOptions (11.0f)));
subtitleLabel.setColour (juce::Label::textColourId, juce::Colour (0xFF5E8688));
addAndMakeVisible (subtitleLabel);
presetBox.addItemList (presetNames(), 1);
presetBox.setSelectedItemIndex (0);
presetBox.addListener (this);
presetBox.setTooltip ("Recall a factory preset");
addAndMakeVisible (presetBox);
scaleBox.addItemList (scaleNames(), 1);
scaleBox.setSelectedItemIndex (2);
scaleBox.addListener (this);
scaleBox.setTooltip ("Set the UI scale");
addAndMakeVisible (scaleBox);
footerLabel.setText (buildFooterText(), juce::dontSendNotification);
footerLabel.setJustificationType (juce::Justification::centred);
footerLabel.setInterceptsMouseClicks (false, false);
addAndMakeVisible (footerLabel);
addAndMakeVisible (display);
auto makeKnob = [this] (const juce::String& title, const juce::String& id,
const juce::String& tip, std::function<juce::String (float)> fmt,
float defVal) -> std::unique_ptr<Knob>
{
return std::make_unique<Knob> (title, apvts, id, defVal, fmt, tip);
};
knobs.push_back (makeKnob ("LOW CUT", "lowCut", "Input high-pass filter (20 Hz - 500 Hz)", formatHz, 40.0f));
knobs.push_back (makeKnob ("HIGH CUT", "highCut", "Input low-pass filter (800 Hz - 20 kHz)", formatHz, 14000.0f));
knobs.push_back (makeKnob ("DECAY", "decay", "Length of the reverb tail (RT60)", formatSec, 3.5f));
knobs.push_back (makeKnob ("SIZE", "size", "Room size scaling", formatSize, 1.0f));
knobs.push_back (makeKnob ("DIFFUSION", "diffusion", "Echo diffusion and smearing", formatPct, 0.6f));
knobs.push_back (makeKnob ("BRIGHTNESS", "brightness", "High-frequency damping of the tail", formatPct, 0.75f));
knobs.push_back (makeKnob ("FEEDBACK", "feedback", "Recirculation through the pitch shifter", formatPct, 0.6f));
knobs.push_back (makeKnob ("PITCH", "pitch", "Pitch shift applied per feedback pass", formatSemis, 0.0f));
knobs.push_back (makeKnob ("DRY / WET", "dryWet", "Dry / wet mix", formatPct, 0.35f));
for (auto& k : knobs)
addAndMakeVisible (k.get());
setSize (juce::roundToInt (baseWidth), juce::roundToInt (baseHeight));
setResizeLimits (juce::roundToInt (baseWidth * 0.75f), juce::roundToInt (baseHeight * 0.75f),
juce::roundToInt (baseWidth * 2.0f), juce::roundToInt (baseHeight * 2.0f));
}
HorizontAudioProcessorEditor::~HorizontAudioProcessorEditor()
{
}
void HorizontAudioProcessorEditor::setScaleFactor (float newScale)
{
newScale = juce::jlimit (0.6f, 2.0f, newScale);
setSize (juce::roundToInt (baseWidth * newScale),
juce::roundToInt (baseHeight * newScale));
}
void HorizontAudioProcessorEditor::updateScaleFactor()
{
const float newScale = juce::jmin ((float) getWidth() / baseWidth,
(float) getHeight() / baseHeight);
if (juce::approximatelyEqual (scaleFactor, newScale))
return;
scaleFactor = newScale;
for (auto& k : knobs)
k->setScaleFactor (scaleFactor);
display.setScaleFactor (scaleFactor);
}
void HorizontAudioProcessorEditor::comboBoxChanged (juce::ComboBox* box)
{
if (box == &presetBox)
{
const int idx = presetBox.getSelectedItemIndex();
const auto& presets = processor.getPresets();
if (idx >= 0 && idx < (int) presets.size())
processor.applyPreset (presets[(size_t) idx]);
}
else if (box == &scaleBox)
{
const int idx = scaleBox.getSelectedItemIndex();
if (idx >= 0 && idx < numScalePresets)
setScaleFactor (scalePresets[idx]);
}
}
void HorizontAudioProcessorEditor::updateScaleSelection()
{
int best = 0;
float bestDiff = std::numeric_limits<float>::max();
for (int i = 0; i < numScalePresets; ++i)
{
const float diff = std::abs (scaleFactor - scalePresets[i]);
if (diff < bestDiff)
{
bestDiff = diff;
best = i;
}
}
scaleBox.setSelectedItemIndex (best, juce::dontSendNotification);
}
void HorizontAudioProcessorEditor::layoutKnobRow (juce::Rectangle<int> area, int startIndex, int count)
{
juce::FlexBox fb;
fb.flexDirection = juce::FlexBox::Direction::row;
fb.justifyContent = juce::FlexBox::JustifyContent::center;
fb.alignItems = juce::FlexBox::AlignItems::stretch;
const float minW = 80.0f * scaleFactor;
const float minH = 96.0f * scaleFactor;
for (int i = startIndex; i < startIndex + count; ++i)
fb.items.add (juce::FlexItem (*knobs[(size_t) i]).withMinWidth (minW).withMinHeight (minH).withFlex (1.0f));
fb.performLayout (area);
}
void HorizontAudioProcessorEditor::mouseWheelMove (const juce::MouseEvent&, const juce::MouseWheelDetails& wheel)
{
if (wheel.deltaY != 0.0f)
{
const float zoomStep = 0.1f;
setScaleFactor (scaleFactor - wheel.deltaY * zoomStep);
}
}
void HorizontAudioProcessorEditor::resized()
{
updateScaleFactor();
updateScaleSelection();
auto area = getLocalBounds();
const float s = scaleFactor;
const int headerH = juce::roundToInt (64.0f * s);
const int footerH = juce::roundToInt (26.0f * s);
const int presetBoxW = juce::roundToInt (180.0f * s);
const int scaleBoxW = juce::roundToInt (72.0f * s);
const int boxGap = juce::roundToInt (10.0f * s);
const int presetBoxMargin = juce::roundToInt (20.0f * s);
const int titleH = juce::roundToInt (32.0f * s);
const int titleMarginH = juce::roundToInt (18.0f * s);
const int titleMarginV = juce::roundToInt (4.0f * s);
const int subtitleMarginH = juce::roundToInt (18.0f * s);
const int subtitleMarginV = juce::roundToInt (2.0f * s);
const int rowsMarginH = juce::roundToInt (16.0f * s);
const int rowsMarginV = juce::roundToInt (4.0f * s);
const int gap = juce::roundToInt (20.0f * s);
2026-08-10 12:55:02 +02:00
footerArea = area.removeFromBottom (footerH);
footerLabel.setBounds (footerArea.reduced (rowsMarginH, 0));
footerLabel.setFont (juce::Font (juce::FontOptions (juce::roundToInt (9.0f * s))));
footerLabel.setColour (juce::Label::textColourId, juce::Colour (0xFF5E8688));
auto header = area.removeFromTop (headerH);
const int comboW = presetBoxW + boxGap + scaleBoxW;
auto comboArea = header.removeFromRight (comboW).reduced (0, presetBoxMargin);
auto scaleArea = comboArea.removeFromLeft (scaleBoxW);
scaleBox.setBounds (scaleArea);
presetBox.setBounds (comboArea);
auto titleArea = header.removeFromTop (titleH).reduced (titleMarginH, titleMarginV).translated (0, juce::roundToInt (10.0f * s));
2026-08-10 12:55:02 +02:00
titleLabel.setBounds (titleArea);
subtitleLabel.setBounds (header.reduced (subtitleMarginH, subtitleMarginV));
titleLabel.setFont (juce::Font (juce::FontOptions (juce::roundToInt (22.0f * s), juce::Font::bold)));
subtitleLabel.setFont (juce::Font (juce::FontOptions (juce::roundToInt (11.0f * s))));
auto modules = area.reduced (rowsMarginH, rowsMarginV);
modules.removeFromBottom (juce::roundToInt (15.0f * s));
2026-08-10 12:55:02 +02:00
const int panelH = (modules.getHeight() - 2 * gap) / 3;
auto displayPanel = modules.removeFromTop (panelH);
display.setBounds (displayPanel);
modules.removeFromTop (gap);
knobRow1Area = modules.removeFromTop (panelH);
modules.removeFromTop (gap);
knobRow2Area = modules.removeFromTop (panelH);
2026-08-10 12:55:02 +02:00
layoutKnobRow (knobRow1Area, 0, 5);
layoutKnobRow (knobRow2Area, 5, 4);
}
void HorizontAudioProcessorEditor::paint (juce::Graphics& g)
{
const auto bounds = getLocalBounds();
g.setGradientFill (juce::ColourGradient (juce::Colour (0xFF0E2A2E), { 0.0f, 0.0f },
juce::Colour (0xFF060B0D), { 0.0f, (float) bounds.getHeight() },
false));
g.fillRect (bounds);
g.setColour (juce::Colour (0x1AFFFFFF));
g.fillRect (0, 0, bounds.getWidth(), 1);
g.setColour (juce::Colour (0x14000000));
g.fillRect (0, bounds.getHeight() - 1, bounds.getWidth(), 1);
if (footerArea.getHeight() > 0)
{
g.setColour (juce::Colour (0x1AFFFFFF));
g.fillRect (0, footerArea.getY(), bounds.getWidth(), 1);
g.setColour (juce::Colour (0x14000000));
g.fillRect (0, footerArea.getY() + 1, bounds.getWidth(), 1);
}
auto drawModule = [&g] (juce::Rectangle<int> moduleArea)
{
const auto r = moduleArea.toFloat();
auto shRect = r.expanded (4.0f, 3.0f).translated (0.0f, 3.0f);
auto shadowGrad = juce::ColourGradient (juce::Colour (0x00000000), { 0.0f, shRect.getY() },
juce::Colour (0x00000000), { 0.0f, shRect.getBottom() },
false);
shadowGrad.addColour (0.45f, juce::Colour (0x70000000));
g.setGradientFill (shadowGrad);
g.fillRoundedRectangle (shRect, 13.0f);
g.setGradientFill (juce::ColourGradient (juce::Colour (0x0024323A), { 0.0f, r.getY() },
juce::Colour (0x000B1216), { 0.0f, r.getBottom() },
2026-08-10 12:55:02 +02:00
false));
g.fillRoundedRectangle (r, 12.0f);
g.setColour (juce::Colour (0x2A3CE0C8));
g.drawRoundedRectangle (r.expanded (1.0f), 13.0f, 1.0f);
g.setColour (juce::Colour (0x38FFFFFF));
g.drawHorizontalLine ((int) r.getY() + 1, (int) r.getX() + 3, (int) r.getRight() - 3);
g.drawVerticalLine ((int) r.getX() + 1, (int) r.getY() + 3, (int) r.getBottom() - 3);
g.setColour (juce::Colour (0x50000000));
g.drawHorizontalLine ((int) r.getBottom() - 1, (int) r.getX() + 3, (int) r.getRight() - 3);
g.drawVerticalLine ((int) r.getRight() - 1, (int) r.getY() + 3, (int) r.getBottom() - 3);
g.saveState();
g.reduceClipRegion (r.toNearestInt().reduced (1));
auto refl = juce::ColourGradient (juce::Colour (0x00FFFFFF), { 0.0f, r.getY() },
2026-08-10 12:55:02 +02:00
juce::Colour (0x00FFFFFF), { 0.0f, r.getY() + r.getHeight() * 0.45f },
false);
g.setGradientFill (refl);
g.fillRect (r);
g.setColour (juce::Colour (0x45FFFFFF));
g.fillRect (r.getX() + 2.0f, r.getY() + 1.0f, r.getWidth() - 4.0f, 1.0f);
g.restoreState();
};
drawModule (knobRow1Area);
drawModule (knobRow2Area);
}