#include "PluginEditor.h" #include "BuildInfo.h" #include 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::halfPi); down = new DirWedge ("DOWN", juce::MathConstants::pi); left = new DirWedge ("LEFT", -juce::MathConstants::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 (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 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 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 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& 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 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::pi; diff -= juce::MathConstants::twoPi) {} for (; diff < -juce::MathConstants::pi; diff += juce::MathConstants::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); }