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:
1 parent
5c942cad0c
commit
e4cea300db
63 files changed
+2360
-805
No files matched your search
@@ -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.
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user