feat(gui): add GUI components and displays

This commit is contained in:
2026-09-08 14:55:22 +02:00
parent 5a80534a2e
commit a9abca9e15
19 changed files with 898 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
#include "Display.h"
namespace serum
{
void Display::paint (juce::Graphics& g)
{
const auto b = getLocalBounds().toFloat();
g.setColour (juce::Colours::black.withAlpha (0.45f));
g.fillRoundedRectangle (b, 4.0f);
g.setColour (theme::outline);
g.drawRoundedRectangle (b.reduced (0.5f), 4.0f, 1.0f);
if (title.isNotEmpty())
{
g.setColour (theme::textDim);
g.setFont (juce::Font (10.0f));
g.drawText (title, 0, 3, getWidth(), 14, juce::Justification::centred, false);
}
g.setColour (theme::accent);
g.setFont (juce::Font (14.0f, juce::Font::bold));
g.drawText (value, 0, title.isNotEmpty() ? 16 : 0, getWidth(), getHeight() - (title.isNotEmpty() ? 16 : 0),
juce::Justification::centred, false);
}
} // namespace serum
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <JuceHeader.h>
#include "../Resources.h"
namespace serum
{
// ===========================================================================
// LED-style text readout (e.g. preset name / master level).
// ===========================================================================
class Display : public juce::Component
{
public:
void setTitle (const juce::String& t) { title = t; repaint(); }
void setValue (const juce::String& v) { value = v; repaint(); }
void paint (juce::Graphics& g) override;
private:
juce::String title, value;
};
} // namespace serum
+80
View File
@@ -0,0 +1,80 @@
#include "EnvelopeDisplay.h"
namespace serum
{
void EnvelopeDisplay::setParams (float a, float d, float s, float r, float c)
{
attack = a; decay = d; sustain = s; release = r; curve = c;
}
void EnvelopeDisplay::paint (juce::Graphics& g)
{
const auto b = getLocalBounds().toFloat().reduced (4.0f);
g.setColour (juce::Colours::black.withAlpha (0.4f));
g.fillRoundedRectangle (b.expanded (4.0f), 4.0f);
g.setColour (theme::outline);
g.drawRoundedRectangle (b.expanded (4.0f).reduced (0.5f), 4.0f, 1.0f);
const float atkShape = 0.3f + curve * 2.7f;
const float decShape = 3.0f - curve * 2.7f;
// Normalise durations for display (attack 0..1, decay 0..0.6, release 0..0.6).
const float aSec = maps::toSeconds (attack);
const float dSec = maps::toSeconds (decay);
const float rSec = maps::toSeconds (release);
const float total = aSec + dSec + rSec + 0.01f;
const float ax = (aSec / total);
const float dx = (dSec / total);
const float rx = (rSec / total);
const float left = b.getX();
const float right = b.getRight();
const float bottom = b.getBottom();
const float top = b.getY();
const float sustainY = bottom - sustain * b.getHeight();
juce::Path path;
path.startNewSubPath (left, bottom);
path.lineTo (left, top);
// Attack (curve-shaped).
const int steps = 48;
const float peakX = left + ax * b.getWidth();
for (int i = 0; i <= steps; ++i)
{
const float p = (float) i / steps;
const float y = top + (bottom - top) * std::pow (p, atkShape);
path.lineTo (left + p * (peakX - left), y);
}
// Decay to sustain.
const float decX = peakX + dx * b.getWidth();
for (int i = 0; i <= steps; ++i)
{
const float p = (float) i / steps;
const float y = sustainY + (bottom - sustainY) * std::pow (1.0f - p, decShape);
path.lineTo (peakX + p * (decX - peakX), y);
}
path.lineTo (decX, sustainY);
path.lineTo (right - rx * b.getWidth(), sustainY);
// Release.
const float relStartX = right - rx * b.getWidth();
for (int i = 0; i <= steps; ++i)
{
const float p = (float) i / steps;
const float y = sustainY + (bottom - sustainY) * std::pow (1.0f - p, decShape);
path.lineTo (relStartX + p * (right - relStartX), y);
}
g.setColour (theme::accent2.withAlpha (0.3f));
juce::Path fill = path;
fill.lineTo (right, bottom);
fill.closeSubPath();
g.fillPath (fill);
g.setColour (theme::accent2);
g.strokePath (path, juce::PathStrokeType (1.5f));
}
} // namespace serum
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <JuceHeader.h>
#include "../Resources.h"
#include "../Params.h"
namespace serum
{
// ===========================================================================
// ADSR curve preview.
// ===========================================================================
class EnvelopeDisplay : public juce::Component
{
public:
void setParams (float attack, float decay, float sustain, float release, float curve);
void paint (juce::Graphics& g) override;
private:
float attack = 0.05f, decay = 0.25f, sustain = 0.7f, release = 0.3f, curve = 0.5f;
};
} // namespace serum
+79
View File
@@ -0,0 +1,79 @@
#include "FilterDisplay.h"
namespace serum
{
float FilterDisplay::magnitude (float freqHz, float cutoffHz, float res, int type)
{
const float w = (cutoffHz > 1.0f) ? freqHz / cutoffHz : 1.0f;
const float q = 0.6f + res * 14.0f;
const float w2 = w * w;
const float denom = std::sqrt ((1.0f - w2) * (1.0f - w2) + (w / q) * (w / q));
switch ((FilterModel) type)
{
case FilterModel::LadderHP: return (denom > 1e-6f) ? w2 / denom : 0.0f;
case FilterModel::LadderBP: return (denom > 1e-6f) ? (w / q) / denom : 0.0f;
case FilterModel::Screamer: return (denom > 1e-6f) ? (w / (q * 0.7f)) / denom : 0.0f;
case FilterModel::Comb:
return 0.5f + 0.5f * std::cos (2.0f * juce::MathConstants<float>::pi * freqHz / cutoffHz);
case FilterModel::Formant:
{
float m = 0.0f;
const float fc[3] = { cutoffHz * 0.5f, cutoffHz * 1.6f, cutoffHz * 3.2f };
for (int i = 0; i < 3; ++i)
{
const float ww = freqHz / fc[i];
const float dd = std::sqrt ((1.0f - ww * ww) * (1.0f - ww * ww) + (ww / q) * (ww / q));
m += (ww / q) / (dd + 1e-6f);
}
return m / 3.0f;
}
case FilterModel::Diode:
default:
return (denom > 1e-6f) ? 1.0f / denom : 0.0f;
}
}
void FilterDisplay::setParams (int t, float c, float r, float d, int s)
{
type = t; cutoff = c; res = r; drive = d; slope = s;
}
void FilterDisplay::paint (juce::Graphics& g)
{
const auto b = getLocalBounds().toFloat();
g.setColour (juce::Colours::black.withAlpha (0.4f));
g.fillRoundedRectangle (b, 4.0f);
g.setColour (theme::outline);
g.drawRoundedRectangle (b.reduced (0.5f), 4.0f, 1.0f);
if (! enabled)
return;
const float cutoffHz = maps::cutoffToHz (cutoff);
const int n = 300;
juce::Path path;
path.startNewSubPath (b.getX() + 4.0f, b.getBottom() - 4.0f);
for (int i = 0; i <= n; ++i)
{
const float logF = std::log10 (20.0f) + ((float) i / (float) n) * (std::log10 (20000.0f) - std::log10 (20.0f));
const float freq = std::pow (10.0f, logF);
const float mag = magnitude (freq, cutoffHz, res, type);
const float db = juce::Decibels::gainToDecibels (mag + 1e-5f);
const float x = b.getX() + 4.0f + ((float) i / (float) n) * (b.getWidth() - 8.0f);
const float y = juce::jmap (db, -30.0f, 12.0f, b.getBottom() - 4.0f, b.getY() + 4.0f);
path.lineTo (x, y);
}
path.lineTo (b.getRight() - 4.0f, b.getBottom() - 4.0f);
path.closeSubPath();
g.setColour (theme::accent.withAlpha (0.25f));
g.fillPath (path);
g.setColour (theme::accent);
g.strokePath (path, juce::PathStrokeType (1.5f));
}
} // namespace serum
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <JuceHeader.h>
#include "../Resources.h"
#include "../Params.h"
namespace serum
{
// ===========================================================================
// Filter frequency-response view (magnitude vs log frequency).
// ===========================================================================
class FilterDisplay : public juce::Component
{
public:
void setParams (int type, float cutoff, float res, float drive, int slope);
void setEnabled (bool e) { enabled = e; }
void paint (juce::Graphics& g) override;
static float magnitude (float freqHz, float cutoffHz, float res, int type);
private:
int type = 0;
float cutoff = 0.5f, res = 0.0f, drive = 0.0f;
int slope = 2;
bool enabled = true;
};
} // namespace serum
+24
View File
@@ -0,0 +1,24 @@
#include "Knob.h"
namespace serum
{
Knob::Knob (const juce::String& name, std::function<juce::String (float)> fmt)
: formatter (std::move (fmt))
{
setSliderStyle (juce::Slider::RotaryVerticalDrag);
setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0);
setRange (0.0, 1.0);
setDoubleClickReturnValue (true, 0.5);
setName (name);
setComponentID (name);
}
juce::String Knob::getTextFromValue (double value)
{
if (formatter)
return formatter ((float) value);
return formatPercent ((float) value);
}
} // namespace serum
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <JuceHeader.h>
#include "../Resources.h"
namespace serum
{
// ===========================================================================
// Rotary knob — a juce::Slider styled by SerumLookAndFeel. The value readout is
// formatted by a user-provided function via getTextFromValue().
// ===========================================================================
class Knob : public juce::Slider
{
public:
explicit Knob (const juce::String& name,
std::function<juce::String (float)> formatter = {});
void setFormatter (std::function<juce::String (float)> f) { formatter = std::move (f); }
juce::String getTextFromValue (double value) override;
private:
std::function<juce::String (float)> formatter;
};
} // namespace serum
+128
View File
@@ -0,0 +1,128 @@
#include "LFODisplay.h"
namespace serum
{
void LFODisplay::setShapeData (const std::vector<float>& data, int s)
{
steps = juce::jlimit (2, 64, s);
shapeData = data;
if ((int) shapeData.size() < 64)
shapeData.resize (64, 0.0f);
repaint();
}
float LFODisplay::valueAt (float phase) const noexcept
{
switch ((LfoShape) shape)
{
case LfoShape::Sine: return std::sin (phase * 6.2831853f);
case LfoShape::Triangle: return 1.0f - 4.0f * std::abs (phase - 0.5f);
case LfoShape::Saw: return 2.0f * phase - 1.0f;
case LfoShape::Square: return (phase < 0.5f) ? 1.0f : -1.0f;
case LfoShape::SampleHold:
{
const int step = juce::jlimit (0, 15, (int) (phase * 16.0f));
return ((step * 2654435761u) & 0xffffu) / 32768.0f - 1.0f;
}
case LfoShape::StepSeq:
{
const int idx = juce::jlimit (0, steps - 1, (int) (phase * steps));
return shapeData.empty() ? 0.0f : shapeData[(size_t) idx];
}
case LfoShape::Freehand:
{
if (shapeData.empty()) return 0.0f;
const float pos = phase * (float) (steps - 1);
const int i0 = (int) pos;
const int i1 = juce::jmin (i0 + 1, steps - 1);
const float frac = pos - (float) i0;
return shapeData[(size_t) i0] * (1.0f - frac) + shapeData[(size_t) i1] * frac;
}
default: return 0.0f;
}
}
void LFODisplay::paint (juce::Graphics& g)
{
const auto b = getLocalBounds().toFloat();
g.setColour (juce::Colours::black.withAlpha (0.4f));
g.fillRoundedRectangle (b, 4.0f);
g.setColour (theme::outline);
g.drawRoundedRectangle (b.reduced (0.5f), 4.0f, 1.0f);
const float midY = b.getCentreY();
g.setColour (theme::outline);
g.drawLine (b.getX() + 4.0f, midY, b.getRight() - 4.0f, midY, 1.0f);
// Editable shapes show step markers.
if (shape == (int) LfoShape::StepSeq)
{
const float stepW = (b.getWidth() - 8.0f) / (float) steps;
for (int i = 0; i < steps; ++i)
{
const float v = shapeData.empty() ? 0.0f : shapeData[(size_t) i];
const float x = b.getX() + 4.0f + stepW * i;
const float y = midY - v * (b.getHeight() * 0.42f);
g.setColour (theme::amber);
g.fillRect (x + 1.0f, juce::jmin (y, midY), stepW - 2.0f, std::abs (midY - y));
}
}
juce::Path path;
path.startNewSubPath (b.getX() + 4.0f, midY);
const int n = 256;
for (int i = 0; i <= n; ++i)
{
const float phase = (float) i / (float) n;
const float v = valueAt (phase);
const float x = b.getX() + 4.0f + phase * (b.getWidth() - 8.0f);
const float y = midY - v * (b.getHeight() * 0.42f);
if (i == 0) path.startNewSubPath (x, y);
else path.lineTo (x, y);
}
g.setColour (theme::green);
g.strokePath (path, juce::PathStrokeType (1.5f));
}
void LFODisplay::editAt (float x, float y)
{
const auto b = getLocalBounds().toFloat();
const float midY = b.getCentreY();
const float normX = juce::jlimit (0.0f, 1.0f, (x - b.getX() - 4.0f) / (b.getWidth() - 8.0f));
const float v = juce::jlimit (-1.0f, 1.0f, (midY - y) / (b.getHeight() * 0.42f));
if (shapeData.empty())
shapeData.assign (64, 0.0f);
if (shape == (int) LfoShape::StepSeq)
{
const int idx = juce::jlimit (0, steps - 1, (int) (normX * steps));
shapeData[(size_t) idx] = v;
}
else if (shape == (int) LfoShape::Freehand)
{
const int idx = juce::jlimit (0, steps - 1, (int) (normX * steps));
shapeData[(size_t) idx] = v;
}
else
{
return;
}
if (onEdited)
onEdited (shapeData, steps);
repaint();
}
void LFODisplay::mouseDown (const juce::MouseEvent& e)
{
editAt (e.x, e.y);
}
void LFODisplay::mouseDrag (const juce::MouseEvent& e)
{
editAt (e.x, e.y);
}
} // namespace serum
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <JuceHeader.h>
#include "../Resources.h"
#include "../Params.h"
namespace serum
{
// ===========================================================================
// LFO shape view. Step-sequencer and freehand shapes are click/drag editable.
// ===========================================================================
class LFODisplay : public juce::Component
{
public:
void setShape (int s) { shape = s; repaint(); }
void setShapeData (const std::vector<float>& data, int steps);
void setOnShapeEdited (std::function<void (const std::vector<float>&, int)> cb) { onEdited = std::move (cb); }
void paint (juce::Graphics& g) override;
void mouseDown (const juce::MouseEvent& e) override;
void mouseDrag (const juce::MouseEvent& e) override;
private:
int shape = 0;
std::vector<float> shapeData;
int steps = 16;
std::function<void (const std::vector<float>&, int)> onEdited;
float valueAt (float phase) const noexcept;
void editAt (float x, float y);
};
} // namespace serum
+26
View File
@@ -0,0 +1,26 @@
#include "Panel.h"
namespace serum
{
Panel::Panel (const juce::String& t) : title (t)
{
}
void Panel::paint (juce::Graphics& g)
{
const auto b = getLocalBounds().toFloat();
g.setColour (theme::panel);
g.fillRoundedRectangle (b, 8.0f);
g.setColour (theme::outline);
g.drawRoundedRectangle (b.reduced (0.5f), 8.0f, 1.0f);
if (title.isNotEmpty())
{
g.setColour (theme::textDim);
g.setFont (juce::Font (11.0f, juce::Font::bold));
g.drawText (title, 10, 6, getWidth() - 20, 16, juce::Justification::left, false);
}
}
} // namespace serum
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <JuceHeader.h>
#include "../Resources.h"
namespace serum
{
// ===========================================================================
// Rounded dark panel with an optional title bar.
// ===========================================================================
class Panel : public juce::Component
{
public:
explicit Panel (const juce::String& title = {});
void paint (juce::Graphics& g) override;
private:
juce::String title;
};
} // namespace serum
+156
View File
@@ -0,0 +1,156 @@
#pragma once
#include <JuceHeader.h>
#include "../Resources.h"
namespace serum
{
// ===========================================================================
// Dark vector look-and-feel. Rotary and linear sliders are drawn with juce::Path
// (arcs + indicator + thumb) so they stay crisp at every UI scale.
// ===========================================================================
class SerumLookAndFeel : public juce::LookAndFeel_V4
{
public:
SerumLookAndFeel()
{
setColour (juce::Slider::backgroundColourId, theme::panel);
setColour (juce::Slider::thumbColourId, theme::accent);
setColour (juce::Slider::trackColourId, theme::outline);
setColour (juce::Slider::rotarySliderFillColourId, theme::accent);
setColour (juce::Slider::rotarySliderOutlineColourId, theme::panelRaised);
setColour (juce::Slider::textBoxTextColourId, theme::text);
setColour (juce::Slider::textBoxBackgroundColourId, theme::panel);
setColour (juce::Slider::textBoxHighlightColourId, theme::accent);
setColour (juce::Slider::textBoxOutlineColourId, theme::outline);
setColour (juce::ComboBox::backgroundColourId, theme::panelRaised);
setColour (juce::ComboBox::textColourId, theme::text);
setColour (juce::ComboBox::arrowColourId, theme::textDim);
setColour (juce::ComboBox::outlineColourId, theme::outline);
setColour (juce::ComboBox::buttonColourId, theme::panelRaised);
setColour (juce::ComboBox::focusedOutlineColourId, theme::accent);
setColour (juce::PopupMenu::backgroundColourId, theme::panelRaised);
setColour (juce::PopupMenu::textColourId, theme::text);
setColour (juce::PopupMenu::highlightedBackgroundColourId, theme::accent);
setColour (juce::PopupMenu::highlightedTextColourId, juce::Colours::black);
setColour (juce::TextButton::buttonColourId, theme::panelRaised);
setColour (juce::TextButton::buttonOnColourId, theme::accent);
setColour (juce::TextButton::textColourOffId, theme::textDim);
setColour (juce::TextButton::textColourOnId, juce::Colours::black);
setColour (juce::TextEditor::backgroundColourId, theme::panel);
setColour (juce::TextEditor::textColourId, theme::text);
setColour (juce::TextEditor::outlineColourId, theme::outline);
setColour (juce::TextEditor::focusedOutlineColourId, theme::accent);
}
void drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height,
float sliderPos, float, float, juce::Slider& slider) override
{
const int labelH = 18;
const int knobH = height - labelH;
const int knobSize = juce::jmin (width, knobH) - 6;
const int cx = x + width / 2;
const int cy = y + knobH / 2;
const float radius = knobSize * 0.5f;
const juce::Rectangle<float> area ((float) cx - radius, (float) cy - radius,
radius * 2.0f, radius * 2.0f);
// Body.
g.setColour (theme::panelRaised);
g.fillEllipse (area);
g.setColour (theme::outline);
g.drawEllipse (area, 1.0f);
const float startAngle = juce::MathConstants<float>::pi * 1.25f; // 225 deg
const float endAngle = juce::MathConstants<float>::pi * -0.25f;
const float valueAngle = startAngle + sliderPos * (endAngle - startAngle);
// Track arc.
juce::Path track;
track.addCentredArc ((float) cx, (float) cy, radius - 4.0f, radius - 4.0f,
0.0f, startAngle, endAngle, true);
g.setColour (theme::outline);
g.strokePath (track, juce::PathStrokeType (3.0f, juce::PathStrokeType::curved));
// Value arc.
juce::Path valueArc;
valueArc.addCentredArc ((float) cx, (float) cy, radius - 4.0f, radius - 4.0f,
0.0f, startAngle, valueAngle, true);
g.setColour (theme::accent);
g.strokePath (valueArc, juce::PathStrokeType (3.0f, juce::PathStrokeType::curved));
// Indicator.
const float indX = (float) cx + (radius - 9.0f) * std::cos (valueAngle);
const float indY = (float) cy + (radius - 9.0f) * std::sin (valueAngle);
g.setColour (theme::text);
g.drawLine ((float) cx, (float) cy, indX, indY, 2.0f);
g.setColour (theme::text);
g.fillEllipse (indX - 2.0f, indY - 2.0f, 4.0f, 4.0f);
// Value readout.
g.setColour (theme::text);
g.setFont (juce::Font (10.0f, juce::Font::bold));
g.drawText (slider.getTextFromValue (slider.getValue()),
juce::Rectangle<int> (x, y, width, knobH - (int) radius + 8),
juce::Justification::centred, false);
// Name label.
g.setColour (theme::textDim);
g.setFont (juce::Font (11.0f));
g.drawText (slider.getName(), juce::Rectangle<int> (x, y + knobH, width, labelH),
juce::Justification::centred, false);
}
void drawLinearSlider (juce::Graphics& g, int x, int y, int width, int height,
float sliderPos, float, float, juce::Slider::SliderStyle, juce::Slider& slider) override
{
const bool vertical = height > width;
juce::Rectangle<int> track;
if (vertical)
{
track = juce::Rectangle<int> (x + width / 2 - 3, y + 8, 6, height - 16);
}
else
{
track = juce::Rectangle<int> (x + 8, y + height / 2 - 3, width - 16, 6);
}
g.setColour (theme::outline);
g.fillRoundedRectangle (track.toFloat(), 3.0f);
juce::Rectangle<float> fill;
if (vertical)
{
const float h = track.getHeight() * sliderPos;
fill = juce::Rectangle<float> ((float) track.getX(), (float) track.getBottom() - h,
(float) track.getWidth(), h);
}
else
{
fill = juce::Rectangle<float> ((float) track.getX(), (float) track.getY(),
track.getWidth() * sliderPos, (float) track.getHeight());
}
g.setColour (theme::accent);
g.fillRoundedRectangle (fill, 3.0f);
// Thumb.
const float thumbC = vertical ? (track.getBottom() - track.getHeight() * sliderPos)
: (track.getX() + track.getWidth() * sliderPos);
juce::Point<float> thumb (vertical ? (float) track.getCentreX() : thumbC,
vertical ? thumbC : (float) track.getCentreY());
g.setColour (theme::text);
g.fillEllipse (thumb.x - 6.0f, thumb.y - 6.0f, 12.0f, 12.0f);
g.setColour (theme::textDim);
g.setFont (juce::Font (10.0f));
g.drawText (slider.getName(), x, y, width, height, juce::Justification::bottomLeft, false);
}
};
} // namespace serum
+13
View File
@@ -0,0 +1,13 @@
#include "Slider.h"
namespace serum
{
Slider::Slider (const juce::String& name, std::function<juce::String (float)> formatter)
: Knob (name, std::move (formatter))
{
setSliderStyle (juce::Slider::LinearHorizontal);
setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0);
}
} // namespace serum
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include "Knob.h"
namespace serum
{
// ===========================================================================
// Horizontal slider (same formatting machinery as Knob).
// ===========================================================================
class Slider : public Knob
{
public:
explicit Slider (const juce::String& name,
std::function<juce::String (float)> formatter = {});
};
} // namespace serum
+71
View File
@@ -0,0 +1,71 @@
#include "ToggleButton.h"
namespace serum
{
ToggleButton::ToggleButton (const juce::String& lbl) : label (lbl)
{
setRepaintsOnMouseActivity (true);
}
void ToggleButton::attach (juce::AudioProcessorValueTreeState& apvts, const juce::String& paramId)
{
param = apvts.getParameter (paramId);
if (param != nullptr)
{
state = param->getValue() > 0.5f;
attachment = std::make_unique<juce::ParameterAttachment> (*param,
[this] (float newValue) { setToggleState (newValue > 0.5f); });
}
}
void ToggleButton::setToggleState (bool s)
{
if (state == s)
return;
state = s;
repaint();
}
void ToggleButton::paint (juce::Graphics& g)
{
const auto bounds = getLocalBounds().toFloat();
const float ledSize = juce::jmin (bounds.getHeight() - 16.0f, 16.0f);
const juce::Rectangle<float> led ((bounds.getWidth() - ledSize) * 0.5f, 4.0f, ledSize, ledSize);
if (state)
{
g.setColour (onColour.withAlpha (0.35f));
g.fillEllipse (led.expanded (6.0f));
g.setColour (onColour);
}
else
{
g.setColour (theme::outline);
}
g.fillEllipse (led);
g.setColour (state ? juce::Colours::black : theme::textDim);
g.drawEllipse (led, 1.0f);
g.setColour (state ? theme::text : theme::textDim);
g.setFont (juce::Font (11.0f));
g.drawText (label, 0, (int) (led.getBottom() + 2), getWidth(), 14, juce::Justification::centred, false);
}
void ToggleButton::mouseDown (const juce::MouseEvent&)
{
const bool newState = ! state;
if (onClick)
{
onClick (newState);
}
else if (param != nullptr)
{
param->setValueNotifyingHost (newState ? 1.0f : 0.0f);
}
setToggleState (newState);
}
} // namespace serum
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <JuceHeader.h>
#include "../Resources.h"
namespace serum
{
// ===========================================================================
// LED-style toggle (power) button that drives a float parameter (0/1) or an
// optional callback (used by RAVE).
// ===========================================================================
class ToggleButton : public juce::Component
{
public:
explicit ToggleButton (const juce::String& label = {});
void attach (juce::AudioProcessorValueTreeState& apvts, const juce::String& paramId);
void setOnClick (std::function<void (bool)> cb) { onClick = std::move (cb); }
void setOnColour (juce::Colour c) { onColour = c; repaint(); }
void setLabel (const juce::String& lbl) { label = lbl; repaint(); }
void setToggleState (bool s);
bool getToggleState() const noexcept { return state; }
void paint (juce::Graphics& g) override;
void mouseDown (const juce::MouseEvent&) override;
private:
bool state = false;
juce::String label;
juce::RangedAudioParameter* param = nullptr;
std::unique_ptr<juce::ParameterAttachment> attachment;
std::function<void (bool)> onClick;
juce::Colour onColour = theme::accent;
};
} // namespace serum
+45
View File
@@ -0,0 +1,45 @@
#include "WaveformDisplay.h"
namespace serum
{
void WaveformDisplay::paint (juce::Graphics& g)
{
const auto b = getLocalBounds().toFloat();
g.setColour (juce::Colours::black.withAlpha (0.4f));
g.fillRoundedRectangle (b, 4.0f);
g.setColour (theme::outline);
g.drawRoundedRectangle (b.reduced (0.5f), 4.0f, 1.0f);
const float midY = b.getCentreY();
g.setColour (theme::outline);
g.drawLine (b.getX() + 4.0f, midY, b.getRight() - 4.0f, midY, 1.0f);
if (wtLib == nullptr || ! enabled)
return;
const Wavetable& wt = wtLib->getTable (wave);
if (! wt.isValid())
return;
const int n = 512;
juce::Path path;
path.startNewSubPath (b.getX() + 4.0f, midY);
for (int i = 0; i <= n; ++i)
{
const float phase = (float) i / (float) n;
const float sample = wt.readSafe (wtPos * 255.0f, phase);
const float x = b.getX() + 4.0f + ((float) i / (float) n) * (b.getWidth() - 8.0f);
const float y = midY - sample * (b.getHeight() * 0.42f);
if (i == 0)
path.startNewSubPath (x, y);
else
path.lineTo (x, y);
}
g.setColour (theme::accent);
g.strokePath (path, juce::PathStrokeType (1.5f));
}
} // namespace serum
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <JuceHeader.h>
#include "../Resources.h"
#include "../Wavetable.h"
namespace serum
{
// ===========================================================================
// Single-cycle wavetable view (morphs with the WT position knob).
// ===========================================================================
class WaveformDisplay : public juce::Component
{
public:
void setWavetables (const WavetableLibrary* lib) { wtLib = lib; }
void setWaveIndex (int index) { wave = index; }
void setFramePosition (float pos) { wtPos = pos; }
void setEnabled (bool e) { enabled = e; }
void paint (juce::Graphics& g) override;
private:
const WavetableLibrary* wtLib = nullptr;
int wave = 0;
float wtPos = 0.0f;
bool enabled = true;
};
} // namespace serum