/* ============================================================================== This file contains the basic framework code for a JUCE plugin editor. ============================================================================== */ #pragma once #include #include "PluginProcessor.h" //============================================================================== /** */ class FluteSynthAudioProcessorEditor : public juce::AudioProcessorEditor, private juce::Timer { public: FluteSynthAudioProcessorEditor (FluteSynthAudioProcessor&); ~FluteSynthAudioProcessorEditor() override; //============================================================================== void paint (juce::Graphics&) override; void resized() override; private: FluteSynthAudioProcessor& audioProcessor; juce::ComboBox presetSelector; juce::Label presetLabel { {}, "Preset" }; juce::ComboBox temperamentSelector; juce::Label temperamentLabel { {}, "Temperament" }; juce::ComboBox velocityCurveSelector; juce::Label velocityCurveLabel { {}, "Velocity Curve" }; juce::ToggleButton soundboardEnable { "Soundboard" }; juce::ToggleButton postRoomEnable { "Post Reverb" }; juce::TextButton resetButton { "Reset to preset" }; juce::Slider soundboardMix; juce::Slider soundboardT60; juce::Slider soundboardDamp; juce::Slider postRoomMix; juce::Slider stringSustain; juce::Slider hammerHardness; juce::Slider pm2GainDb; juce::Slider outputLpfCutoff; juce::Slider masterVolume; juce::Label soundboardMixLabel { {}, "Soundboard\nMix" }; juce::Label soundboardT60Label { {}, "Reverb Decay" }; juce::Label soundboardDampLabel{ {}, "Damp" }; juce::Label postRoomMixLabel { {}, "Post Reverb" }; juce::Label stringSustainLabel { {}, "String\nSustain" }; juce::Label hammerHardnessLabel { {}, "Hammer\nHardness" }; juce::Label pm2GainLabel { {}, "PM2 Gain" }; juce::Label outputLpfLabel { {}, "Brightness" }; juce::Label masterVolumeLabel { {}, "Master" }; using SliderAttachment = juce::AudioProcessorValueTreeState::SliderAttachment; using ButtonAttachment = juce::AudioProcessorValueTreeState::ButtonAttachment; using ComboBoxAttachment = juce::AudioProcessorValueTreeState::ComboBoxAttachment; std::unique_ptr soundboardEnableAttachment; std::unique_ptr postRoomEnableAttachment; std::unique_ptr soundboardMixAttachment; std::unique_ptr soundboardT60Attachment; std::unique_ptr soundboardDampAttachment; std::unique_ptr postRoomMixAttachment; std::unique_ptr stringSustainAttachment; std::unique_ptr hammerHardnessAttachment; std::unique_ptr pm2GainAttachment; std::unique_ptr outputLpfAttachment; std::unique_ptr masterVolumeAttachment; std::unique_ptr temperamentAttachment; std::unique_ptr velocityCurveAttachment; //============================================================================== // Custom LookAndFeel for elegant dropdown menus struct ElegantComboBoxLookAndFeel : juce::LookAndFeel_V4 { ElegantComboBoxLookAndFeel() { // Set popup menu colors setColour (juce::PopupMenu::backgroundColourId, juce::Colour::fromRGB (18, 20, 24)); setColour (juce::PopupMenu::textColourId, juce::Colour::fromRGB (220, 220, 225)); setColour (juce::PopupMenu::highlightedBackgroundColourId, juce::Colour::fromRGB (60, 70, 85)); setColour (juce::PopupMenu::highlightedTextColourId, juce::Colours::white); } void drawComboBox (juce::Graphics& g, int width, int height, bool isButtonDown, int buttonX, int buttonY, int buttonW, int buttonH, juce::ComboBox& box) override { juce::ignoreUnused (buttonX, buttonY, buttonW, buttonH); auto bounds = juce::Rectangle (0, 0, (float) width, (float) height); auto cornerSize = 3.0f; // Reduced from 6.0f // Outer glow/shadow effect g.setColour (juce::Colours::black.withAlpha (0.4f)); g.fillRoundedRectangle (bounds.translated (0, 2).reduced (1), cornerSize); // Main background gradient - dark metallic juce::ColourGradient bgGradient (juce::Colour::fromRGB (38, 42, 48), bounds.getCentreX(), bounds.getY(), juce::Colour::fromRGB (22, 24, 28), bounds.getCentreX(), bounds.getBottom(), false); g.setGradientFill (bgGradient); g.fillRoundedRectangle (bounds.reduced (1), cornerSize); // Subtle inner highlight at top juce::ColourGradient innerHighlight (juce::Colours::white.withAlpha (0.08f), bounds.getCentreX(), bounds.getY() + 1, juce::Colours::transparentWhite, bounds.getCentreX(), bounds.getY() + height * 0.4f, false); g.setGradientFill (innerHighlight); g.fillRoundedRectangle (bounds.reduced (2), cornerSize - 1); // Border - subtle golden/brass tint for piano aesthetic auto borderColour = isButtonDown ? juce::Colour::fromRGB (140, 130, 100) : juce::Colour::fromRGB (80, 75, 65); if (box.hasKeyboardFocus (false)) borderColour = juce::Colour::fromRGB (160, 150, 120); g.setColour (borderColour); g.drawRoundedRectangle (bounds.reduced (1.5f), cornerSize, 1.0f); // Arrow indicator auto arrowZone = bounds.removeFromRight ((float) height).reduced (8); juce::Path arrow; auto arrowH = arrowZone.getHeight() * 0.3f; auto arrowW = arrowZone.getWidth() * 0.5f; arrow.addTriangle (arrowZone.getCentreX() - arrowW * 0.5f, arrowZone.getCentreY() - arrowH * 0.3f, arrowZone.getCentreX() + arrowW * 0.5f, arrowZone.getCentreY() - arrowH * 0.3f, arrowZone.getCentreX(), arrowZone.getCentreY() + arrowH * 0.5f); g.setColour (juce::Colour::fromRGB (180, 175, 165)); g.fillPath (arrow); } void drawPopupMenuBackground (juce::Graphics& g, int width, int height) override { auto bounds = juce::Rectangle (0, 0, (float) width, (float) height); // Shadow g.setColour (juce::Colours::black.withAlpha (0.5f)); g.fillRoundedRectangle (bounds.translated (2, 3), 3.0f); // Reduced from 6.0f // Main background juce::ColourGradient bgGradient (juce::Colour::fromRGB (28, 32, 38), bounds.getCentreX(), bounds.getY(), juce::Colour::fromRGB (18, 20, 24), bounds.getCentreX(), bounds.getBottom(), false); g.setGradientFill (bgGradient); g.fillRoundedRectangle (bounds, 3.0f); // Reduced from 6.0f // Border g.setColour (juce::Colour::fromRGB (60, 58, 52)); g.drawRoundedRectangle (bounds.reduced (0.5f), 3.0f, 1.0f); // Reduced from 6.0f } void drawPopupMenuItem (juce::Graphics& g, const juce::Rectangle& area, bool isSeparator, bool isActive, bool isHighlighted, bool isTicked, bool hasSubMenu, const juce::String& text, const juce::String& shortcutKeyText, const juce::Drawable* icon, const juce::Colour* textColour) override { juce::ignoreUnused (shortcutKeyText, icon, textColour, hasSubMenu); if (isSeparator) { auto r = area.reduced (5, 0).toFloat(); r.removeFromTop ((float) r.getHeight() * 0.5f - 0.5f); g.setColour (juce::Colour::fromRGB (50, 52, 58)); g.fillRect (r.removeFromTop (1.0f)); return; } auto r = area.reduced (2); if (isHighlighted && isActive) { // Highlighted background with subtle gradient juce::ColourGradient hlGradient (juce::Colour::fromRGB (55, 65, 80), (float) r.getCentreX(), (float) r.getY(), juce::Colour::fromRGB (45, 52, 65), (float) r.getCentreX(), (float) r.getBottom(), false); g.setGradientFill (hlGradient); g.fillRoundedRectangle (r.toFloat(), 4.0f); } auto textColourToUse = isHighlighted ? juce::Colours::white : juce::Colour::fromRGB (200, 200, 205); if (! isActive) textColourToUse = textColourToUse.withAlpha (0.4f); g.setColour (textColourToUse); g.setFont (getPopupMenuFont().withHeight (15.0f)); auto textArea = r.reduced (12, 0); if (isTicked) { // Checkmark for selected item auto tickArea = textArea.removeFromLeft (20); juce::Path tick; tick.startNewSubPath ((float) tickArea.getX() + 4, (float) tickArea.getCentreY()); tick.lineTo ((float) tickArea.getX() + 8, (float) tickArea.getCentreY() + 4); tick.lineTo ((float) tickArea.getX() + 14, (float) tickArea.getCentreY() - 4); g.setColour (juce::Colour::fromRGB (180, 170, 140)); g.strokePath (tick, juce::PathStrokeType (2.0f)); } g.drawFittedText (text, textArea, juce::Justification::centredLeft, 1); } juce::Font getComboBoxFont (juce::ComboBox&) override { return juce::Font (15.0f); } void positionComboBoxText (juce::ComboBox& box, juce::Label& label) override { label.setBounds (12, 0, box.getWidth() - 30, box.getHeight()); label.setFont (getComboBoxFont (box)); label.setColour (juce::Label::textColourId, juce::Colour::fromRGB (220, 218, 212)); } }; //============================================================================== // Custom LookAndFeel for toggle buttons struct ElegantToggleLookAndFeel : juce::LookAndFeel_V4 { void drawToggleButton (juce::Graphics& g, juce::ToggleButton& button, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override { juce::ignoreUnused (shouldDrawButtonAsDown); auto bounds = button.getLocalBounds().toFloat(); auto tickBounds = bounds.removeFromLeft (bounds.getHeight()).reduced (4); // Checkbox background juce::ColourGradient bgGradient (juce::Colour::fromRGB (35, 38, 44), tickBounds.getCentreX(), tickBounds.getY(), juce::Colour::fromRGB (20, 22, 26), tickBounds.getCentreX(), tickBounds.getBottom(), false); g.setGradientFill (bgGradient); g.fillRoundedRectangle (tickBounds, 4.0f); // Border auto borderColour = shouldDrawButtonAsHighlighted ? juce::Colour::fromRGB (100, 95, 80) : juce::Colour::fromRGB (70, 68, 62); g.setColour (borderColour); g.drawRoundedRectangle (tickBounds.reduced (0.5f), 4.0f, 1.0f); if (button.getToggleState()) { // Filled state with warm glow auto innerBounds = tickBounds.reduced (3); juce::ColourGradient fillGradient (juce::Colour::fromRGB (160, 150, 120), innerBounds.getCentreX(), innerBounds.getY(), juce::Colour::fromRGB (120, 110, 85), innerBounds.getCentreX(), innerBounds.getBottom(), false); g.setGradientFill (fillGradient); g.fillRoundedRectangle (innerBounds, 2.0f); // Checkmark juce::Path tick; auto cx = tickBounds.getCentreX(); auto cy = tickBounds.getCentreY(); tick.startNewSubPath (cx - 4, cy); tick.lineTo (cx - 1, cy + 3); tick.lineTo (cx + 5, cy - 4); g.setColour (juce::Colour::fromRGB (30, 28, 24)); g.strokePath (tick, juce::PathStrokeType (2.2f, juce::PathStrokeType::curved)); } // Label text g.setColour (juce::Colour::fromRGB (210, 208, 200)); g.setFont (14.5f); g.drawText (button.getButtonText(), bounds.reduced (4, 0), juce::Justification::centredLeft, true); } }; //============================================================================== // Custom LookAndFeel for sliders - compact design struct DarkRectSliderLookAndFeel : juce::LookAndFeel_V4 { void drawLinearSlider (juce::Graphics& g, int x, int y, int width, int height, float sliderPos, float min, float max, const juce::Slider::SliderStyle style, juce::Slider& slider) override { juce::ignoreUnused (min, max, style, slider); // Track - narrower auto trackWidth = width * 0.11f; // Further reduced auto track = juce::Rectangle ((float) x + (width - trackWidth) * 0.5f, (float) y + 4.0f, trackWidth, (float) height - 8.0f); // Track shadow g.setColour (juce::Colours::black.withAlpha (0.3f)); g.fillRoundedRectangle (track.translated (1, 1), 2.0f); // Track gradient juce::ColourGradient trackGradient (juce::Colour::fromRGB (15, 15, 18), track.getCentreX(), track.getY(), juce::Colour::fromRGB (8, 8, 10), track.getCentreX(), track.getBottom(), false); g.setGradientFill (trackGradient); g.fillRoundedRectangle (track, 2.0f); // Track border g.setColour (juce::Colour::fromRGB (45, 45, 50)); g.drawRoundedRectangle (track, 2.0f, 1.0f); // Fill from bottom to thumb position auto fillTrack = track; fillTrack.setTop (sliderPos); juce::ColourGradient fillGradient (juce::Colour::fromRGB (80, 75, 60).withAlpha (0.4f), fillTrack.getCentreX(), fillTrack.getY(), juce::Colour::fromRGB (60, 55, 45).withAlpha (0.2f), fillTrack.getCentreX(), fillTrack.getBottom(), false); g.setGradientFill (fillGradient); g.fillRoundedRectangle (fillTrack, 2.0f); // Thumb - smaller float thumbW = (float) width * 0.50f; float thumbH = 9.0f; // Reduced from 11.0f float thumbX = (float) x + (width - thumbW) * 0.5f; float thumbY = sliderPos - thumbH * 0.5f; juce::Rectangle thumb (thumbX, thumbY, thumbW, thumbH); // Thumb shadow g.setColour (juce::Colours::black.withAlpha (0.4f)); g.fillRoundedRectangle (thumb.translated (0, 1.5f), 2.0f); // Thumb gradient juce::ColourGradient thumbGradient (juce::Colour::fromRGB (130, 125, 115), thumb.getCentreX(), thumb.getY(), juce::Colour::fromRGB (70, 68, 62), thumb.getCentreX(), thumb.getBottom(), false); g.setGradientFill (thumbGradient); g.fillRoundedRectangle (thumb, 2.0f); // Thumb highlight auto highlightRect = thumb.reduced (1.5f).withHeight (thumb.getHeight() * 0.35f); g.setColour (juce::Colours::white.withAlpha (0.12f)); g.fillRoundedRectangle (highlightRect, 1.0f); // Thumb border g.setColour (juce::Colour::fromRGB (25, 25, 28)); g.drawRoundedRectangle (thumb, 2.0f, 1.0f); // Center notch on thumb g.setColour (juce::Colour::fromRGB (40, 40, 44)); g.fillRect (thumb.getCentreX() - 5.0f, thumb.getCentreY() - 0.5f, 10.0f, 1.0f); } juce::Label* createSliderTextBox (juce::Slider& slider) override { auto* label = LookAndFeel_V4::createSliderTextBox (slider); label->setColour (juce::Label::textColourId, juce::Colour::fromRGB (180, 178, 170)); label->setColour (juce::Label::backgroundColourId, juce::Colour::fromRGB (20, 22, 26)); label->setColour (juce::Label::outlineColourId, juce::Colour::fromRGB (50, 50, 55)); return label; } }; ElegantComboBoxLookAndFeel comboLook; ElegantToggleLookAndFeel toggleLook; DarkRectSliderLookAndFeel sliderLook; void syncControlsFromParams(); void timerCallback() override; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FluteSynthAudioProcessorEditor) };