Add new 'add effect' button and toggle between the list of effects and the effects grid

pull/307/head
James H Ball 2025-08-09 15:32:40 +01:00
rodzic cfd2773f52
commit 3fc9fa208b
12 zmienionych plików z 164 dodań i 79 usunięć

Wyświetl plik

@ -33,8 +33,55 @@ EffectsComponent::EffectsComponent(OscirenderAudioProcessor& p, OscirenderAudioP
audioProcessor.broadcaster.addChangeListener(this); audioProcessor.broadcaster.addChangeListener(this);
} }
// Wire list model to notify when user wants to add
itemData.onAddNewEffectRequested = [this]() {
showingGrid = true;
if (grid)
grid->setVisible(true);
listBox.setVisible(false);
resized();
repaint();
};
// Start with grid visible by default
showingGrid = true;
grid = std::make_unique<EffectTypeGridComponent>(audioProcessor);
grid->onEffectSelected = [this](const juce::String& effectId) {
DBG("Effect selected from grid: " + effectId);
// Mark the chosen effect as selected and enabled (no instance creation for now)
{
juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock);
for (auto& eff : audioProcessor.toggleableEffects) {
if (eff->getId() == effectId) {
eff->markSelectable(true);
break;
}
}
}
// Refresh list content
itemData.resetData();
listBox.updateContent();
showingGrid = false;
listBox.setVisible(true);
if (grid)
grid->setVisible(false);
resized();
repaint();
};
grid->onCanceled = [this]() {
// If canceled while default grid, just show list
showingGrid = false;
listBox.setVisible(true);
if (grid)
grid->setVisible(false);
resized();
repaint();
};
listBox.setModel(&listBoxModel); listBox.setModel(&listBoxModel);
addAndMakeVisible(listBox); addAndMakeVisible(listBox);
addAndMakeVisible(*grid);
listBox.setVisible(false); // grid shown first
} }
EffectsComponent::~EffectsComponent() { EffectsComponent::~EffectsComponent() {
@ -52,7 +99,12 @@ void EffectsComponent::resized() {
frequency.setBounds(area.removeFromTop(30)); frequency.setBounds(area.removeFromTop(30));
area.removeFromTop(6); area.removeFromTop(6);
listBox.setBounds(area); if (showingGrid) {
if (grid)
grid->setBounds(area);
} else {
listBox.setBounds(area);
}
} }
void EffectsComponent::changeListenerCallback(juce::ChangeBroadcaster* source) { void EffectsComponent::changeListenerCallback(juce::ChangeBroadcaster* source) {

Wyświetl plik

@ -6,6 +6,7 @@
#include "PluginProcessor.h" #include "PluginProcessor.h"
#include "components/DraggableListBox.h" #include "components/DraggableListBox.h"
#include "components/EffectsListComponent.h" #include "components/EffectsListComponent.h"
#include "components/EffectTypeGridComponent.h"
class OscirenderAudioProcessorEditor; class OscirenderAudioProcessorEditor;
class EffectsComponent : public juce::GroupComponent, public juce::ChangeListener { class EffectsComponent : public juce::GroupComponent, public juce::ChangeListener {
@ -26,6 +27,8 @@ private:
AudioEffectListBoxItemData itemData; AudioEffectListBoxItemData itemData;
EffectsListBoxModel listBoxModel; EffectsListBoxModel listBoxModel;
DraggableListBox listBox; DraggableListBox listBox;
std::unique_ptr<EffectTypeGridComponent> grid;
bool showingGrid = true; // show grid by default
EffectComponent frequency = EffectComponent(*audioProcessor.frequencyEffect, false); EffectComponent frequency = EffectComponent(*audioProcessor.frequencyEffect, false);

Wyświetl plik

@ -162,6 +162,9 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse
for (int i = 0; i < toggleableEffects.size(); i++) { for (int i = 0; i < toggleableEffects.size(); i++) {
auto effect = toggleableEffects[i]; auto effect = toggleableEffects[i];
effect->markSelectable(false);
booleanParameters.push_back(effect->selected);
effect->selected->setValueNotifyingHost(false);
effect->markEnableable(false); effect->markEnableable(false);
booleanParameters.push_back(effect->enabled); booleanParameters.push_back(effect->enabled);
effect->enabled->setValueNotifyingHost(false); effect->enabled->setValueNotifyingHost(false);
@ -630,7 +633,9 @@ void OscirenderAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer, ju
juce::SpinLock::ScopedLockType lock2(effectsLock); juce::SpinLock::ScopedLockType lock2(effectsLock);
if (volume > EPSILON) { if (volume > EPSILON) {
for (auto& effect : toggleableEffects) { for (auto& effect : toggleableEffects) {
if (effect->enabled->getValue()) { bool isEnabled = effect->enabled != nullptr && effect->enabled->getValue();
bool isSelected = effect->selected == nullptr ? true : effect->selected->getBoolValue();
if (isEnabled && isSelected) {
if (effect->getId() == custom->getId()) { if (effect->getId() == custom->getId()) {
effect->setExternalInput(osci::Point{ left, right }); effect->setExternalInput(osci::Point{ left, right });
} }

Wyświetl plik

@ -6,6 +6,10 @@ EffectTypeGridComponent::EffectTypeGridComponent(OscirenderAudioProcessor& proce
{ {
setupEffectItems(); setupEffectItems();
setSize(400, 200); setSize(400, 200);
addAndMakeVisible(cancelButton);
cancelButton.onClick = [this]() {
if (onCanceled) onCanceled();
};
} }
EffectTypeGridComponent::~EffectTypeGridComponent() = default; EffectTypeGridComponent::~EffectTypeGridComponent() = default;
@ -44,6 +48,8 @@ void EffectTypeGridComponent::paint(juce::Graphics& g)
void EffectTypeGridComponent::resized() void EffectTypeGridComponent::resized()
{ {
auto bounds = getLocalBounds(); auto bounds = getLocalBounds();
auto topBar = bounds.removeFromTop(30);
cancelButton.setBounds(topBar.removeFromRight(80).reduced(4));
// Create FlexBox for responsive grid layout // Create FlexBox for responsive grid layout
flexBox = juce::FlexBox(); flexBox = juce::FlexBox();

Wyświetl plik

@ -14,11 +14,13 @@ public:
int calculateRequiredHeight(int availableWidth) const; int calculateRequiredHeight(int availableWidth) const;
std::function<void(const juce::String& effectId)> onEffectSelected; std::function<void(const juce::String& effectId)> onEffectSelected;
std::function<void()> onCanceled; // optional cancel handler
private: private:
OscirenderAudioProcessor& audioProcessor; OscirenderAudioProcessor& audioProcessor;
juce::OwnedArray<EffectTypeItemComponent> effectItems; juce::OwnedArray<EffectTypeItemComponent> effectItems;
juce::FlexBox flexBox; juce::FlexBox flexBox;
juce::TextButton cancelButton { "Cancel" };
static constexpr int ITEM_HEIGHT = 80; static constexpr int ITEM_HEIGHT = 80;
static constexpr int MIN_ITEM_WIDTH = 180; static constexpr int MIN_ITEM_WIDTH = 180;

Wyświetl plik

@ -1,8 +1,7 @@
#include "EffectTypeItemComponent.h" #include "EffectTypeItemComponent.h"
EffectTypeItemComponent::EffectTypeItemComponent(const juce::String& name, const juce::String& icon, const juce::String& id) EffectTypeItemComponent::EffectTypeItemComponent(const juce::String& name, const juce::String& icon, const juce::String& id)
: effectName(name), effectId(id), : effectName(name), effectId(id)
hoverAnimation(std::make_unique<HoverAnimationMixin>(this))
{ {
juce::String iconSvg = icon; juce::String iconSvg = icon;
if (icon.isEmpty()) { if (icon.isEmpty()) {
@ -26,8 +25,8 @@ void EffectTypeItemComponent::paint(juce::Graphics& g)
{ {
auto bounds = getLocalBounds().toFloat().reduced(10); auto bounds = getLocalBounds().toFloat().reduced(10);
// Get animation progress from the hover animation mixin // Get animation progress from inherited HoverAnimationMixin
auto animationProgress = hoverAnimation->getAnimationProgress(); auto animationProgress = getAnimationProgress();
// Apply upward shift based on animation progress // Apply upward shift based on animation progress
auto yOffset = -animationProgress * HOVER_LIFT_AMOUNT; auto yOffset = -animationProgress * HOVER_LIFT_AMOUNT;
@ -40,10 +39,11 @@ void EffectTypeItemComponent::paint(juce::Graphics& g)
shadow.radius = 15 * animationProgress; shadow.radius = 15 * animationProgress;
shadow.offset = juce::Point<int>(0, 4); shadow.offset = juce::Point<int>(0, 4);
juce::Path shadowPath; if (shadow.radius > 0) {
shadowPath.addRoundedRectangle(bounds.toFloat(), CORNER_RADIUS); juce::Path shadowPath;
shadow.drawForPath(g, shadowPath); shadowPath.addRoundedRectangle(bounds.toFloat(), CORNER_RADIUS);
shadow.drawForPath(g, shadowPath);
}
} }
// Draw background with rounded corners - interpolate between normal and hover colors // Draw background with rounded corners - interpolate between normal and hover colors
@ -79,32 +79,19 @@ void EffectTypeItemComponent::resized()
iconButton->setBounds(iconArea); iconButton->setBounds(iconArea);
// Get animation progress and calculate Y offset // Get animation progress and calculate Y offset
auto animationProgress = hoverAnimation->getAnimationProgress(); auto animationProgress = getAnimationProgress();
auto yOffset = -animationProgress * HOVER_LIFT_AMOUNT; auto yOffset = -animationProgress * HOVER_LIFT_AMOUNT;
iconButton->setTransform(juce::AffineTransform::translation(0, yOffset)); iconButton->setTransform(juce::AffineTransform::translation(0, yOffset));
} }
void EffectTypeItemComponent::mouseEnter(const juce::MouseEvent& event)
{
hoverAnimation->handleMouseEnter();
}
void EffectTypeItemComponent::mouseExit(const juce::MouseEvent& event)
{
hoverAnimation->handleMouseExit();
}
void EffectTypeItemComponent::mouseDown(const juce::MouseEvent& event) void EffectTypeItemComponent::mouseDown(const juce::MouseEvent& event)
{ {
if (onEffectSelected) // Extend base behavior to keep hover press animation
HoverAnimationMixin::mouseDown(event);
if (onEffectSelected) {
onEffectSelected(effectId); onEffectSelected(effectId);
hoverAnimation->handleMouseDown(); }
}
void EffectTypeItemComponent::mouseUp(const juce::MouseEvent& event)
{
hoverAnimation->handleMouseUp(event.getPosition(), getLocalBounds());
} }
void EffectTypeItemComponent::mouseMove(const juce::MouseEvent& event) { void EffectTypeItemComponent::mouseMove(const juce::MouseEvent& event) {

Wyświetl plik

@ -4,7 +4,7 @@
#include "HoverAnimationMixin.h" #include "HoverAnimationMixin.h"
#include "SvgButton.h" #include "SvgButton.h"
class EffectTypeItemComponent : public juce::Component class EffectTypeItemComponent : public HoverAnimationMixin
{ {
public: public:
EffectTypeItemComponent(const juce::String& name, const juce::String& icon, const juce::String& id); EffectTypeItemComponent(const juce::String& name, const juce::String& icon, const juce::String& id);
@ -12,10 +12,7 @@ public:
void paint(juce::Graphics& g) override; void paint(juce::Graphics& g) override;
void resized() override; void resized() override;
void mouseEnter(const juce::MouseEvent& event) override;
void mouseExit(const juce::MouseEvent& event) override;
void mouseDown(const juce::MouseEvent& event) override; void mouseDown(const juce::MouseEvent& event) override;
void mouseUp(const juce::MouseEvent& event) override;
void mouseMove(const juce::MouseEvent& event) override; void mouseMove(const juce::MouseEvent& event) override;
const juce::String& getEffectId() const { return effectId; } const juce::String& getEffectId() const { return effectId; }
@ -27,9 +24,6 @@ private:
juce::String effectName; juce::String effectName;
juce::String effectId; juce::String effectId;
// Hover animation functionality
std::unique_ptr<HoverAnimationMixin> hoverAnimation;
// Icon for the effect // Icon for the effect
std::unique_ptr<SvgButton> iconButton; std::unique_ptr<SvgButton> iconButton;

Wyświetl plik

@ -114,6 +114,10 @@ std::shared_ptr<juce::Component> EffectsListComponent::createComponent(osci::Eff
int EffectsListBoxModel::getRowHeight(int row) { int EffectsListBoxModel::getRowHeight(int row) {
auto data = (AudioEffectListBoxItemData&)modelData; auto data = (AudioEffectListBoxItemData&)modelData;
if (row == data.getNumItems() - 1) {
// Last row is the "Add new effect" button
return 44; // a tidy button height
}
return data.getEffect(row)->parameters.size() * EffectsListComponent::ROW_HEIGHT + EffectsListComponent::PADDING; return data.getEffect(row)->parameters.size() * EffectsListComponent::ROW_HEIGHT + EffectsListComponent::PADDING;
} }
@ -124,11 +128,24 @@ bool EffectsListBoxModel::hasVariableHeightRows() const {
juce::Component* EffectsListBoxModel::refreshComponentForRow(int rowNumber, bool isRowSelected, juce::Component *existingComponentToUpdate) { juce::Component* EffectsListBoxModel::refreshComponentForRow(int rowNumber, bool isRowSelected, juce::Component *existingComponentToUpdate) {
auto data = (AudioEffectListBoxItemData&)modelData; auto data = (AudioEffectListBoxItemData&)modelData;
if (juce::isPositiveAndBelow(rowNumber, data.getNumItems())) { if (juce::isPositiveAndBelow(rowNumber, data.getNumItems() - 1)) {
// Regular effect component // Regular effect component
std::unique_ptr<EffectsListComponent> item(dynamic_cast<EffectsListComponent*>(existingComponentToUpdate)); std::unique_ptr<EffectsListComponent> item(dynamic_cast<EffectsListComponent*>(existingComponentToUpdate));
item = std::make_unique<EffectsListComponent>(listBox, data, rowNumber, *data.getEffect(rowNumber)); item = std::make_unique<EffectsListComponent>(listBox, (AudioEffectListBoxItemData&)modelData, rowNumber, *data.getEffect(rowNumber));
return item.release(); return item.release();
} else if (rowNumber == data.getNumItems() - 1) {
// Last row becomes an "Add new effect" button
auto* btn = dynamic_cast<juce::TextButton*>(existingComponentToUpdate);
if (btn == nullptr)
btn = new juce::TextButton("+ Add new effect");
btn->setButtonText("+ Add new effect");
auto onAdd = data.onAddNewEffectRequested; // copy to avoid dangling reference
btn->onClick = [onAdd]() mutable {
if (onAdd)
onAdd();
};
return btn;
} }
return nullptr; return nullptr;

Wyświetl plik

@ -5,6 +5,7 @@
#include "EffectComponent.h" #include "EffectComponent.h"
#include "ComponentList.h" #include "ComponentList.h"
#include "SwitchButton.h" #include "SwitchButton.h"
#include "EffectTypeGridComponent.h"
#include <random> #include <random>
// Application-specific data container // Application-specific data container
@ -14,6 +15,7 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData
std::vector<std::shared_ptr<osci::Effect>> data; std::vector<std::shared_ptr<osci::Effect>> data;
OscirenderAudioProcessor& audioProcessor; OscirenderAudioProcessor& audioProcessor;
OscirenderAudioProcessorEditor& editor; OscirenderAudioProcessorEditor& editor;
std::function<void()> onAddNewEffectRequested; // callback hooked by parent to open the grid
AudioEffectListBoxItemData(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) : audioProcessor(p), editor(editor) { AudioEffectListBoxItemData(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) : audioProcessor(p), editor(editor) {
resetData(); resetData();
@ -21,11 +23,31 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData
void randomise() { void randomise() {
juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock);
// Decide how many effects to select (1..5 or up to available)
for (int i = 0; i < data.size(); i++) { int total = (int) audioProcessor.toggleableEffects.size();
auto effect = data[i]; int maxPick = juce::jmin(5, total);
int numPick = juce::jmax(1, juce::Random::getSystemRandom().nextInt({1, maxPick + 1}));
// Build indices [0..total)
std::vector<int> indices(total);
std::iota(indices.begin(), indices.end(), 0);
std::random_device rd;
std::mt19937 g(rd());
std::shuffle(indices.begin(), indices.end(), g);
// First, deselect and disable all
for (auto& effect : audioProcessor.toggleableEffects) {
effect->markSelectable(false);
effect->markEnableable(false);
}
// Pick numPick to select & enable, and randomise params
for (int k = 0; k < numPick && k < indices.size(); ++k) {
auto& effect = audioProcessor.toggleableEffects[indices[k]];
effect->markSelectable(true);
effect->markEnableable(true);
auto id = effect->getId().toLowerCase(); auto id = effect->getId().toLowerCase();
if (id.contains("scale") || id.contains("translate") || id.contains("trace")) { if (id.contains("scale") || id.contains("translate") || id.contains("trace")) {
continue; continue;
} }
@ -35,25 +57,22 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData
if (parameter->lfo != nullptr) { if (parameter->lfo != nullptr) {
parameter->lfo->setUnnormalisedValueNotifyingHost((int) osci::LfoType::Static); parameter->lfo->setUnnormalisedValueNotifyingHost((int) osci::LfoType::Static);
parameter->lfoRate->setUnnormalisedValueNotifyingHost(1); parameter->lfoRate->setUnnormalisedValueNotifyingHost(1);
if (juce::Random::getSystemRandom().nextFloat() > 0.8) { if (juce::Random::getSystemRandom().nextFloat() > 0.8) {
parameter->lfo->setUnnormalisedValueNotifyingHost((int)(juce::Random::getSystemRandom().nextFloat() * (int) osci::LfoType::Noise)); parameter->lfo->setUnnormalisedValueNotifyingHost((int)(juce::Random::getSystemRandom().nextFloat() * (int) osci::LfoType::Noise));
parameter->lfoRate->setValueNotifyingHost(juce::Random::getSystemRandom().nextFloat() * 0.1); parameter->lfoRate->setValueNotifyingHost(juce::Random::getSystemRandom().nextFloat() * 0.1);
} }
} }
} }
effect->enabled->setValueNotifyingHost(juce::Random::getSystemRandom().nextFloat() > 0.7);
} }
// shuffle precedence // Refresh local data with only selected effects
std::random_device rd; resetData();
std::mt19937 g(rd());
// shuffle precedence of the selected subset
std::shuffle(data.begin(), data.end(), g); std::shuffle(data.begin(), data.end(), g);
for (int i = 0; i < data.size(); i++) { for (int i = 0; i < data.size(); i++) {
data[i]->setPrecedence(i); data[i]->setPrecedence(i);
} }
audioProcessor.updateEffectPrecedence(); audioProcessor.updateEffectPrecedence();
} }
@ -63,12 +82,16 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData
for (int i = 0; i < audioProcessor.toggleableEffects.size(); i++) { for (int i = 0; i < audioProcessor.toggleableEffects.size(); i++) {
auto effect = audioProcessor.toggleableEffects[i]; auto effect = audioProcessor.toggleableEffects[i];
effect->setValue(effect->getValue()); effect->setValue(effect->getValue());
data.push_back(effect); // Ensure 'selected' exists and defaults to true for older projects
effect->markSelectable(effect->selected == nullptr ? true : effect->selected->getBoolValue());
if (effect->selected == nullptr || effect->selected->getBoolValue()) {
data.push_back(effect);
}
} }
} }
int getNumItems() override { int getNumItems() override {
return data.size(); return data.size() + 1;
} }
// CURRENTLY NOT USED // CURRENTLY NOT USED

Wyświetl plik

@ -1,17 +1,14 @@
#include "HoverAnimationMixin.h" #include "HoverAnimationMixin.h"
HoverAnimationMixin::HoverAnimationMixin(juce::Component* targetComponent) HoverAnimationMixin::HoverAnimationMixin()
: component(targetComponent), : animatorUpdater(this),
animatorUpdater(targetComponent),
hoverAnimator(juce::ValueAnimatorBuilder{} hoverAnimator(juce::ValueAnimatorBuilder{}
.withEasing(getEasingFunction()) .withEasing(getEasingFunction())
.withDurationMs(getHoverAnimationDurationMs()) .withDurationMs(getHoverAnimationDurationMs())
.withValueChangedCallback([this](auto value) { .withValueChangedCallback([this](auto value) {
animationProgress = static_cast<float>(value); animationProgress = static_cast<float>(value);
if (component != nullptr) { repaint();
component->repaint(); resized();
component->resized();
}
}) })
.build()), .build()),
unhoverAnimator(juce::ValueAnimatorBuilder{} unhoverAnimator(juce::ValueAnimatorBuilder{}
@ -19,10 +16,8 @@ HoverAnimationMixin::HoverAnimationMixin(juce::Component* targetComponent)
.withDurationMs(getHoverAnimationDurationMs()) .withDurationMs(getHoverAnimationDurationMs())
.withValueChangedCallback([this](auto value) { .withValueChangedCallback([this](auto value) {
animationProgress = 1.0f - static_cast<float>(value); animationProgress = 1.0f - static_cast<float>(value);
if (component != nullptr) { repaint();
component->repaint(); resized();
component->resized();
}
}) })
.build()) .build())
{ {
@ -49,27 +44,27 @@ void HoverAnimationMixin::animateHover(bool isHovering)
} }
} }
void HoverAnimationMixin::handleMouseEnter() void HoverAnimationMixin::mouseEnter(const juce::MouseEvent&)
{ {
isHovered = true; isHovered = true;
animateHover(true); animateHover(true);
} }
void HoverAnimationMixin::handleMouseExit() void HoverAnimationMixin::mouseExit(const juce::MouseEvent&)
{ {
isHovered = false; isHovered = false;
// Fixed logic to prevent getting stuck in hovered state // Fixed logic to prevent getting stuck in hovered state
animateHover(false); animateHover(false);
} }
void HoverAnimationMixin::handleMouseDown() void HoverAnimationMixin::mouseDown(const juce::MouseEvent&)
{ {
animateHover(false); animateHover(false);
} }
void HoverAnimationMixin::handleMouseUp(const juce::Point<int>& mousePosition, const juce::Rectangle<int>& componentBounds) void HoverAnimationMixin::mouseUp(const juce::MouseEvent& event)
{ {
// Only animate hover if the mouse is still within the component bounds // Only animate hover if the mouse is still within the component bounds
if (componentBounds.contains(mousePosition)) if (getLocalBounds().contains(event.getEventRelativeTo(this).getPosition()))
animateHover(true); animateHover(true);
} }

Wyświetl plik

@ -1,32 +1,33 @@
#pragma once #pragma once
#include <JuceHeader.h> #include <JuceHeader.h>
class HoverAnimationMixin // Base Component providing animated hover behavior via JUCE mouse overrides.
class HoverAnimationMixin : public juce::Component
{ {
public: public:
HoverAnimationMixin(juce::Component* targetComponent); HoverAnimationMixin();
virtual ~HoverAnimationMixin() = default; ~HoverAnimationMixin() override = default;
// Animation control // Animation control (available for programmatic triggers if needed)
void animateHover(bool isHovering); void animateHover(bool isHovering);
// Getters // Getters
float getAnimationProgress() const { return animationProgress; } float getAnimationProgress() const { return animationProgress; }
bool getIsHovered() const { return isHovered; } bool getIsHovered() const { return isHovered; }
// Mouse event handlers to be called from the component
void handleMouseEnter();
void handleMouseExit();
void handleMouseDown();
void handleMouseUp(const juce::Point<int>& mousePosition, const juce::Rectangle<int>& componentBounds);
protected: protected:
// Override this to customize animation parameters // Customization hooks
virtual int getHoverAnimationDurationMs() const { return 200; } virtual int getHoverAnimationDurationMs() const { return 200; }
virtual std::function<float(float)> getEasingFunction() const { return juce::Easings::createEaseOut(); } virtual std::function<float(float)> getEasingFunction() const { return juce::Easings::createEaseOut(); }
public:
// juce::Component overrides for mouse events - keep public so children can call base explicitly
void mouseEnter(const juce::MouseEvent& event) override;
void mouseExit(const juce::MouseEvent& event) override;
void mouseDown(const juce::MouseEvent& event) override;
void mouseUp(const juce::MouseEvent& event) override;
private: private:
juce::Component* component;
float animationProgress = 0.0f; float animationProgress = 0.0f;
bool isHovered = false; bool isHovered = false;

@ -1 +1 @@
Subproject commit 475f3017cda5377062611d287605b4f26d4e3551 Subproject commit e21dd509d93aba1ee8ef31eecb54f5f9fc6f0644