chore: land janitor round 1 and graft integration

Round 1 findings and fixes, all previously uncommitted:
- FXProcessor per-slot DSP history repair (HEAD does not compile without it)
- SynthVoice::getModulationValue, editor control-lock guard
- ExactCompare.h naming the deliberate exact float comparisons
- KnobLookAndFeel replacing the never-installed SerumLookAndFeel
- 55 first-party compiler warnings to 0
- AGENT.md renamed to AGENTS.md across Source/
- graft repository context graph and its hooks
- JANITOR_TASKS.md and UI_UX_AUDIT.md records
This commit is contained in:
biggy committed 2026-10-06 08:50:45 +02:00
1 parent 5c942cad0c
commit e4cea300db
63 files changed
+2360 -805

No files matched your search

+1 -3
View File
@@ -3,7 +3,6 @@
## Owned files
- Related editor implementation: `../PluginEditor.h`, `../PluginEditor.cpp`
- SerumLookAndFeel.h
- Knob.h, Knob.cpp
- Slider.h, Slider.cpp
- ToggleButton.h, ToggleButton.cpp
@@ -16,7 +15,6 @@
## Rules
- Draw all controls through `SerumLookAndFeel`.
- Use `theme::` colours from Resources.h instead of hardcoded colour values.
- Own GUI attachments with `std::unique_ptr`.
- Own component children by value or with `std::unique_ptr`; `addAndMakeVisible` only attaches and shows them, and does not manage their lifetime.
@@ -44,4 +42,4 @@ const float v = processor.parameters.getRawParameterValue (ids::f1Cutoff)
: 0.0f;
```
This file overrides /AGENT.md where they conflict.
This file overrides /AGENTS.md where they conflict.
+6 -3
View File
@@ -1,11 +1,14 @@
#include "EnvelopeDisplay.h"
#include "../Envelope.h"
#include "../ExactCompare.h"
namespace serum
{
void EnvelopeDisplay::setParams (float a, float d, float s, float r, float c)
{
if (attack == a && decay == d && sustain == s && release == r && curve == c)
if (! changed (attack, a) && ! changed (decay, d) && ! changed (sustain, s)
&& ! changed (release, r) && ! changed (curve, c))
return;
attack = a; decay = d; sustain = s; release = r; curve = c;
repaint();
@@ -19,8 +22,8 @@ void EnvelopeDisplay::paint (juce::Graphics& g)
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;
const float atkShape = attackShapeFor (curve);
const float decShape = decayShapeFor (curve);
// Normalise durations for display, allowing a short sustain plateau.
const float aSec = maps::toSeconds (attack);
+8 -1
View File
@@ -1,8 +1,12 @@
#include "FilterDisplay.h"
#include "../ExactCompare.h"
namespace serum
{
// Schematic frequency-response preview for this view only. It does not model the
// real DSP in Source/Filter.cpp, nor the drive, the slope or modulation, so it is
// not expected to match the nonlinear filter numerically.
float FilterDisplay::magnitude (float freqHz, float cutoffHz, float res, int type)
{
const float w = (cutoffHz > 1.0f) ? freqHz / cutoffHz : 1.0f;
@@ -29,7 +33,9 @@ float FilterDisplay::magnitude (float freqHz, float cutoffHz, float res, int typ
}
return m / 3.0f;
}
case FilterModel::LadderLP:
case FilterModel::Diode:
case FilterModel::Count:
default:
return (denom > 1e-6f) ? 1.0f / denom : 0.0f;
}
@@ -37,7 +43,8 @@ float FilterDisplay::magnitude (float freqHz, float cutoffHz, float res, int typ
void FilterDisplay::setParams (int t, float c, float r, float d, int s)
{
if (type == t && cutoff == c && res == r && drive == d && slope == s)
if (type == t && ! changed (cutoff, c) && ! changed (res, r)
&& ! changed (drive, d) && slope == s)
return;
type = t; cutoff = c; res = r; drive = d; slope = s;
repaint();
+75 -7
View File
@@ -3,6 +3,17 @@
namespace serum
{
namespace
{
// One shared look-and-feel for every knob. It outlives every Knob because
// it is a function-local static and the GUI owns the knobs.
KnobLookAndFeel& knobStyle()
{
static KnobLookAndFeel style;
return style;
}
}
Knob::Knob (const juce::String& name, std::function<juce::String (float)> fmt)
: formatter (std::move (fmt))
{
@@ -12,6 +23,10 @@ Knob::Knob (const juce::String& name, std::function<juce::String (float)> fmt)
setDoubleClickReturnValue (true, 0.5);
setName (name);
setComponentID (name);
// Draw the dial and the name strip through the theme, not through the
// shared default look-and-feel.
setLookAndFeel (&knobStyle());
}
juce::String Knob::getTextFromValue (double value)
@@ -21,6 +36,32 @@ juce::String Knob::getTextFromValue (double value)
return formatPercent ((float) value);
}
// The dial takes the top of the footprint and the name takes a strip at the
// bottom, so the name is always visible without a tooltip hover.
void Knob::resized()
{
juce::Slider::resized();
textArea = getLocalBounds().removeFromBottom (
juce::jmin (KnobLookAndFeel::labelHeight, getHeight() / 2));
}
void Knob::paint (juce::Graphics& g)
{
juce::Slider::paint (g);
if (textArea.isEmpty())
return;
// Dim while disabled, bright while hovered, otherwise the normal label
// colour, so the three states are distinguishable without colour alone.
const auto colour = isEnabled() ? (isMouseOverOrDragging() ? theme::text : theme::textDim)
: theme::outline;
g.setColour (colour);
g.setFont (juce::Font (11.0f));
g.drawText (getName(), textArea, juce::Justification::centred, false);
}
void Knob::mouseDown (const juce::MouseEvent& e)
{
juce::Slider::mouseDown (e);
@@ -56,6 +97,24 @@ void Knob::showValuePopup (const juce::MouseEvent& e)
if (topLevel == nullptr)
return;
constexpr int baseW = 84;
constexpr int baseH = 24;
constexpr int gap = 10;
// The interface scale is applied to a parent component, not to this slider.
// Measure it from the local-space distance between two neighbouring points,
// so a scaled interface does not leave the readout at a fixed size.
float scale = 1.0f;
if (auto* parent = getParentComponent())
{
const auto origin = getLocalPoint (parent, juce::Point<int> (0, 0)).toFloat();
const auto unitX = getLocalPoint (parent, juce::Point<int> (1, 0)).toFloat();
scale = origin.getDistanceFrom (unitX);
}
scale = juce::jlimit (0.5f, 2.5f, scale);
const int w = juce::roundToInt (baseW * scale);
const int h = juce::roundToInt (baseH * scale);
if (valuePopup == nullptr)
{
valuePopup = std::make_unique<juce::Label>();
@@ -63,19 +122,20 @@ void Knob::showValuePopup (const juce::MouseEvent& e)
valuePopup->setInterceptsMouseClicks (false, false);
valuePopup->setColour (juce::Label::backgroundColourId, juce::Colours::transparentBlack);
valuePopup->setColour (juce::Label::textColourId, theme::text);
valuePopup->setFont (juce::Font (14.0f, juce::Font::bold));
valuePopup->setJustificationType (juce::Justification::centred);
topLevel->addAndMakeVisible (valuePopup.get());
}
valuePopup->setFont (juce::Font (14.0f * scale, juce::Font::bold));
valuePopup->setText (getTextFromValue (getValue()), juce::dontSendNotification);
constexpr int w = 84;
constexpr int h = 24;
constexpr int gap = 10;
const auto cursor = topLevel->getLocalPoint (nullptr, e.getScreenPosition());
valuePopup->setBounds (cursor.getX() - w / 2, cursor.getY() - h - gap, w, h);
// Keep the readout inside the window so a knob near an edge still shows it.
const auto popupPos = topLevel->getLocalPoint (nullptr, e.getScreenPosition());
valuePopup->setBounds (juce::jlimit (0, juce::jmax (0, topLevel->getWidth() - w),
popupPos.getX() - w / 2),
juce::jlimit (0, juce::jmax (0, topLevel->getHeight() - h),
popupPos.getY() - h - gap),
w, h);
}
void Knob::hideValuePopup()
@@ -83,4 +143,12 @@ void Knob::hideValuePopup()
valuePopup.reset();
}
// A section switch can hide the view holding this knob mid-drag. The popup is a
// sibling of that view, so it has to be taken down here or it stays on screen.
void Knob::visibilityChanged()
{
if (! isVisible())
hideValuePopup();
}
} // namespace serum
+14 -2
View File
@@ -3,13 +3,16 @@
#include <JuceHeader.h>
#include <memory>
#include "../Resources.h"
#include "KnobLookAndFeel.h"
namespace serum
{
// ===========================================================================
// Rotary knob — a juce::Slider styled by SerumLookAndFeel. The value readout is
// formatted by a user-provided function via getTextFromValue().
// Rotary knob — a juce::Slider drawn by KnobLookAndFeel. The knob paints its
// own name below the dial, so no control is a bare arc whose only hint is a
// tooltip. The value readout is formatted by a user-provided function via
// getTextFromValue().
//
// While the knob is being dragged it also shows a small floating label near the
// mouse cursor with the live (formatted) value, giving immediate feedback
@@ -23,8 +26,16 @@ public:
void setFormatter (std::function<juce::String (float)> f) { formatter = std::move (f); }
// Value a double click returns to. Call it with the parameter's own default
// so the gesture restores the real default, never a hardcoded 0.5.
void setDefaultValue (double v) { setDoubleClickReturnValue (true, v); }
juce::String getTextFromValue (double value) override;
void paint (juce::Graphics& g) override;
void resized() override;
void visibilityChanged() override;
void mouseDown (const juce::MouseEvent& e) override;
void mouseDrag (const juce::MouseEvent& e) override;
void mouseUp (const juce::MouseEvent& e) override;
@@ -36,6 +47,7 @@ private:
std::function<juce::String (float)> formatter;
std::unique_ptr<juce::Label> valuePopup;
juce::Rectangle<int> textArea; // name strip under the dial
};
} // namespace serum
+47
View File
@@ -0,0 +1,47 @@
#include "KnobLookAndFeel.h"
namespace serum
{
void KnobLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height,
float sliderPos, float rotaryStartAngle, float rotaryEndAngle,
juce::Slider& slider)
{
const auto dial = juce::Rectangle<float> ((float) x, (float) y, (float) width, (float) height);
if (dial.isEmpty())
return;
const float radius = juce::jmin (dial.getWidth(), dial.getHeight()) * 0.5f - 2.0f;
if (radius <= 1.0f)
return;
const float lineW = juce::jlimit (1.5f, 5.0f, radius * 0.22f);
const float arcR = radius - lineW * 0.5f;
const float centreX = dial.getCentreX();
const float centreY = dial.getCentreY();
const float toAngle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle);
const bool live = slider.isEnabled();
juce::Path track;
track.addCentredArc (centreX, centreY, arcR, arcR, 0.0f, rotaryStartAngle, rotaryEndAngle, true);
g.setColour (theme::outline);
g.strokePath (track, juce::PathStrokeType (lineW, juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
if (live)
{
juce::Path value;
value.addCentredArc (centreX, centreY, arcR, arcR, 0.0f, rotaryStartAngle, toAngle, true);
g.setColour (theme::accent.withAlpha (slider.isMouseOverOrDragging() ? 1.0f : 0.75f));
g.strokePath (value, juce::PathStrokeType (lineW, juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
}
const float pointer = radius * 0.62f;
g.setColour (live ? theme::text : theme::textDim);
g.drawLine (centreX + std::sin (toAngle) * pointer * 0.35f,
centreY - std::cos (toAngle) * pointer * 0.35f,
centreX + std::sin (toAngle) * pointer,
centreY - std::cos (toAngle) * pointer,
juce::jmax (1.5f, lineW * 0.5f));
}
} // namespace serum
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <JuceHeader.h>
#include "../Resources.h"
namespace serum
{
// ===========================================================================
// Look-and-feel for Knob.
//
// Two jobs:
//
// 1. Draw the rotary with theme:: colours. The shared default look-and-feel
// paints it with its own palette, which ignores the rest of the editor.
//
// 2. Leave room for the knob's name. juce::LookAndFeel_V4 reduces the rotary
// bounds by a fixed ten pixels on each side, so a 76 pixel knob loses
// 20 pixels of dial and a smaller knob loses most of it. This version
// keeps the dial whole inside its footprint and reserves a text strip
// under it, which is the strip Knob paints its name into.
// ===========================================================================
class KnobLookAndFeel : public juce::LookAndFeel_V4
{
public:
// Height of the name strip reserved under the dial.
static constexpr int labelHeight = 16;
void drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height,
float sliderPos, float rotaryStartAngle, float rotaryEndAngle,
juce::Slider& slider) override;
};
} // namespace serum
+29 -33
View File
@@ -1,52 +1,28 @@
#include "LFODisplay.h"
#include "../ExactCompare.h"
namespace serum
{
void LFODisplay::setShapeData (const std::vector<float>& data, int s)
{
const int newSteps = juce::jlimit (2, 64, s);
const size_t dataSize = std::min (data.size(), size_t (64));
if (steps == newSteps && shapeData.size() == 64
const int newSteps = juce::jlimit (2, LFO::kShapePoints, s);
const auto dataSize = (std::ptrdiff_t) std::min (data.size(), size_t (LFO::kShapePoints));
if (steps == newSteps && (int) shapeData.size() == LFO::kShapePoints
&& std::equal (data.begin(), data.begin() + dataSize, shapeData.begin())
&& std::all_of (shapeData.begin() + dataSize, shapeData.end(), [] (float v) { return v == 0.0f; }))
&& std::all_of (shapeData.begin() + dataSize, shapeData.end(), [] (float v) { return exactly (v, 0.0f); }))
return;
steps = newSteps;
shapeData.assign (data.begin(), data.begin() + dataSize);
shapeData.resize (64, 0.0f);
shapeData.resize ((size_t) LFO::kShapePoints, 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;
}
return lfoShapeValue (phase, (LfoShape) shape, steps,
shapeData.data(), (int) shapeData.size(), kSampleHoldLevel);
}
void LFODisplay::paint (juce::Graphics& g)
@@ -75,6 +51,26 @@ void LFODisplay::paint (juce::Graphics& g)
}
}
// Sample and hold is stateful in the audio path, so the preview draws the
// held level as a flat schematic with its hold steps marked, and labels it,
// rather than inventing a sequence the synth will never play.
if (shape == (int) LfoShape::SampleHold)
{
const float stepW = (b.getWidth() - 8.0f) / (float) steps;
g.setColour (theme::outline);
for (int i = 1; i < steps; ++i)
{
const float x = b.getX() + 4.0f + stepW * (float) i;
g.drawLine (x, midY - b.getHeight() * 0.28f, x, midY + b.getHeight() * 0.28f, 1.0f);
}
g.setColour (theme::textDim);
g.setFont (10.0f);
g.drawText ("S&H schematic",
juce::Rectangle<float> (b.getX(), b.getY(), b.getWidth(), 14.0f).toNearestInt(),
juce::Justification::centred, false);
}
juce::Path path;
path.startNewSubPath (b.getX() + 4.0f, midY);
const int n = 256;
@@ -99,7 +95,7 @@ void LFODisplay::editAt (float x, float y)
const float v = juce::jlimit (-1.0f, 1.0f, (midY - y) / (b.getHeight() * 0.42f));
if (shapeData.empty())
shapeData.assign (64, 0.0f);
shapeData.assign ((size_t) LFO::kShapePoints, 0.0f);
if (shape == (int) LfoShape::StepSeq)
{
+14 -1
View File
@@ -3,12 +3,17 @@
#include <JuceHeader.h>
#include "../Resources.h"
#include "../Params.h"
#include "../LFO.h"
namespace serum
{
// ===========================================================================
// LFO shape view. Step-sequencer and freehand shapes are click/drag editable.
// The preview evaluates the same lfoShapeValue() function as the audio path, so
// the drawn shape cannot drift from the sound. Sample and hold has no phase
// function (the audio level is stateful), so its preview is a labelled
// schematic rather than a fabricated sequence.
// ===========================================================================
class LFODisplay : public juce::Component
{
@@ -21,13 +26,21 @@ public:
void mouseDown (const juce::MouseEvent& e) override;
void mouseDrag (const juce::MouseEvent& e) override;
// Level the sample-and-hold preview draws. The audio LFO holds a level that
// the display cannot observe on the message thread, so the preview shows a
// flat schematic line at this value instead of inventing a sequence.
static constexpr float kSampleHoldLevel = 0.0f;
// Value of the current shape at `phase` (0..1); the audio path calls the
// same lfoShapeValue() with its live sample-and-hold level.
float valueAt (float phase) const noexcept;
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);
};
-156
View File
@@ -1,156 +0,0 @@
#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
+2 -2
View File
@@ -3,8 +3,8 @@
namespace serum
{
Slider::Slider (const juce::String& name, std::function<juce::String (float)> formatter)
: Knob (name, std::move (formatter))
Slider::Slider (const juce::String& name, std::function<juce::String (float)> fmt)
: Knob (name, std::move (fmt))
{
setSliderStyle (juce::Slider::LinearHorizontal);
setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0);
+13 -5
View File
@@ -33,21 +33,29 @@ void ToggleButton::paint (juce::Graphics& g)
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);
const bool live = isEnabled();
const bool hovered = live && isMouseOverOrDragging();
if (state)
{
g.setColour (onColour.withAlpha (0.35f));
// Hover brightens the halo, disabled dims it, so the three states are
// distinguishable without relying on colour alone.
const float halo = live ? (hovered ? 0.55f : 0.35f) : 0.15f;
g.setColour (onColour.withAlpha (halo));
g.fillEllipse (led.expanded (6.0f));
g.setColour (onColour);
g.setColour (live ? onColour : onColour.withAlpha (0.4f));
}
else
{
g.setColour (theme::outline);
g.setColour (hovered ? theme::textDim : theme::outline);
}
g.fillEllipse (led);
g.setColour (state ? juce::Colours::black : theme::textDim);
g.drawEllipse (led, 1.0f);
g.drawEllipse (led, live ? (hovered ? 1.6f : 1.0f) : 0.8f);
g.setColour (state ? theme::text : theme::textDim);
const auto labelColour = ! live ? theme::outline
: (state ? theme::text : (hovered ? theme::text : theme::textDim));
g.setColour (labelColour);
g.setFont (juce::Font (11.0f));
g.drawText (label, 0, (int) (led.getBottom() + 2), getWidth(), 14, juce::Justification::centred, false);
}
+2 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#include <JuceHeader.h>
#include "../ExactCompare.h"
#include "../Resources.h"
#include "../Wavetable.h"
@@ -15,7 +16,7 @@ class WaveformDisplay : public juce::Component
public:
void setWavetables (const WavetableLibrary* lib) { if (wtLib != lib) { wtLib = lib; repaint(); } }
void setWaveIndex (int index) { if (wave != index) { wave = index; repaint(); } }
void setFramePosition (float pos) { if (wtPos != pos) { wtPos = pos; repaint(); } }
void setFramePosition (float pos) { if (changed (wtPos, pos)) { wtPos = pos; repaint(); } }
void setEnabled (bool e) { if (enabled != e) { enabled = e; repaint(); } }
void paint (juce::Graphics& g) override;