rmx-19/Source/PluginEditor.cpp
2026-10-02 18:34:49 +02:00

354 lines
13 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "PluginEditor.h"
#include "BuildInfo.h"
#include <juce_gui_basics/juce_gui_basics.h>
RackEditor::RackEditor (RMX19AudioProcessor& p)
: AudioProcessorEditor (p)
, proc (p)
, display (p)
{
face = juce::Colour (0xff16130f);
backgroundImage = juce::ImageFileFormat::loadFrom (BinaryData::b3_png,
BinaryData::b3_pngSize);
// circular joystick ring: angles run clockwise from 12 o'clock
up = new DirWedge ("UP", 0.0);
right = new DirWedge ("RIGHT", juce::MathConstants<double>::halfPi);
down = new DirWedge ("DOWN", juce::MathConstants<double>::pi);
left = new DirWedge ("LEFT", -juce::MathConstants<double>::halfPi);
okB = new CentreOkButton ("OK");
escB = new EscButton ("ESC");
for (auto* b : { (juce::Button*) up, (juce::Button*) down,
(juce::Button*) left, (juce::Button*) right,
(juce::Button*) okB, (juce::Button*) escB })
addAndMakeVisible (b);
up->onClick = [this] { display.navUp(); };
down->onClick = [this] { display.navDown(); };
left->onClick = [this] { display.navLeft(); };
right->onClick = [this] { display.navRight(); };
okB->onClick = [this] { display.navEnter(); };
escB->onClick = [this] { display.navExit(); };
addAndMakeVisible (display);
scaleBox.addItem ("50%", 1);
scaleBox.addItem ("75%", 2);
scaleBox.addItem ("100%", 3);
scaleBox.addItem ("125%", 4);
scaleBox.addItem ("150%", 5);
scaleBox.addItem ("200%", 6);
scaleBox.addItem ("250%", 7);
scaleBox.addItem ("300%", 8);
scaleBox.setSelectedId (5, juce::dontSendNotification);
scaleBox.setJustificationType (juce::Justification::centred);
scaleBox.setTextWhenNothingSelected ("UI scale");
scaleBox.setColour (juce::ComboBox::backgroundColourId, juce::Colour (0xff221d18));
scaleBox.setColour (juce::ComboBox::outlineColourId, juce::Colour (0xff6b5f4e));
scaleBox.setColour (juce::ComboBox::textColourId, juce::Colour (0xffd8cdb9));
scaleBox.setColour (juce::ComboBox::arrowColourId, juce::Colour (0xffd8cdb9));
scaleBox.setColour (juce::PopupMenu::backgroundColourId, juce::Colour (0xff1c1813));
scaleBox.setColour (juce::PopupMenu::textColourId, juce::Colour (0xffd8cdb9));
scaleBox.setColour (juce::PopupMenu::highlightedBackgroundColourId, juce::Colour (0xff3a332b));
scaleBox.onChange = [this]
{
switch (scaleBox.getSelectedId())
{
case 1: setScale (0.5f); break;
case 2: setScale (0.75f); break;
case 3: setScale (1.0f); break;
case 4: setScale (1.25f); break;
case 5: setScale (1.5f); break;
case 6: setScale (2.0f); break;
case 7: setScale (2.5f); break;
case 8: setScale (3.0f); break;
default: break;
}
};
addAndMakeVisible (scaleBox);
setWantsKeyboardFocus (true);
setScale (1.5f);
startTimerHz (18);
}
RackEditor::~RackEditor()
{
stopTimer();
}
void RackEditor::paint (juce::Graphics& g)
{
// full-area backdrop so scaled edges stay covered
g.fillAll (juce::Colour (0xff0d0b09));
const auto b = juce::Rectangle<int> (0, 0, baseW, baseH);
// --- metal rack face (b3.png background, cover-scaled) ---
if (backgroundImage.isValid())
{
const float imageAspect = (float) backgroundImage.getWidth() / (float) backgroundImage.getHeight();
const float boxAspect = (float) b.getWidth() / (float) b.getHeight();
int sx = 0, sy = 0, sw = backgroundImage.getWidth(), sh = backgroundImage.getHeight();
if (imageAspect > boxAspect)
{
sw = (int) (backgroundImage.getHeight() * boxAspect);
sx = (backgroundImage.getWidth() - sw) / 2;
}
else
{
sh = (int) (backgroundImage.getWidth() / boxAspect);
sy = (backgroundImage.getHeight() - sh) / 2;
}
g.drawImage (backgroundImage, b.getX(), b.getY(),
juce::roundToInt (b.getWidth() * uiScale),
juce::roundToInt (b.getHeight() * uiScale),
sx, sy, sw, sh);
}
else
{
g.setColour (face);
g.fillRect (b);
}
g.setColour (juce::Colour (0xff08));
g.addTransform (juce::AffineTransform::scale (uiScale));
// brushed texture
g.setColour (juce::Colour::fromRGBA (255, 255, 255, 7));
for (int x = 0; x < b.getWidth(); x += 3)
g.fillRect (x, 0, 1, b.getHeight());
// rack rails (top/bottom)
g.setColour (juce::Colour (0xff221e1a));
g.fillRect (0, 0, b.getWidth(), 4);
g.fillRect (0, b.getHeight() - 4, b.getWidth(), 4);
g.setColour (juce::Colour (0xff5a5347));
g.fillRect (0, 4, b.getWidth(), 1);
g.fillRect (0, b.getHeight() - 5, b.getWidth(), 1);
// brand plate
g.setColour (juce::Colour (0xff614f3a));
g.fillRect (12, 6, 150, 12);
g.setColour (juce::Colour (0xffc9a86f));
g.setFont (juce::FontOptions ("Arial", 10.0f, juce::Font::bold));
g.drawFittedText ("RMX-19", 12, 6, 150, 12, juce::Justification::centred, 1);
// rear engraving text (kept clear of the UI-scale dropdown at the bottom right)
g.setColour (juce::Colour (0xff6e6657));
g.setFont (juce::FontOptions ("Courier New", 8.0f, juce::Font::plain));
const juce::String ser = "RMX-19 S/N 0001-1943-04-19 24BIT/96K SUPER SOUND MODULE BUILT "
+ juce::String (BUILD_DATE) + " " + juce::String (BUILD_TIME);
g.drawFittedText (ser, 12, b.getHeight() - 16, b.getWidth() - 128, 11, juce::Justification::centred, 1);
}
void RackEditor::resized()
{
// everything renders in base 656×248 space, scaled by an affine transform
const juce::AffineTransform tf = juce::AffineTransform::scale (uiScale);
// UI-scale dropdown now scales with the rest of the UI
scaleBox.setTransform (tf);
scaleBox.setBounds (baseW - 112, baseH - 30, 104, 20);
// LCD recessed module
const juce::Rectangle<int> lcdArea (14, 24, 356, 168);
display.setTransform (tf);
display.setBounds (lcdArea);
// circular joystick ring + separate ESC key, to the right of the LCD
const juce::Rectangle<int> navArea (lcdArea.getRight() + 16, 26, baseW - lcdArea.getRight() - 30, 164);
constexpr float ro = DirWedge::outerR;
const float ringCx = (float) navArea.getCentreX();
const float ringCy = (float) lcdArea.getCentreY(); // ring sits centred on the display
// all four wedges share the ring's bounding box; each paints only its own sector
const juce::Rectangle<int> ringBox ((int) (ringCx - ro), (int) (ringCy - ro),
(int) (ro * 2.0f), (int) (ro * 2.0f));
up->setTransform (tf); up->setBounds (ringBox);
right->setTransform (tf); right->setBounds (ringBox);
down->setTransform (tf); down->setBounds (ringBox);
left->setTransform (tf); left->setBounds (ringBox);
// OK, dead centre of the ring
okB->setTransform (tf);
okB->setBounds ((int) (ringCx - CentreOkButton::radius), (int) (ringCy - CentreOkButton::radius),
(int) (CentreOkButton::radius * 2.0f), (int) (CentreOkButton::radius * 2.0f));
// ESC, a round key centred on the point the old rectangular key occupied
constexpr float escD = EscButton::diameter;
const float escCx = (float) navArea.getX() + 54.0f;
const float escCy = (float) ringBox.getBottom() - 10.0f;
escB->setTransform (tf);
escB->setBounds ((int) (escCx - escD * 0.5f), (int) (escCy - escD * 0.5f),
(int) escD, (int) escD);
}
void RackEditor::setScale (float s)
{
uiScale = s;
setSize (juce::roundToInt (baseW * s), juce::roundToInt (baseH * s));
resized(); // ensure child layouts refresh at the new size
}
void RackEditor::timerCallback()
{
powerPulse = 0.5f + 0.5f * std::sin (juce::Time::getMillisecondCounter() * 0.0022f);
// only the pulsing power LED needs redraws; everything else is static
const int ledX = getWidth() - 20, ledY = 18;
repaint (ledX - 26, ledY - 7, 30, 14);
}
bool RackEditor::keyPressed (const juce::KeyPress& k)
{
if (display.keyTrap (k))
{
repaint();
return true;
}
return false;
}
// ---- buttons -------------------------------------------------------------
// shared palette for the black joystick keys
static const juce::Colour keyFace (0xff080808); // black key body
static const juce::Colour keyDown (0xff1c1c1c); // pressed
static const juce::Colour keyRim (0xff4a4741); // keeps black keys legible on dark metal
static const juce::Colour keyRimDn (0xff8a8a86);
static const juce::Colour legendIdle (0xfff2f2f2); // white arrow legends
static const juce::Colour legendHot (0xffffffff);
// solid triangular arrow centred on `centre`, pointing along `angleRad`
// (radians clockwise from 12 o'clock, screen y pointing down)
static void drawArrow (juce::Graphics& g, const juce::Point<float>& centre,
double angleRad, float size)
{
const float dx = (float) std::sin (angleRad);
const float dy = (float) -std::cos (angleRad);
const float px = -dy, py = dx; // perpendicular, for the two rear corners
const float back = size * 0.80f;
const float wide = size * 0.85f;
juce::Path t;
t.startNewSubPath (centre.x + dx * size, centre.y + dy * size);
t.lineTo (centre.x - dx * back + px * wide, centre.y - dy * back + py * wide);
t.lineTo (centre.x - dx * back - px * wide, centre.y - dy * back - py * wide);
t.closeSubPath();
g.fillPath (t);
}
void RackEditor::DirWedge::paintButton (juce::Graphics& g, bool over, bool down)
{
const juce::Point<float> c (outerR, outerR);
// a pressed key sinks slightly into the ring
const float ro = down ? outerR - 1.5f : outerR;
const float ri = down ? innerR + 1.5f : innerR;
const double a0 = angle - halfSpan;
const double a1 = angle + halfSpan;
juce::Path wedge;
wedge.startNewSubPath (c.getPointOnCircumference (ro, a0));
wedge.addCentredArc (c.x, c.y, ro, ro, 0.0f, a0, a1, false);
wedge.lineTo (c.getPointOnCircumference (ri, a1));
wedge.addCentredArc (c.x, c.y, ri, ri, 0.0f, a1, a0, false);
wedge.closeSubPath();
g.setColour (down ? keyDown : keyFace);
g.fillPath (wedge);
g.setColour (down ? keyRimDn : keyRim);
g.strokePath (wedge, juce::PathStrokeType (1.0f));
// white arrow legend, centred across the wedge band and pointing outwards
g.setColour (over || down ? legendHot : legendIdle);
drawArrow (g, c.getPointOnCircumference ((innerR + outerR) * 0.5f, angle), angle, 11.0f);
}
bool RackEditor::DirWedge::hitTest (int x, int y)
{
if (! juce::Component::hitTest (x, y))
return false;
const float dx = (float) x - outerR;
const float dy = (float) y - outerR;
const float r = std::sqrt (dx * dx + dy * dy);
if (r < innerR || r > outerR)
return false;
// same convention as the painted sector: radians clockwise from 12 o'clock
double diff = std::atan2 (dx, -dy) - angle;
for (; diff > juce::MathConstants<double>::pi; diff -= juce::MathConstants<double>::twoPi) {}
for (; diff < -juce::MathConstants<double>::pi; diff += juce::MathConstants<double>::twoPi) {}
return std::abs (diff) <= halfSpan;
}
bool RackEditor::CentreOkButton::hitTest (int x, int y)
{
if (! juce::Component::hitTest (x, y))
return false;
const float dx = (float) x - radius;
const float dy = (float) y - radius;
return dx * dx + dy * dy <= radius * radius;
}
void RackEditor::CentreOkButton::paintButton (juce::Graphics& g, bool over, bool down)
{
const auto c = getLocalBounds().getCentre().toFloat();
const float r = radius - (down ? 1.0f : 0.0f);
juce::Path disc;
disc.addEllipse (c.x - r, c.y - r, r * 2.0f, r * 2.0f);
g.setColour (down ? keyDown : keyFace);
g.fillPath (disc);
g.setColour (down ? keyRimDn : keyRim);
g.strokePath (disc, juce::PathStrokeType (1.0f));
g.setColour (over || down ? legendHot : legendIdle);
g.setFont (juce::FontOptions ("Arial", 9.5f, juce::Font::bold));
g.drawText (getName(), getLocalBounds(), juce::Justification::centred, true);
}
bool RackEditor::EscButton::hitTest (int x, int y)
{
if (! juce::Component::hitTest (x, y))
return false;
const float r = diameter * 0.5f;
const float dx = (float) x - r;
const float dy = (float) y - r;
return dx * dx + dy * dy <= r * r;
}
void RackEditor::EscButton::paintButton (juce::Graphics& g, bool over, bool down)
{
const auto c = getLocalBounds().getCentre().toFloat();
const float r = diameter * 0.5f - (down ? 1.0f : 0.0f);
juce::Path disc;
disc.addEllipse (c.x - r, c.y - r, r * 2.0f, r * 2.0f);
g.setColour (down ? keyDown : keyFace);
g.fillPath (disc);
g.setColour (down ? keyRimDn : keyRim);
g.strokePath (disc, juce::PathStrokeType (1.0f));
g.setColour (over || down ? legendHot : legendIdle);
g.setFont (juce::FontOptions ("Arial", 7.0f, juce::Font::bold));
g.drawText (getName(), getLocalBounds(), juce::Justification::centred, true);
}