Make grid component more generic

pull/319/head
James H Ball 2025-08-17 20:53:49 +01:00
rodzic 13a7744fa3
commit ccfb8391be
8 zmienionych plików z 357 dodań i 126 usunięć

Wyświetl plik

@ -30,7 +30,7 @@ public:
std::make_shared<PerspectiveEffect>(),
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("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;

Wyświetl plik

@ -1,25 +1,24 @@
#include "EffectTypeGridComponent.h"
#include <JuceHeader.h>
#include "../LookAndFeel.h"
#include "GridComponent.h"
#include "GridItemComponent.h"
#include <unordered_set>
#include <algorithm>
#include <numeric>
// ===================== EffectTypeGridComponent (wrapper) =====================
EffectTypeGridComponent::EffectTypeGridComponent(OscirenderAudioProcessor& processor)
: audioProcessor(processor)
{
// 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);
setupEffectItems();
addAndMakeVisible(grid);
setSize(400, 200);
addAndMakeVisible(cancelButton);
cancelButton.onClick = [this]() {
if (onCanceled) onCanceled();
};
setupEffectItems();
refreshDisabledStates();
}
@ -27,10 +26,8 @@ EffectTypeGridComponent::~EffectTypeGridComponent() = default;
void EffectTypeGridComponent::setupEffectItems()
{
// Clear existing items
effectItems.clear();
content.removeAllChildren();
grid.clearItems();
// Get effect types directly from the audio processor's toggleableEffects
juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock);
const int n = (int) audioProcessor.toggleableEffects.size();
@ -52,11 +49,11 @@ void EffectTypeGridComponent::setupEffectItems()
// Extract effect name from the effect
juce::String effectName = effect->getName();
// Create new item component
auto* item = new EffectTypeItemComponent(effectName, effect->getIcon(), effect->getId());
// Create new generic item component
auto* item = new GridItemComponent(effectName, effect->getIcon(), effect->getId());
// Set up callback to forward effect selection
item->onEffectSelected = [this](const juce::String& effectId) {
// Set up callback to forward selection
item->onItemSelected = [this](const juce::String& effectId) {
if (onEffectSelected)
onEffectSelected(effectId);
};
@ -74,8 +71,7 @@ void EffectTypeGridComponent::setupEffectItems()
}
};
effectItems.add(item);
content.addAndMakeVisible(item);
grid.addItem(item);
}
}
@ -93,13 +89,11 @@ void EffectTypeGridComponent::refreshDisabledStates()
selectedIds.insert(eff->getId().toStdString());
}
}
for (auto* item : effectItems) {
const bool disable = selectedIds.find(item->getEffectId().toStdString()) != selectedIds.end();
for (auto* item : grid.getItems()) {
const bool disable = selectedIds.find(item->getId().toStdString()) != selectedIds.end();
item->setEnabled(! disable);
}
cancelButton.setVisible(anySelected);
// Update fade visibility/layout in case scrollability changed
layoutScrollFade(viewport.getBounds(), true, 48);
}
void EffectTypeGridComponent::paint(juce::Graphics& g)
@ -112,94 +106,5 @@ void EffectTypeGridComponent::resized()
auto bounds = getLocalBounds();
auto topBar = bounds.removeFromTop(30);
cancelButton.setBounds(topBar.removeFromRight(80).reduced(4));
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 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;
grid.setBounds(bounds);
}

Wyświetl plik

@ -1,10 +1,10 @@
#pragma once
#include <JuceHeader.h>
#include "../PluginProcessor.h"
#include "EffectTypeItemComponent.h"
#include "ScrollFadeMixin.h"
#include "GridComponent.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:
EffectTypeGridComponent(OscirenderAudioProcessor& processor);
@ -13,23 +13,16 @@ public:
void paint(juce::Graphics& g) override;
void resized() override;
int calculateRequiredHeight(int availableWidth) const;
std::function<void(const juce::String& effectId)> onEffectSelected;
std::function<void()> onCanceled; // optional cancel handler
void refreshDisabledStates(); // grey-out items that are already selected
private:
OscirenderAudioProcessor& audioProcessor;
juce::Viewport viewport; // scroll container
juce::Component content; // holds the grid items
juce::OwnedArray<EffectTypeItemComponent> effectItems;
juce::FlexBox flexBox;
GridComponent grid;
juce::TextButton cancelButton { "Cancel" };
static constexpr int ITEM_HEIGHT = 80;
static constexpr int MIN_ITEM_WIDTH = 180;
void setupEffectItems();
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EffectTypeGridComponent)
};

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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)
};

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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)
};

Wyświetl plik

@ -197,6 +197,13 @@
file="Source/components/EffectTypeItemComponent.h"/>
<FILE id="aEprcE" name="ErrorCodeEditorComponent.h" compile="0" resource="0"
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="Source/components/HoverAnimationMixin.cpp"/>
<FILE id="JyPgec" name="HoverAnimationMixin.h" compile="0" resource="0"