Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A zoom container which isn't a viewport #121

Merged
merged 1 commit into from
Aug 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/component-demo/SSTJuceGuiDemo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include "TextPushButtonDemo.h"
#include "SevenSegmentDemo.h"
#include "VUMeterDemo.h"
#include "ZoomContainerDemo.h"

struct SSTJuceGuiDemo : public juce::JUCEApplication
{
Expand Down Expand Up @@ -131,6 +132,7 @@ struct SSTJuceGuiDemo : public juce::JUCEApplication
mk<SevenSegmentDemo>();
mk<VUMeterDemo>();
mk<GlyphDemo>();
mk<ZoomContainerDemo>();
}
void paint(juce::Graphics &g) override { g.fillAll(juce::Colours::black); }
void resized() override
Expand Down
136 changes: 136 additions & 0 deletions examples/component-demo/ZoomContainerDemo.h
Original file line number Diff line number Diff line change
@@ -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 <sst/jucegui/components/ZoomContainer.h>
#include <sst/jucegui/components/NamedPanel.h>
#include <sst/jucegui/components/WindowPanel.h>
#include <sst/jucegui/util/DebugHelpers.h>
#include "ExampleUtils.h"
#include <cassert>

struct ZoomContainerDemo : public sst::jucegui::components::WindowPanel
{
static constexpr const char *name = "ZoomContainer";

template <bool DO_H, bool DO_V>
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<sst::jucegui::components::NamedPanel>("ZoomThis");
panelContents = std::make_unique<sst::jucegui::components::ZoomContainer>(
std::make_unique<PaintGrid<true, false>>());
lowerPanelContents = std::make_unique<sst::jucegui::components::ZoomContainer>(
std::make_unique<PaintGrid<true, true>>());
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<sst::jucegui::components::NamedPanel> panelOne;
std::unique_ptr<sst::jucegui::components::ZoomContainer> panelContents;
std::unique_ptr<sst::jucegui::components::ZoomContainer> lowerPanelContents;
};

#endif // SST_JUCEGUI_ZOOMCONTAINER_H
204 changes: 204 additions & 0 deletions include/sst/jucegui/components/ZoomContainer.h
Original file line number Diff line number Diff line change
@@ -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 <memory>
#include <juce_gui_basics/juce_gui_basics.h>
#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<ScrollBar> vScroll, hScroll;
std::unique_ptr<ZoomContainerClient> contents;
ZoomContainer(std::unique_ptr<ZoomContainerClient> &&c) : contents(std::move(c))
{
if (contents->supportsHorizontalZoom())
{
hScroll = std::make_unique<ScrollBar>(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<ScrollBar>(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<float> &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<float> &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
Loading