/* * DISTRHO Plugin Framework (DPF) * Copyright (C) 2012-2016 Filipe Coelho * * Permission to use, copy, modify, and/or distribute this software for any purpose with * or without fee is hereby granted, provided that the above copyright notice and this * permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ // This is a custom knob widget for DPF with numeric label for zam-plugins #ifndef DGL_ZAM_WIDGETS_HPP_INCLUDED #define DGL_ZAM_WIDGETS_HPP_INCLUDED #include "../dpf/dgl/Image.hpp" #include "../dpf/dgl/Widget.hpp" #include "../dpf/dgl/Window.hpp" #include "../dpf/dgl/NanoVG.hpp" #include "../dpf/dgl/src/Common.hpp" #include "../dpf/dgl/src/WidgetPrivateData.hpp" START_NAMESPACE_DGL // ----------------------------------------------------------------------- class ZamKnob : public Widget, public NanoVG { public: enum Orientation { Horizontal, Vertical }; class Callback { public: virtual ~Callback() {} virtual void imageKnobDragStarted(ZamKnob* imageKnob) = 0; virtual void imageKnobDragFinished(ZamKnob* imageKnob) = 0; virtual void imageKnobValueChanged(ZamKnob* imageKnob, float value) = 0; }; explicit ZamKnob(Window& parent, const Image& image, Orientation orientation = Vertical) noexcept; explicit ZamKnob(Widget* widget, const Image& image, Orientation orientation = Vertical) noexcept; explicit ZamKnob(const ZamKnob& imageKnob); ZamKnob& operator=(const ZamKnob& imageKnob); ~ZamKnob() override; float getValue() const noexcept; void setDefault(float def) noexcept; void setRange(float min, float max) noexcept; void setStep(float step) noexcept; void setScrollStep(float step) noexcept; void setLabel(bool label) noexcept; void setValue(float value, bool sendCallback = false) noexcept; void setUsingLogScale(bool yesNo) noexcept; void setCallback(Callback* callback) noexcept; void setOrientation(Orientation orientation) noexcept; void setRotationAngle(int angle); void setImageLayerCount(uint count) noexcept; protected: void onDisplay() override; bool onMouse(const MouseEvent&) override; bool onMotion(const MotionEvent&) override; bool onScroll(const ScrollEvent&) override; private: Image fImage; bool fLabel; float fMinimum; float fMaximum; float fScrollStep; float fStep; float fValue; float fValueDef; float fValueTmp; bool fUsingDefault; bool fUsingLog; Orientation fOrientation; int fRotationAngle; bool fDragging; int fLastX; int fLastY; Callback* fCallback; bool fIsImgVertical; uint fImgLayerWidth; uint fImgLayerHeight; uint fImgLayerCount; bool fIsReady; GLuint fTextureId; float _logscale(float value) const; float _invlogscale(float value) const; void labelDisplay(void); DISTRHO_LEAK_DETECTOR(ZamKnob) }; // ----------------------------------------------------------------------- ZamKnob::ZamKnob(Window& parent, const Image& image, Orientation orientation) noexcept : Widget(parent), NanoVG(CREATE_ANTIALIAS), fImage(image), fLabel(false), fMinimum(0.0f), fMaximum(1.0f), fScrollStep(0.0f), fStep(0.0f), fValue(0.5f), fValueDef(fValue), fValueTmp(fValue), fUsingDefault(false), fUsingLog(false), fOrientation(orientation), fRotationAngle(0), fDragging(false), fLastX(0), fLastY(0), fCallback(nullptr), fIsImgVertical(image.getHeight() > image.getWidth()), fImgLayerWidth(fIsImgVertical ? image.getWidth() : image.getHeight()), fImgLayerHeight(fImgLayerWidth), fImgLayerCount(fIsImgVertical ? image.getHeight()/fImgLayerHeight : image.getWidth()/fImgLayerWidth), fIsReady(false), fTextureId(0) { glGenTextures(1, &fTextureId); setSize(fImgLayerWidth, fImgLayerHeight); NanoVG::loadSharedResources(); } ZamKnob::ZamKnob(Widget* widget, const Image& image, Orientation orientation) noexcept : Widget(widget->getParentWindow()), NanoVG(CREATE_ANTIALIAS), fImage(image), fLabel(false), fMinimum(0.0f), fMaximum(1.0f), fScrollStep(0.0f), fStep(0.0f), fValue(0.5f), fValueDef(fValue), fValueTmp(fValue), fUsingDefault(false), fUsingLog(false), fOrientation(orientation), fRotationAngle(0), fDragging(false), fLastX(0), fLastY(0), fCallback(nullptr), fIsImgVertical(image.getHeight() > image.getWidth()), fImgLayerWidth(fIsImgVertical ? image.getWidth() : image.getHeight()), fImgLayerHeight(fImgLayerWidth), fImgLayerCount(fIsImgVertical ? image.getHeight()/fImgLayerHeight : image.getWidth()/fImgLayerWidth), fIsReady(false), fTextureId(0) { glGenTextures(1, &fTextureId); setSize(fImgLayerWidth, fImgLayerHeight); NanoVG::loadSharedResources(); } ZamKnob::ZamKnob(const ZamKnob& imageKnob) : Widget(imageKnob.getParentWindow()), NanoVG(CREATE_ANTIALIAS), fImage(imageKnob.fImage), fLabel(false), fMinimum(imageKnob.fMinimum), fMaximum(imageKnob.fMaximum), fScrollStep(imageKnob.fScrollStep), fStep(imageKnob.fStep), fValue(imageKnob.fValue), fValueDef(imageKnob.fValueDef), fValueTmp(fValue), fUsingDefault(imageKnob.fUsingDefault), fUsingLog(imageKnob.fUsingLog), fOrientation(imageKnob.fOrientation), fRotationAngle(imageKnob.fRotationAngle), fDragging(false), fLastX(0), fLastY(0), fCallback(imageKnob.fCallback), fIsImgVertical(imageKnob.fIsImgVertical), fImgLayerWidth(imageKnob.fImgLayerWidth), fImgLayerHeight(imageKnob.fImgLayerHeight), fImgLayerCount(imageKnob.fImgLayerCount), fIsReady(false), fTextureId(0) { glGenTextures(1, &fTextureId); setSize(fImgLayerWidth, fImgLayerHeight); NanoVG::loadSharedResources(); } ZamKnob& ZamKnob::operator=(const ZamKnob& imageKnob) { fImage = imageKnob.fImage; fMinimum = imageKnob.fMinimum; fMaximum = imageKnob.fMaximum; fScrollStep = imageKnob.fScrollStep; fStep = imageKnob.fStep; fValue = imageKnob.fValue; fValueDef = imageKnob.fValueDef; fValueTmp = fValue; fUsingDefault = imageKnob.fUsingDefault; fUsingLog = imageKnob.fUsingLog; fOrientation = imageKnob.fOrientation; fRotationAngle = imageKnob.fRotationAngle; fDragging = false; fLastX = 0; fLastY = 0; fCallback = imageKnob.fCallback; fIsImgVertical = imageKnob.fIsImgVertical; fImgLayerWidth = imageKnob.fImgLayerWidth; fImgLayerHeight = imageKnob.fImgLayerHeight; fImgLayerCount = imageKnob.fImgLayerCount; fIsReady = false; if (fTextureId != 0) { glDeleteTextures(1, &fTextureId); fTextureId = 0; } glGenTextures(1, &fTextureId); setSize(fImgLayerWidth, fImgLayerHeight); return *this; } ZamKnob::~ZamKnob() { if (fTextureId != 0) { glDeleteTextures(1, &fTextureId); fTextureId = 0; } } float ZamKnob::getValue() const noexcept { return fValue; } // NOTE: value is assumed to be scaled if using log void ZamKnob::setDefault(float value) noexcept { fValueDef = value; fUsingDefault = true; } void ZamKnob::setRange(float min, float max) noexcept { DISTRHO_SAFE_ASSERT_RETURN(max > min,); if (fValue < min) { fValue = min; repaint(); if (fCallback != nullptr) { try { fCallback->imageKnobValueChanged(this, fValue); } DISTRHO_SAFE_EXCEPTION("ZamKnob::setRange < min"); } } else if (fValue > max) { fValue = max; repaint(); if (fCallback != nullptr) { try { fCallback->imageKnobValueChanged(this, fValue); } DISTRHO_SAFE_EXCEPTION("ZamKnob::setRange > max"); } } fMinimum = min; fMaximum = max; } void ZamKnob::setScrollStep(float step) noexcept { fScrollStep = step; } void ZamKnob::setLabel(bool label) noexcept { fLabel = label; } void ZamKnob::setStep(float step) noexcept { fStep = step; } // NOTE: value is assumed to be scaled if using log void ZamKnob::setValue(float value, bool sendCallback) noexcept { if (d_isEqual(fValue, value)) return; fValue = value; if (d_isZero(fStep)) fValueTmp = value; if (fRotationAngle == 0) fIsReady = false; repaint(); if (sendCallback && fCallback != nullptr) { try { fCallback->imageKnobValueChanged(this, fValue); } DISTRHO_SAFE_EXCEPTION("ZamKnob::setValue"); } } void ZamKnob::setUsingLogScale(bool yesNo) noexcept { fUsingLog = yesNo; } void ZamKnob::setCallback(Callback* callback) noexcept { fCallback = callback; } void ZamKnob::setOrientation(Orientation orientation) noexcept { if (fOrientation == orientation) return; fOrientation = orientation; } void ZamKnob::setRotationAngle(int angle) { if (fRotationAngle == angle) return; fRotationAngle = angle; fIsReady = false; } void ZamKnob::setImageLayerCount(uint count) noexcept { DISTRHO_SAFE_ASSERT_RETURN(count > 1,); fImgLayerCount = count; if (fIsImgVertical) fImgLayerHeight = fImage.getHeight()/count; else fImgLayerWidth = fImage.getWidth()/count; setSize(fImgLayerWidth, fImgLayerHeight); } void ZamKnob::labelDisplay() { char txt[16]; float ww = (float)getParentWindow().getWidth(); float wh = (float)getParentWindow().getHeight(); float w = (float)fImage.getWidth(); float h = (float)fImage.getHeight(); NanoVG::beginFrame(ww, wh); if (fValue > 1000.f) { snprintf(txt, sizeof(txt), "%.0f", fValue); } else { snprintf(txt, sizeof(txt), "%.1f", fValue); } FontId fid = findFont(NANOVG_DEJAVU_SANS_TTF); fontFaceId(fid); fontSize(14.f); fillColor(255, 255, 255, 255); strokeColor(255, 255, 255, 255); textAlign(ALIGN_CENTER | ALIGN_MIDDLE); textLineHeight(14.f); text(w/2., h/2., txt, NULL); NanoVG::endFrame(); } void ZamKnob::onDisplay() { const float normValue = ((fUsingLog ? _invlogscale(fValue) : fValue) - fMinimum) / (fMaximum - fMinimum); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, fTextureId); if (! fIsReady) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); static const float trans[] = { 0.0f, 0.0f, 0.0f, 0.0f }; glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, trans); glPixelStorei(GL_PACK_ALIGNMENT, 1); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); uint imageDataOffset = 0; if (fRotationAngle == 0) { DISTRHO_SAFE_ASSERT_RETURN(fImgLayerCount > 0,); DISTRHO_SAFE_ASSERT_RETURN(normValue >= 0.0f,); const uint& v1(fIsImgVertical ? fImgLayerWidth : fImgLayerHeight); const uint& v2(fIsImgVertical ? fImgLayerHeight : fImgLayerWidth); const uint layerDataSize = v1 * v2 * ((fImage.getFormat() == GL_BGRA || fImage.getFormat() == GL_RGBA) ? 4 : 3); imageDataOffset = layerDataSize * uint(normValue * float(fImgLayerCount-1)); } glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, static_cast(getWidth()), static_cast(getHeight()), 0, fImage.getFormat(), fImage.getType(), fImage.getRawData() + imageDataOffset); fIsReady = true; } const int w = static_cast(getWidth()); const int h = static_cast(getHeight()); if (fRotationAngle != 0) { glPushMatrix(); const int w2 = w/2; const int h2 = h/2; glTranslatef(static_cast(w2), static_cast(h2), 0.0f); glRotatef(normValue*static_cast(fRotationAngle), 0.0f, 0.0f, 1.0f); Rectangle(-w2, -h2, w, h).draw(); glPopMatrix(); } else { Rectangle(0, 0, w, h).draw(); } glBindTexture(GL_TEXTURE_2D, 0); glDisable(GL_TEXTURE_2D); if (fLabel) labelDisplay(); } bool ZamKnob::onMouse(const MouseEvent& ev) { if (ev.button != 1) return false; if (ev.press) { if (! contains(ev.pos)) return false; if ((ev.mod & kModifierShift) != 0 && fUsingDefault) { setValue(fValueDef, true); fValueTmp = fValue; return true; } fDragging = true; fLastX = ev.pos.getX(); fLastY = ev.pos.getY(); if (fCallback != nullptr) fCallback->imageKnobDragStarted(this); return true; } else if (fDragging) { if (fCallback != nullptr) fCallback->imageKnobDragFinished(this); fDragging = false; return true; } return false; } bool ZamKnob::onMotion(const MotionEvent& ev) { if (! fDragging) return false; bool doVal = false; float d, value = 0.0f; if (fOrientation == ZamKnob::Horizontal) { if (const int movX = ev.pos.getX() - fLastX) { d = (ev.mod & kModifierControl) ? 2000.0f : 200.0f; value = (fUsingLog ? _invlogscale(fValueTmp) : fValueTmp) + (float(fMaximum - fMinimum) / d * float(movX)); doVal = true; } } else if (fOrientation == ZamKnob::Vertical) { if (const int movY = fLastY - ev.pos.getY()) { d = (ev.mod & kModifierControl) ? 2000.0f : 200.0f; value = (fUsingLog ? _invlogscale(fValueTmp) : fValueTmp) + (float(fMaximum - fMinimum) / d * float(movY)); doVal = true; } } if (! doVal) return false; if (fUsingLog) value = _logscale(value); if (value < fMinimum) { fValueTmp = value = fMinimum; } else if (value > fMaximum) { fValueTmp = value = fMaximum; } else if (d_isNotZero(fStep)) { fValueTmp = value; const float rest = std::fmod(value, fStep); value = value - rest + (rest > fStep/2.0f ? fStep : 0.0f); } setValue(value, true); fLastX = ev.pos.getX(); fLastY = ev.pos.getY(); return true; } bool ZamKnob::onScroll(const ScrollEvent& ev) { if (! contains(ev.pos)) return false; float value, d; if (d_isZero(fScrollStep)) { d = (ev.mod & kModifierControl) ? 2000.0f : 200.0f; value = (fUsingLog ? _invlogscale(fValueTmp) : fValueTmp) + (float(fMaximum - fMinimum) / d * 10.f * ev.delta.getY()); } else { d = (ev.mod & kModifierControl) ? fScrollStep * 0.1f : fScrollStep; value = (fUsingLog ? _invlogscale(fValueTmp + d * ev.delta.getY()) : fValueTmp + d * ev.delta.getY()); } if (fUsingLog) value = _logscale(value); if (value < fMinimum) { fValueTmp = value = fMinimum; } else if (value > fMaximum) { fValueTmp = value = fMaximum; } else if (d_isNotZero(fStep)) { fValueTmp = value; const float rest = std::fmod(value, fStep); value = value - rest + (rest > fStep/2.0f ? fStep : 0.0f); } setValue(value, true); return true; } // ----------------------------------------------------------------------- float ZamKnob::_logscale(float value) const { const float b = std::log(fMaximum/fMinimum)/(fMaximum-fMinimum); const float a = fMaximum/std::exp(fMaximum*b); if (value < fMinimum) value = fMinimum; if (value > fMaximum) value = fMaximum; return a * std::exp(b*value); } float ZamKnob::_invlogscale(float value) const { const float b = std::log(fMaximum/fMinimum)/(fMaximum-fMinimum); const float a = fMaximum/std::exp(fMaximum*b); if (value < fMinimum) value = fMinimum; if (value > fMaximum) value = fMaximum; return std::log(value/a)/b; } // ----------------------------------------------------------------------- class ZamSwitch : public Widget { public: class Callback { public: virtual ~Callback() {} virtual void imageSwitchClicked(ZamSwitch* imageButton, bool down) = 0; }; explicit ZamSwitch(Window& parent, const Image& imageNormal, const Image& imageDown) noexcept; explicit ZamSwitch(Widget* widget, const Image& imageNormal, const Image& imageDown) noexcept; explicit ZamSwitch(const ZamSwitch& imageSwitch) noexcept; ZamSwitch& operator=(const ZamSwitch& imageSwitch) noexcept; bool isDown() const noexcept; void setDown(bool down) noexcept; void setCallback(Callback* callback) noexcept; protected: void onDisplay() override; bool onMouse(const MouseEvent&) override; private: Image fImageNormal; Image fImageDown; bool fIsDown; Callback* fCallback; DISTRHO_LEAK_DETECTOR(ZamSwitch) }; ZamSwitch::ZamSwitch(Window& parent, const Image& imageNormal, const Image& imageDown) noexcept : Widget(parent), fImageNormal(imageNormal), fImageDown(imageDown), fIsDown(false), fCallback(nullptr) { DISTRHO_SAFE_ASSERT(fImageNormal.getSize() == fImageDown.getSize()); setSize(fImageNormal.getSize()); } ZamSwitch::ZamSwitch(Widget* widget, const Image& imageNormal, const Image& imageDown) noexcept : Widget(widget->getParentWindow()), fImageNormal(imageNormal), fImageDown(imageDown), fIsDown(false), fCallback(nullptr) { DISTRHO_SAFE_ASSERT(fImageNormal.getSize() == fImageDown.getSize()); setSize(fImageNormal.getSize()); } ZamSwitch::ZamSwitch(const ZamSwitch& imageSwitch) noexcept : Widget(imageSwitch.getParentWindow()), fImageNormal(imageSwitch.fImageNormal), fImageDown(imageSwitch.fImageDown), fIsDown(imageSwitch.fIsDown), fCallback(imageSwitch.fCallback) { DISTRHO_SAFE_ASSERT(fImageNormal.getSize() == fImageDown.getSize()); setSize(fImageNormal.getSize()); } ZamSwitch& ZamSwitch::operator=(const ZamSwitch& imageSwitch) noexcept { fImageNormal = imageSwitch.fImageNormal; fImageDown = imageSwitch.fImageDown; fIsDown = imageSwitch.fIsDown; fCallback = imageSwitch.fCallback; DISTRHO_SAFE_ASSERT(fImageNormal.getSize() == fImageDown.getSize()); setSize(fImageNormal.getSize()); return *this; } bool ZamSwitch::isDown() const noexcept { return fIsDown; } void ZamSwitch::setDown(bool down) noexcept { fIsDown = down; } void ZamSwitch::setCallback(Callback* callback) noexcept { fCallback = callback; } void ZamSwitch::onDisplay() { if (fIsDown) fImageDown.draw(); else fImageNormal.draw(); } bool ZamSwitch::onMouse(const MouseEvent& ev) { if (ev.press && contains(ev.pos)) { fIsDown = true; repaint(); if (fCallback != nullptr) fCallback->imageSwitchClicked(this, true); return true; } return false; } END_NAMESPACE_DGL #endif