kopia lustrzana https://github.com/jameshball/osci-render
Make grid component more generic
rodzic
13a7744fa3
commit
ccfb8391be
|
@ -30,7 +30,7 @@ public:
|
||||||
std::make_shared<PerspectiveEffect>(),
|
std::make_shared<PerspectiveEffect>(),
|
||||||
std::vector<osci::EffectParameter*>{
|
std::vector<osci::EffectParameter*>{
|
||||||
new osci::EffectParameter("Perspective", "Controls the strength of the 3D perspective projection.", "perspectiveStrength", VERSION_HINT, 1.0, 0.0, 1.0),
|
new osci::EffectParameter("Perspective", "Controls the strength of the 3D perspective projection.", "perspectiveStrength", VERSION_HINT, 1.0, 0.0, 1.0),
|
||||||
new osci::EffectParameter("FOV", "Controls the camera's field of view in degrees. A lower field of view makes the image look more flat, and a higher field of view makes the image look more 3D.", "perspectiveFov", VERSION_HINT, 50.0, 5.0, 130.0),
|
new osci::EffectParameter("Field of View", "Controls the camera's field of view in degrees. A lower field of view makes the image look more flat, and a higher field of view makes the image look more 3D.", "perspectiveFov", VERSION_HINT, 50.0, 5.0, 130.0),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return eff;
|
return eff;
|
||||||
|
|
|
@ -1,25 +1,24 @@
|
||||||
#include "EffectTypeGridComponent.h"
|
#include "EffectTypeGridComponent.h"
|
||||||
|
#include <JuceHeader.h>
|
||||||
#include "../LookAndFeel.h"
|
#include "../LookAndFeel.h"
|
||||||
|
#include "GridComponent.h"
|
||||||
|
#include "GridItemComponent.h"
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
|
|
||||||
|
// ===================== EffectTypeGridComponent (wrapper) =====================
|
||||||
|
|
||||||
EffectTypeGridComponent::EffectTypeGridComponent(OscirenderAudioProcessor& processor)
|
EffectTypeGridComponent::EffectTypeGridComponent(OscirenderAudioProcessor& processor)
|
||||||
: audioProcessor(processor)
|
: audioProcessor(processor)
|
||||||
{
|
{
|
||||||
// Setup scrollable viewport and content
|
addAndMakeVisible(grid);
|
||||||
addAndMakeVisible(viewport);
|
|
||||||
viewport.setViewedComponent(&content, false);
|
|
||||||
viewport.setScrollBarsShown(true, false); // vertical only
|
|
||||||
// Setup reusable bottom fade
|
|
||||||
initScrollFade(*this);
|
|
||||||
attachToViewport(viewport);
|
|
||||||
setupEffectItems();
|
|
||||||
setSize(400, 200);
|
setSize(400, 200);
|
||||||
addAndMakeVisible(cancelButton);
|
addAndMakeVisible(cancelButton);
|
||||||
cancelButton.onClick = [this]() {
|
cancelButton.onClick = [this]() {
|
||||||
if (onCanceled) onCanceled();
|
if (onCanceled) onCanceled();
|
||||||
};
|
};
|
||||||
|
setupEffectItems();
|
||||||
refreshDisabledStates();
|
refreshDisabledStates();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,9 +26,7 @@ EffectTypeGridComponent::~EffectTypeGridComponent() = default;
|
||||||
|
|
||||||
void EffectTypeGridComponent::setupEffectItems()
|
void EffectTypeGridComponent::setupEffectItems()
|
||||||
{
|
{
|
||||||
// Clear existing items
|
grid.clearItems();
|
||||||
effectItems.clear();
|
|
||||||
content.removeAllChildren();
|
|
||||||
|
|
||||||
// Get effect types directly from the audio processor's toggleableEffects
|
// Get effect types directly from the audio processor's toggleableEffects
|
||||||
juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock);
|
juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock);
|
||||||
|
@ -52,11 +49,11 @@ void EffectTypeGridComponent::setupEffectItems()
|
||||||
// Extract effect name from the effect
|
// Extract effect name from the effect
|
||||||
juce::String effectName = effect->getName();
|
juce::String effectName = effect->getName();
|
||||||
|
|
||||||
// Create new item component
|
// Create new generic item component
|
||||||
auto* item = new EffectTypeItemComponent(effectName, effect->getIcon(), effect->getId());
|
auto* item = new GridItemComponent(effectName, effect->getIcon(), effect->getId());
|
||||||
|
|
||||||
// Set up callback to forward effect selection
|
// Set up callback to forward selection
|
||||||
item->onEffectSelected = [this](const juce::String& effectId) {
|
item->onItemSelected = [this](const juce::String& effectId) {
|
||||||
if (onEffectSelected)
|
if (onEffectSelected)
|
||||||
onEffectSelected(effectId);
|
onEffectSelected(effectId);
|
||||||
};
|
};
|
||||||
|
@ -74,8 +71,7 @@ void EffectTypeGridComponent::setupEffectItems()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
effectItems.add(item);
|
grid.addItem(item);
|
||||||
content.addAndMakeVisible(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,13 +89,11 @@ void EffectTypeGridComponent::refreshDisabledStates()
|
||||||
selectedIds.insert(eff->getId().toStdString());
|
selectedIds.insert(eff->getId().toStdString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (auto* item : effectItems) {
|
for (auto* item : grid.getItems()) {
|
||||||
const bool disable = selectedIds.find(item->getEffectId().toStdString()) != selectedIds.end();
|
const bool disable = selectedIds.find(item->getId().toStdString()) != selectedIds.end();
|
||||||
item->setEnabled(! disable);
|
item->setEnabled(! disable);
|
||||||
}
|
}
|
||||||
cancelButton.setVisible(anySelected);
|
cancelButton.setVisible(anySelected);
|
||||||
// Update fade visibility/layout in case scrollability changed
|
|
||||||
layoutScrollFade(viewport.getBounds(), true, 48);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EffectTypeGridComponent::paint(juce::Graphics& g)
|
void EffectTypeGridComponent::paint(juce::Graphics& g)
|
||||||
|
@ -112,94 +106,5 @@ void EffectTypeGridComponent::resized()
|
||||||
auto bounds = getLocalBounds();
|
auto bounds = getLocalBounds();
|
||||||
auto topBar = bounds.removeFromTop(30);
|
auto topBar = bounds.removeFromTop(30);
|
||||||
cancelButton.setBounds(topBar.removeFromRight(80).reduced(4));
|
cancelButton.setBounds(topBar.removeFromRight(80).reduced(4));
|
||||||
viewport.setBounds(bounds);
|
grid.setBounds(bounds);
|
||||||
auto contentArea = viewport.getLocalBounds();
|
|
||||||
// Lock content width to viewport width to avoid horizontal scrolling
|
|
||||||
content.setSize(contentArea.getWidth(), content.getHeight());
|
|
||||||
|
|
||||||
// Create FlexBox for responsive grid layout within content
|
|
||||||
flexBox = juce::FlexBox();
|
|
||||||
flexBox.flexWrap = juce::FlexBox::Wrap::wrap;
|
|
||||||
flexBox.justifyContent = juce::FlexBox::JustifyContent::spaceBetween;
|
|
||||||
flexBox.alignContent = juce::FlexBox::AlignContent::flexStart;
|
|
||||||
flexBox.flexDirection = juce::FlexBox::Direction::row;
|
|
||||||
|
|
||||||
// Determine fixed per-item width for this viewport width
|
|
||||||
const int viewW = contentArea.getWidth();
|
|
||||||
const int viewH = contentArea.getHeight();
|
|
||||||
const int itemsPerRow = juce::jmax(1, viewW / MIN_ITEM_WIDTH);
|
|
||||||
const int fixedItemWidth = (itemsPerRow > 0 ? viewW / itemsPerRow : viewW);
|
|
||||||
|
|
||||||
// Add each effect item with a fixed width, and pad the final row with placeholders so it's centered
|
|
||||||
const int total = effectItems.size();
|
|
||||||
const int fullRows = (itemsPerRow > 0 ? total / itemsPerRow : 0);
|
|
||||||
const int remainder = (itemsPerRow > 0 ? total % itemsPerRow : 0);
|
|
||||||
|
|
||||||
auto addItemFlex = [&](juce::Component* c)
|
|
||||||
{
|
|
||||||
flexBox.items.add(juce::FlexItem(*c)
|
|
||||||
.withMinWidth((float) fixedItemWidth)
|
|
||||||
.withMaxWidth((float) fixedItemWidth)
|
|
||||||
.withHeight((float) ITEM_HEIGHT)
|
|
||||||
.withFlex(1.0f) // keep existing flex behaviour; fixed max width holds size
|
|
||||||
.withMargin(juce::FlexItem::Margin(0)));
|
|
||||||
};
|
|
||||||
|
|
||||||
auto addPlaceholder = [&]()
|
|
||||||
{
|
|
||||||
// Placeholder occupies a slot visually but has no component; ensures last row is centered
|
|
||||||
juce::FlexItem placeholder((float) fixedItemWidth, (float) ITEM_HEIGHT);
|
|
||||||
placeholder.flexGrow = 1.0f; // match item flex for consistent spacing
|
|
||||||
placeholder.margin = juce::FlexItem::Margin(0);
|
|
||||||
flexBox.items.add(std::move(placeholder));
|
|
||||||
};
|
|
||||||
|
|
||||||
int index = 0;
|
|
||||||
// Add complete rows
|
|
||||||
for (int r = 0; r < fullRows; ++r)
|
|
||||||
for (int c = 0; c < itemsPerRow; ++c)
|
|
||||||
addItemFlex(effectItems.getUnchecked(index++));
|
|
||||||
|
|
||||||
// Add last row centered with balanced placeholders
|
|
||||||
if (remainder > 0)
|
|
||||||
{
|
|
||||||
const int missing = itemsPerRow - remainder;
|
|
||||||
const int leftPad = missing / 2;
|
|
||||||
const int rightPad = missing - leftPad;
|
|
||||||
|
|
||||||
for (int i = 0; i < leftPad; ++i) addPlaceholder();
|
|
||||||
for (int i = 0; i < remainder; ++i) addItemFlex(effectItems.getUnchecked(index++));
|
|
||||||
for (int i = 0; i < rightPad; ++i) addPlaceholder();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute required content height
|
|
||||||
const int requiredHeight = calculateRequiredHeight(viewW);
|
|
||||||
|
|
||||||
// If content is shorter than viewport, make content at least as tall as viewport
|
|
||||||
int yOffset = 0;
|
|
||||||
if (requiredHeight < viewH) {
|
|
||||||
content.setSize(viewW, viewH);
|
|
||||||
yOffset = (viewH - requiredHeight) / 2;
|
|
||||||
} else {
|
|
||||||
content.setSize(viewW, requiredHeight);
|
|
||||||
}
|
|
||||||
// Layout items within content at the computed offset
|
|
||||||
flexBox.performLayout(juce::Rectangle<float>(0.0f, (float) yOffset, (float) viewW, (float) requiredHeight));
|
|
||||||
|
|
||||||
// Layout bottom scroll fade over the viewport area
|
|
||||||
layoutScrollFade(viewport.getBounds(), true, 48);
|
|
||||||
}
|
|
||||||
|
|
||||||
int EffectTypeGridComponent::calculateRequiredHeight(int availableWidth) const
|
|
||||||
{
|
|
||||||
if (effectItems.isEmpty())
|
|
||||||
return ITEM_HEIGHT;
|
|
||||||
|
|
||||||
// Calculate how many items can fit per row
|
|
||||||
int itemsPerRow = juce::jmax(1, availableWidth / MIN_ITEM_WIDTH);
|
|
||||||
|
|
||||||
// Calculate number of rows needed
|
|
||||||
int numRows = (effectItems.size() + itemsPerRow - 1) / itemsPerRow; // Ceiling division
|
|
||||||
|
|
||||||
return numRows * ITEM_HEIGHT;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <JuceHeader.h>
|
#include <JuceHeader.h>
|
||||||
#include "../PluginProcessor.h"
|
#include "../PluginProcessor.h"
|
||||||
#include "EffectTypeItemComponent.h"
|
#include "GridComponent.h"
|
||||||
#include "ScrollFadeMixin.h"
|
|
||||||
|
|
||||||
class EffectTypeGridComponent : public juce::Component, private ScrollFadeMixin
|
// Effect-specific wrapper that declares which items appear in the grid
|
||||||
|
class EffectTypeGridComponent : public juce::Component
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
EffectTypeGridComponent(OscirenderAudioProcessor& processor);
|
EffectTypeGridComponent(OscirenderAudioProcessor& processor);
|
||||||
|
@ -13,22 +13,15 @@ public:
|
||||||
void paint(juce::Graphics& g) override;
|
void paint(juce::Graphics& g) override;
|
||||||
void resized() override;
|
void resized() override;
|
||||||
|
|
||||||
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
|
std::function<void()> onCanceled; // optional cancel handler
|
||||||
void refreshDisabledStates(); // grey-out items that are already selected
|
void refreshDisabledStates(); // grey-out items that are already selected
|
||||||
|
|
||||||
private:
|
private:
|
||||||
OscirenderAudioProcessor& audioProcessor;
|
OscirenderAudioProcessor& audioProcessor;
|
||||||
juce::Viewport viewport; // scroll container
|
GridComponent grid;
|
||||||
juce::Component content; // holds the grid items
|
|
||||||
juce::OwnedArray<EffectTypeItemComponent> effectItems;
|
|
||||||
juce::FlexBox flexBox;
|
|
||||||
juce::TextButton cancelButton { "Cancel" };
|
juce::TextButton cancelButton { "Cancel" };
|
||||||
|
|
||||||
static constexpr int ITEM_HEIGHT = 80;
|
|
||||||
static constexpr int MIN_ITEM_WIDTH = 180;
|
|
||||||
|
|
||||||
void setupEffectItems();
|
void setupEffectItems();
|
||||||
|
|
||||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EffectTypeGridComponent)
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EffectTypeGridComponent)
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
#include "GridComponent.h"
|
||||||
|
|
||||||
|
GridComponent::GridComponent()
|
||||||
|
{
|
||||||
|
// Setup scrollable viewport and content
|
||||||
|
addAndMakeVisible(viewport);
|
||||||
|
viewport.setViewedComponent(&content, false);
|
||||||
|
viewport.setScrollBarsShown(true, false); // vertical only
|
||||||
|
// Setup reusable bottom fade
|
||||||
|
initScrollFade(*this);
|
||||||
|
attachToViewport(viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
GridComponent::~GridComponent() = default;
|
||||||
|
|
||||||
|
void GridComponent::clearItems()
|
||||||
|
{
|
||||||
|
items.clear();
|
||||||
|
content.removeAllChildren();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GridComponent::addItem(GridItemComponent* item)
|
||||||
|
{
|
||||||
|
items.add(item);
|
||||||
|
content.addAndMakeVisible(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GridComponent::paint(juce::Graphics& g)
|
||||||
|
{
|
||||||
|
// transparent background
|
||||||
|
}
|
||||||
|
|
||||||
|
void GridComponent::resized()
|
||||||
|
{
|
||||||
|
auto bounds = getLocalBounds();
|
||||||
|
viewport.setBounds(bounds);
|
||||||
|
auto contentArea = viewport.getLocalBounds();
|
||||||
|
// Lock content width to viewport width to avoid horizontal scrolling
|
||||||
|
content.setSize(contentArea.getWidth(), content.getHeight());
|
||||||
|
|
||||||
|
// Create FlexBox for responsive grid layout within content
|
||||||
|
flexBox = juce::FlexBox();
|
||||||
|
flexBox.flexWrap = juce::FlexBox::Wrap::wrap;
|
||||||
|
flexBox.justifyContent = juce::FlexBox::JustifyContent::spaceBetween;
|
||||||
|
flexBox.alignContent = juce::FlexBox::AlignContent::flexStart;
|
||||||
|
flexBox.flexDirection = juce::FlexBox::Direction::row;
|
||||||
|
|
||||||
|
// Determine fixed per-item width for this viewport width
|
||||||
|
const int viewW = contentArea.getWidth();
|
||||||
|
const int viewH = contentArea.getHeight();
|
||||||
|
const int itemsPerRow = juce::jmax(1, viewW / MIN_ITEM_WIDTH);
|
||||||
|
const int fixedItemWidth = (itemsPerRow > 0 ? viewW / itemsPerRow : viewW);
|
||||||
|
|
||||||
|
// Add each item with a fixed width, and pad the final row with placeholders so it's centered
|
||||||
|
const int total = items.size();
|
||||||
|
const int fullRows = (itemsPerRow > 0 ? total / itemsPerRow : 0);
|
||||||
|
const int remainder = (itemsPerRow > 0 ? total % itemsPerRow : 0);
|
||||||
|
|
||||||
|
auto addItemFlex = [&](juce::Component* c)
|
||||||
|
{
|
||||||
|
flexBox.items.add(juce::FlexItem(*c)
|
||||||
|
.withMinWidth((float) fixedItemWidth)
|
||||||
|
.withMaxWidth((float) fixedItemWidth)
|
||||||
|
.withHeight(80.0f)
|
||||||
|
.withFlex(1.0f)
|
||||||
|
.withMargin(juce::FlexItem::Margin(0)));
|
||||||
|
};
|
||||||
|
|
||||||
|
auto addPlaceholder = [&]()
|
||||||
|
{
|
||||||
|
// Placeholder occupies a slot visually but has no component; ensures last row is centered
|
||||||
|
juce::FlexItem placeholder((float) fixedItemWidth, 80.0f);
|
||||||
|
placeholder.flexGrow = 1.0f; // match item flex for consistent spacing
|
||||||
|
placeholder.margin = juce::FlexItem::Margin(0);
|
||||||
|
flexBox.items.add(std::move(placeholder));
|
||||||
|
};
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
// Add complete rows
|
||||||
|
for (int r = 0; r < fullRows; ++r)
|
||||||
|
for (int c = 0; c < itemsPerRow; ++c)
|
||||||
|
addItemFlex(items.getUnchecked(index++));
|
||||||
|
|
||||||
|
// Add last row centered with balanced placeholders
|
||||||
|
if (remainder > 0)
|
||||||
|
{
|
||||||
|
const int missing = itemsPerRow - remainder;
|
||||||
|
const int leftPad = missing / 2;
|
||||||
|
const int rightPad = missing - leftPad;
|
||||||
|
|
||||||
|
for (int i = 0; i < leftPad; ++i) addPlaceholder();
|
||||||
|
for (int i = 0; i < remainder; ++i) addItemFlex(items.getUnchecked(index++));
|
||||||
|
for (int i = 0; i < rightPad; ++i) addPlaceholder();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute required content height
|
||||||
|
const int requiredHeight = calculateRequiredHeight(viewW);
|
||||||
|
|
||||||
|
// If content is shorter than viewport, make content at least as tall as viewport
|
||||||
|
int yOffset = 0;
|
||||||
|
if (requiredHeight < viewH) {
|
||||||
|
content.setSize(viewW, viewH);
|
||||||
|
yOffset = (viewH - requiredHeight) / 2;
|
||||||
|
} else {
|
||||||
|
content.setSize(viewW, requiredHeight);
|
||||||
|
}
|
||||||
|
// Layout items within content at the computed offset
|
||||||
|
flexBox.performLayout(juce::Rectangle<float>(0.0f, (float) yOffset, (float) viewW, (float) requiredHeight));
|
||||||
|
|
||||||
|
// Layout bottom scroll fade over the viewport area
|
||||||
|
layoutScrollFadeIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
int GridComponent::calculateRequiredHeight(int availableWidth) const
|
||||||
|
{
|
||||||
|
if (items.isEmpty())
|
||||||
|
return 80; // ITEM_HEIGHT
|
||||||
|
|
||||||
|
// Calculate how many items can fit per row
|
||||||
|
int itemsPerRow = juce::jmax(1, availableWidth / MIN_ITEM_WIDTH);
|
||||||
|
|
||||||
|
// Calculate number of rows needed
|
||||||
|
int numRows = (items.size() + itemsPerRow - 1) / itemsPerRow; // Ceiling division
|
||||||
|
|
||||||
|
return numRows * 80; // ITEM_HEIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
void GridComponent::layoutScrollFadeIfNeeded()
|
||||||
|
{
|
||||||
|
layoutScrollFade(viewport.getBounds(), true, 48);
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
#pragma once
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
#include "ScrollFadeMixin.h"
|
||||||
|
#include "GridItemComponent.h"
|
||||||
|
|
||||||
|
// Generic grid component that owns and lays out GridItemComponent children
|
||||||
|
class GridComponent : public juce::Component, private ScrollFadeMixin
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GridComponent();
|
||||||
|
~GridComponent() override;
|
||||||
|
|
||||||
|
void paint(juce::Graphics& g) override;
|
||||||
|
void resized() override;
|
||||||
|
|
||||||
|
void clearItems();
|
||||||
|
void addItem(GridItemComponent* item); // takes ownership
|
||||||
|
juce::OwnedArray<GridItemComponent>& getItems() { return items; }
|
||||||
|
int calculateRequiredHeight(int availableWidth) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
juce::Viewport viewport; // scroll container
|
||||||
|
juce::Component content; // holds the grid items
|
||||||
|
juce::OwnedArray<GridItemComponent> items;
|
||||||
|
juce::FlexBox flexBox;
|
||||||
|
|
||||||
|
static constexpr int ITEM_HEIGHT = 80;
|
||||||
|
static constexpr int MIN_ITEM_WIDTH = 180;
|
||||||
|
|
||||||
|
void layoutScrollFadeIfNeeded();
|
||||||
|
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(GridComponent)
|
||||||
|
};
|
|
@ -0,0 +1,123 @@
|
||||||
|
#include "GridItemComponent.h"
|
||||||
|
|
||||||
|
GridItemComponent::GridItemComponent(const juce::String& name, const juce::String& icon, const juce::String& id)
|
||||||
|
: itemName(name), itemId(id)
|
||||||
|
{
|
||||||
|
juce::String iconSvg = icon;
|
||||||
|
if (icon.isEmpty()) {
|
||||||
|
// Default icon if none is provided
|
||||||
|
iconSvg = juce::String::createStringFromData(BinaryData::rotate_svg, BinaryData::rotate_svgSize);
|
||||||
|
}
|
||||||
|
iconButton = std::make_unique<SvgButton>(
|
||||||
|
"gridItemIcon",
|
||||||
|
iconSvg,
|
||||||
|
juce::Colours::white.withAlpha(0.7f)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Make the icon non-interactive since this is just a visual element
|
||||||
|
iconButton->setInterceptsMouseClicks(false, false);
|
||||||
|
addAndMakeVisible(*iconButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
GridItemComponent::~GridItemComponent() = default;
|
||||||
|
|
||||||
|
void GridItemComponent::paint(juce::Graphics& g)
|
||||||
|
{
|
||||||
|
auto bounds = getLocalBounds().toFloat().reduced(10);
|
||||||
|
|
||||||
|
// Get animation progress from inherited HoverAnimationMixin (disabled => no hover)
|
||||||
|
auto animationProgress = isEnabled() ? getAnimationProgress() : 0.0f;
|
||||||
|
|
||||||
|
// Apply upward shift based on animation progress
|
||||||
|
auto yOffset = -animationProgress * HOVER_LIFT_AMOUNT;
|
||||||
|
bounds = bounds.translated(0, yOffset);
|
||||||
|
|
||||||
|
// Draw drop shadow
|
||||||
|
if (animationProgress > 0.01f) {
|
||||||
|
juce::DropShadow shadow;
|
||||||
|
shadow.colour = juce::Colours::lime.withAlpha(animationProgress * 0.2f);
|
||||||
|
shadow.radius = 15 * animationProgress;
|
||||||
|
shadow.offset = juce::Point<int>(0, 4);
|
||||||
|
|
||||||
|
if (shadow.radius > 0) {
|
||||||
|
juce::Path shadowPath;
|
||||||
|
shadowPath.addRoundedRectangle(bounds.toFloat(), CORNER_RADIUS);
|
||||||
|
shadow.drawForPath(g, shadowPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw background with rounded corners - interpolate between normal and hover colors
|
||||||
|
juce::Colour normalBgColour = Colours::veryDark;
|
||||||
|
juce::Colour hoverBgColour = normalBgColour.brighter(0.05f);
|
||||||
|
juce::Colour bgColour = normalBgColour.interpolatedWith(hoverBgColour, animationProgress);
|
||||||
|
|
||||||
|
g.setColour(bgColour);
|
||||||
|
g.fillRoundedRectangle(bounds.toFloat(), CORNER_RADIUS);
|
||||||
|
|
||||||
|
// Draw colored outline
|
||||||
|
juce::Colour outlineColour = juce::Colour::fromRGB(160, 160, 160);
|
||||||
|
g.setColour(outlineColour.withAlpha(0.9f));
|
||||||
|
g.drawRoundedRectangle(bounds.toFloat(), CORNER_RADIUS, 1.0f);
|
||||||
|
|
||||||
|
// Create text area - now accounting for icon space on the left
|
||||||
|
auto textArea = bounds.reduced(8, 4);
|
||||||
|
textArea.removeFromLeft(28); // Remove space for icon (24px + 4px gap)
|
||||||
|
|
||||||
|
g.setColour(juce::Colours::white);
|
||||||
|
g.setFont(juce::FontOptions(16.0f, juce::Font::plain));
|
||||||
|
g.drawText(itemName, textArea, juce::Justification::centred, true);
|
||||||
|
|
||||||
|
// If disabled, draw a dark transparent overlay over the rounded rect to simplify visuals
|
||||||
|
if (! isEnabled()) {
|
||||||
|
g.setColour(juce::Colours::black.withAlpha(0.35f));
|
||||||
|
g.fillRoundedRectangle(bounds.toFloat(), CORNER_RADIUS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GridItemComponent::resized()
|
||||||
|
{
|
||||||
|
auto bounds = getLocalBounds().reduced(10);
|
||||||
|
|
||||||
|
// Reserve space for the icon on the left
|
||||||
|
auto iconArea = bounds.removeFromLeft(60); // 24px for icon
|
||||||
|
iconArea = iconArea.withSizeKeepingCentre(40, 40); // Make icon 20x20px
|
||||||
|
|
||||||
|
iconButton->setBounds(iconArea);
|
||||||
|
|
||||||
|
// Get animation progress and calculate Y offset
|
||||||
|
auto animationProgress = isEnabled() ? getAnimationProgress() : 0.0f;
|
||||||
|
auto yOffset = -animationProgress * HOVER_LIFT_AMOUNT;
|
||||||
|
|
||||||
|
iconButton->setTransform(juce::AffineTransform::translation(0, yOffset));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GridItemComponent::mouseDown(const juce::MouseEvent& event)
|
||||||
|
{
|
||||||
|
if (! isEnabled()) return;
|
||||||
|
// Extend base behavior to keep hover press animation
|
||||||
|
HoverAnimationMixin::mouseDown(event);
|
||||||
|
// Ensure any hover preview is cleared before permanently selecting/enabling the item
|
||||||
|
if (onHoverEnd) onHoverEnd();
|
||||||
|
if (onItemSelected) {
|
||||||
|
onItemSelected(itemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GridItemComponent::mouseMove(const juce::MouseEvent& event) {
|
||||||
|
setMouseCursor(isEnabled() ? juce::MouseCursor::PointingHandCursor : juce::MouseCursor::NormalCursor);
|
||||||
|
juce::Desktop::getInstance().getMainMouseSource().forceMouseCursorUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GridItemComponent::mouseEnter(const juce::MouseEvent& event)
|
||||||
|
{
|
||||||
|
HoverAnimationMixin::mouseEnter(event);
|
||||||
|
if (isEnabled() && onHoverStart)
|
||||||
|
onHoverStart(itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GridItemComponent::mouseExit(const juce::MouseEvent& event)
|
||||||
|
{
|
||||||
|
HoverAnimationMixin::mouseExit(event);
|
||||||
|
if (onHoverEnd)
|
||||||
|
onHoverEnd();
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
#pragma once
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
#include "../LookAndFeel.h"
|
||||||
|
#include "HoverAnimationMixin.h"
|
||||||
|
#include "SvgButton.h"
|
||||||
|
|
||||||
|
// Generic grid item with name, icon, and id
|
||||||
|
class GridItemComponent : public HoverAnimationMixin
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GridItemComponent(const juce::String& name, const juce::String& icon, const juce::String& id);
|
||||||
|
~GridItemComponent() override;
|
||||||
|
|
||||||
|
void paint(juce::Graphics& g) override;
|
||||||
|
void resized() override;
|
||||||
|
void mouseDown(const juce::MouseEvent& event) override;
|
||||||
|
void mouseMove(const juce::MouseEvent& event) override;
|
||||||
|
void mouseEnter(const juce::MouseEvent& event) override;
|
||||||
|
void mouseExit(const juce::MouseEvent& event) override;
|
||||||
|
|
||||||
|
const juce::String& getId() const { return itemId; }
|
||||||
|
const juce::String& getName() const { return itemName; }
|
||||||
|
|
||||||
|
std::function<void(const juce::String& id)> onItemSelected;
|
||||||
|
std::function<void(const juce::String& id)> onHoverStart;
|
||||||
|
std::function<void()> onHoverEnd;
|
||||||
|
|
||||||
|
private:
|
||||||
|
juce::String itemName;
|
||||||
|
juce::String itemId;
|
||||||
|
|
||||||
|
// Icon for the item
|
||||||
|
std::unique_ptr<SvgButton> iconButton;
|
||||||
|
|
||||||
|
static constexpr int CORNER_RADIUS = 8;
|
||||||
|
static constexpr float HOVER_LIFT_AMOUNT = 2.0f;
|
||||||
|
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(GridItemComponent)
|
||||||
|
};
|
|
@ -197,6 +197,13 @@
|
||||||
file="Source/components/EffectTypeItemComponent.h"/>
|
file="Source/components/EffectTypeItemComponent.h"/>
|
||||||
<FILE id="aEprcE" name="ErrorCodeEditorComponent.h" compile="0" resource="0"
|
<FILE id="aEprcE" name="ErrorCodeEditorComponent.h" compile="0" resource="0"
|
||||||
file="Source/components/ErrorCodeEditorComponent.h"/>
|
file="Source/components/ErrorCodeEditorComponent.h"/>
|
||||||
|
<FILE id="sqD2Zy" name="GridComponent.cpp" compile="1" resource="0"
|
||||||
|
file="Source/components/GridComponent.cpp"/>
|
||||||
|
<FILE id="QRwdXD" name="GridComponent.h" compile="0" resource="0" file="Source/components/GridComponent.h"/>
|
||||||
|
<FILE id="Dfsnmg" name="GridItemComponent.cpp" compile="1" resource="0"
|
||||||
|
file="Source/components/GridItemComponent.cpp"/>
|
||||||
|
<FILE id="MwZoTt" name="GridItemComponent.h" compile="0" resource="0"
|
||||||
|
file="Source/components/GridItemComponent.h"/>
|
||||||
<FILE id="SYA32K" name="HoverAnimationMixin.cpp" compile="1" resource="0"
|
<FILE id="SYA32K" name="HoverAnimationMixin.cpp" compile="1" resource="0"
|
||||||
file="Source/components/HoverAnimationMixin.cpp"/>
|
file="Source/components/HoverAnimationMixin.cpp"/>
|
||||||
<FILE id="JyPgec" name="HoverAnimationMixin.h" compile="0" resource="0"
|
<FILE id="JyPgec" name="HoverAnimationMixin.h" compile="0" resource="0"
|
||||||
|
|
Ładowanie…
Reference in New Issue