diff --git a/examples/component-demo/SSTJuceGuiDemo.cpp b/examples/component-demo/SSTJuceGuiDemo.cpp index 3493387..72c9df1 100644 --- a/examples/component-demo/SSTJuceGuiDemo.cpp +++ b/examples/component-demo/SSTJuceGuiDemo.cpp @@ -32,6 +32,7 @@ #include "TextPushButtonDemo.h" #include "SevenSegmentDemo.h" #include "VUMeterDemo.h" +#include "ZoomContainerDemo.h" struct SSTJuceGuiDemo : public juce::JUCEApplication { @@ -131,6 +132,7 @@ struct SSTJuceGuiDemo : public juce::JUCEApplication mk(); mk(); mk(); + mk(); } void paint(juce::Graphics &g) override { g.fillAll(juce::Colours::black); } void resized() override diff --git a/examples/component-demo/ZoomContainerDemo.h b/examples/component-demo/ZoomContainerDemo.h new file mode 100644 index 0000000..16d5cba --- /dev/null +++ b/examples/component-demo/ZoomContainerDemo.h @@ -0,0 +1,136 @@ +/* + * sst-jucegui - an open source library of juce widgets + * built by Surge Synth Team. + * + * Copyright 2023-2024, various authors, as described in the GitHub + * transaction log. + * + * sst-jucegui is released under the MIT license, as described + * by "LICENSE.md" in this repository. This means you may use this + * in commercial software if you are a JUCE Licensee. If you use JUCE + * in the open source / GPL3 context, your combined work must be + * released under GPL3. + * + * All source in sst-jucegui available at + * https://github.com/surge-synthesizer/sst-jucegui + */ + +#ifndef SSTJUCEGUI_EXAMPLES_COMPONENT_DEMO_ZOOMCONTAINERDEMO_H +#define SSTJUCEGUI_EXAMPLES_COMPONENT_DEMO_ZOOMCONTAINERDEMO_H + +#include +#include +#include +#include +#include "ExampleUtils.h" +#include + +struct ZoomContainerDemo : public sst::jucegui::components::WindowPanel +{ + static constexpr const char *name = "ZoomContainer"; + + template + struct PaintGrid : juce::Component, sst::jucegui::components::ZoomContainerClient + { + juce::Component *associatedComponent() override { return this; } + bool supportsVerticalZoom() const override { return DO_V; } + bool supportsHorizontalZoom() const override { return DO_H; } + float hZoomStart{0.f}, hZoomScale{1.f}; + float vZoomStart{0.f}, vZoomScale{1.f}; + void setHorizontalZoom(float pctStart, float zoomFactor) override + { + assert(DO_H); + hZoomStart = pctStart; + hZoomScale = zoomFactor; + repaint(); + } + void setVerticalZoom(float pctStart, float zoomFactor) override + { + assert(DO_V); + vZoomStart = pctStart; + vZoomScale = zoomFactor; + repaint(); + } + + void paint(juce::Graphics &g) override + { + g.fillAll(juce::Colour(40, 40, 70)); + + if (DO_H) + { + // q = (i-zoomStart) * scale + // q == 0 -> i = zoomStart + // q = 1 -> i = 1/scale + zoomStart + // but q is stepping in 0.25 so if we start at q=0 we + // quantize. So we want to start at the 0.025 below + // zoomstart + auto start = std::floor(hZoomStart * 20) / 20; + auto end = 1.0 / hZoomScale + hZoomStart + 0.025; + bool first{true}; + for (float i = start; i < end; i += 0.025) + { + auto q = (i - hZoomStart) * hZoomScale; + g.setColour(juce::Colours::red); + g.drawVerticalLine(q * getWidth(), 0, getHeight()); + g.setFont(12); + g.setColour(juce::Colours::white); + g.drawText(std::to_string((int)(i / 0.025)), q * getWidth(), 0, 60, 20, + juce::Justification::centredLeft); + } + } + + if (DO_V) + { + // q = (i-zoomStart) * scale + // q == 0 -> i = zoomStart + // q = 1 -> i = 1/scale + zoomStart + // but q is stepping in 0.25 so if we start at q=0 we + // quantize. So we want to start at the 0.025 below + // zoomstart + auto start = std::floor(vZoomStart * 20) / 20; + auto end = 1.0 / vZoomScale + vZoomStart + 0.025; + bool first{true}; + for (float i = start; i < end; i += 0.025) + { + auto q = (i - vZoomStart) * vZoomScale; + g.setColour(juce::Colours::green); + g.drawHorizontalLine(q * getHeight(), 0, getWidth()); + g.setFont(12); + g.setColour(juce::Colours::white); + g.drawText(std::to_string((int)(i / 0.025)), 0, q * getHeight(), 60, 20, + juce::Justification::centredLeft); + } + } + g.setColour(juce::Colours::yellow); + g.drawRect(getLocalBounds()); + } + }; + + ZoomContainerDemo() + { + panelOne = std::make_unique("ZoomThis"); + panelContents = std::make_unique( + std::make_unique>()); + lowerPanelContents = std::make_unique( + std::make_unique>()); + addAndMakeVisible(*panelOne); + panelOne->addAndMakeVisible(*panelContents); + panelOne->addAndMakeVisible(*lowerPanelContents); + } + + void resized() override + { + panelOne->setBounds(getLocalBounds().reduced(10)); + auto plb = panelOne->getLocalBounds().reduced(20); + + panelContents->setBounds(plb.withHeight(plb.getHeight() / 2).reduced(10)); + lowerPanelContents->setBounds( + plb.withHeight(plb.getHeight() / 2).translated(0, plb.getHeight() / 2).reduced(10)); + } + + std::unique_ptr panelOne; + std::unique_ptr panelContents; + std::unique_ptr lowerPanelContents; +}; + +#endif // SST_JUCEGUI_ZOOMCONTAINER_H diff --git a/include/sst/jucegui/components/ZoomContainer.h b/include/sst/jucegui/components/ZoomContainer.h new file mode 100644 index 0000000..8075463 --- /dev/null +++ b/include/sst/jucegui/components/ZoomContainer.h @@ -0,0 +1,204 @@ +/* + * sst-jucegui - an open source library of juce widgets + * built by Surge Synth Team. + * + * Copyright 2023-2024, various authors, as described in the GitHub + * transaction log. + * + * sst-jucegui is released under the MIT license, as described + * by "LICENSE.md" in this repository. This means you may use this + * in commercial software if you are a JUCE Licensee. If you use JUCE + * in the open source / GPL3 context, your combined work must be + * released under GPL3. + * + * All source in sst-jucegui available at + * https://github.com/surge-synthesizer/sst-jucegui + */ + +#ifndef INCLUDE_SST_JUCEGUI_COMPONENTS_ZOOMCONTAINER_H +#define INCLUDE_SST_JUCEGUI_COMPONENTS_ZOOMCONTAINER_H + +#include +#include +#include "ScrollBar.h" + +namespace sst::jucegui::components +{ +struct ZoomContainerClient +{ + virtual ~ZoomContainerClient() = default; + virtual juce::Component *associatedComponent() = 0; + virtual bool supportsVerticalZoom() const = 0; + virtual bool supportsHorizontalZoom() const = 0; + virtual void setHorizontalZoom(float pctStart, float zoomFactor) {} + virtual void setVerticalZoom(float pctStart, float zoomFactor) {} +}; +struct ZoomContainer : juce::Component, juce::ScrollBar::Listener +{ + std::unique_ptr vScroll, hScroll; + std::unique_ptr contents; + ZoomContainer(std::unique_ptr &&c) : contents(std::move(c)) + { + if (contents->supportsHorizontalZoom()) + { + hScroll = std::make_unique(false); + hScroll->setAutoHide(false); + hScroll->setRangeLimits(0.0, 1.0, juce::NotificationType::dontSendNotification); + hScroll->addListener(this); + addAndMakeVisible(*hScroll); + } + if (contents->supportsVerticalZoom()) + { + vScroll = std::make_unique(true); + vScroll->setAutoHide(false); + vScroll->setRangeLimits(0.0, 1.0, juce::NotificationType::dontSendNotification); + vScroll->addListener(this); + addAndMakeVisible(*vScroll); + } + addAndMakeVisible(*(contents->associatedComponent())); + } + + static constexpr int sbw{6}; + + void resized() override + { + auto bx = getLocalBounds(); + + if (hScroll && vScroll) + { + auto hb = bx.withTrimmedTop(bx.getHeight() - sbw).withTrimmedRight(sbw); + auto vb = bx.withTrimmedLeft(bx.getWidth() - sbw).withTrimmedBottom(sbw); + bx = bx.withTrimmedBottom(sbw).withTrimmedRight(sbw); + hScroll->setBounds(hb); + vScroll->setBounds(vb); + } + else if (hScroll) + { + auto hb = bx.withTrimmedTop(bx.getHeight() - sbw); + bx = bx.withTrimmedBottom(sbw); + hScroll->setBounds(hb); + } + else if (vScroll) + { + auto vb = bx.withTrimmedLeft(bx.getWidth() - sbw); + bx = bx.withTrimmedRight(sbw); + vScroll->setBounds(vb); + } + contents->associatedComponent()->setBounds(bx); + } + + void mouseMagnify(const juce::MouseEvent &event, float scaleFactor) override + { + if (event.mods.isShiftDown()) + { + adjustVerticalZoom(event.position, scaleFactor); + } + else + { + adjustHorizontalZoom(event.position, scaleFactor); + } + Component::mouseMagnify(event, scaleFactor); + } + + void mouseWheelMove(const juce::MouseEvent &event, + const juce::MouseWheelDetails &wheel) override + { + if (event.mods.isShiftDown() && event.mods.isAltDown()) + { + // HZOM by -Delta Y + if (contents->supportsHorizontalZoom()) + { + adjustHorizontalZoom(event.position, 1.0 - wheel.deltaY); + } + return; + } + if (event.mods.isShiftDown()) + { + // VZoom by delta Y + if (contents->supportsVerticalZoom()) + { + adjustVerticalZoom(event.position, 1.0 - wheel.deltaY); + } + return; + } + + // Assume a 2D mouse so do what you think. This is just a joy on + // a mac trackpad! + if (fabs(wheel.deltaX) > fabs(wheel.deltaY)) + { + if (hScroll) + { + auto dy = wheel.deltaX; + auto rs = hScroll->getCurrentRangeStart(); + rs = std::clamp(rs + dy, 0., 1.); + hScroll->setCurrentRangeStart(rs); + } + } + else + { + if (vScroll) + { + auto dy = wheel.deltaY; + auto rs = vScroll->getCurrentRangeStart(); + rs = std::clamp(rs + dy, 0., 1.); + vScroll->setCurrentRangeStart(rs); + } + } + } + + void scrollBarMoved(juce::ScrollBar *scrollBarThatHasMoved, double newRangeStart) override + { + if (scrollBarThatHasMoved == hScroll.get()) + { + contents->setHorizontalZoom(newRangeStart, 1.0 / hScroll->getCurrentRangeSize()); + } + if (scrollBarThatHasMoved == vScroll.get()) + { + contents->setVerticalZoom(newRangeStart, 1.0 / vScroll->getCurrentRangeSize()); + } + } + void adjustVerticalZoom(const juce::Point &p, float scaleFactor) + { + auto rs = vScroll->getCurrentRangeStart(); + auto re = vScroll->getCurrentRangeSize(); + + auto nre = re / scaleFactor; + auto dre = re - nre; + re = nre; + + auto mfac = std::clamp(p.getY() / (getHeight() - (hScroll ? sbw : 0)), 0.f, 1.f); + + rs += dre * mfac; + + rs = std::clamp(rs, 0., 1.); + re = std::clamp(re, 0., 1.); + + contents->setVerticalZoom(rs, 1.0 / re); + + vScroll->setCurrentRange(rs, re, juce::NotificationType::dontSendNotification); + vScroll->repaint(); + } + void adjustHorizontalZoom(const juce::Point &p, float scaleFactor) + { + auto rs = hScroll->getCurrentRangeStart(); + auto re = hScroll->getCurrentRangeSize(); + + auto nre = re / scaleFactor; + auto dre = re - nre; + re = nre; + + auto mfac = std::clamp(p.getX() / (getWidth() - (vScroll ? sbw : 0)), 0.f, 1.f); + + rs += dre * mfac; + + rs = std::clamp(rs, 0., 1.); + re = std::clamp(re, 0., 1.); + + contents->setHorizontalZoom(rs, 1.0 / re); + + hScroll->setCurrentRange(rs, re, juce::NotificationType::dontSendNotification); + hScroll->repaint(); + } +}; +} // namespace sst::jucegui::components +#endif // SST_JUCEGUI_ZOOMCONTAINER_H